From 6099538fa8cbf467f85de824e563f024304fc443 Mon Sep 17 00:00:00 2001 From: Heavy Date: Thu, 12 Feb 2026 15:47:52 +0000 Subject: [PATCH] add Block + Component, add TextComponent with min, max and regex validators and early NumberComponent --- go.mod | 3 + internal/block/block.go | 42 +++++++++ internal/block/component.go | 27 ++++++ internal/block/number_component.go | 54 ++++++++++++ .../block/text_component/text_component.go | 85 +++++++++++++++++++ .../text_component/validators/max_length.go | 21 +++++ .../validators/max_length_test.go | 36 ++++++++ .../text_component/validators/min_length.go | 21 +++++ .../validators/min_length_test.go | 36 ++++++++ .../block/text_component/validators/regex.go | 24 ++++++ .../text_component/validators/regex_test.go | 33 +++++++ internal/block/validation/component_option.go | 7 ++ internal/block/validation/required.go | 14 +++ internal/block/validation/required_test.go | 35 ++++++++ internal/block/validation/validator.go | 3 + main.go | 47 ++++++++++ 16 files changed, 488 insertions(+) create mode 100644 go.mod create mode 100644 internal/block/block.go create mode 100644 internal/block/component.go create mode 100644 internal/block/number_component.go create mode 100644 internal/block/text_component/text_component.go create mode 100644 internal/block/text_component/validators/max_length.go create mode 100644 internal/block/text_component/validators/max_length_test.go create mode 100644 internal/block/text_component/validators/min_length.go create mode 100644 internal/block/text_component/validators/min_length_test.go create mode 100644 internal/block/text_component/validators/regex.go create mode 100644 internal/block/text_component/validators/regex_test.go create mode 100644 internal/block/validation/component_option.go create mode 100644 internal/block/validation/required.go create mode 100644 internal/block/validation/required_test.go create mode 100644 internal/block/validation/validator.go create mode 100644 main.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b844a60 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module blocky + +go 1.25 diff --git a/internal/block/block.go b/internal/block/block.go new file mode 100644 index 0000000..19022dc --- /dev/null +++ b/internal/block/block.go @@ -0,0 +1,42 @@ +package block + +import ( + "encoding/json" + "fmt" +) + +type Block struct { + Components []Component +} + +type BlockInstance struct { + Components []ComponentInstance +} + +func (b *BlockInstance) FromJSON(block *Block, data []byte) error { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + var components []ComponentInstance + + for _, c := range block.Components { + jsonValue, ok := raw[c.Identifier()] + if !ok { + continue + } + instance := c.NewInstance() + + if err := instance.FromJSON(c, jsonValue); err != nil { + return err + } + components = append(components, instance) + + fmt.Println(c.Identifier(), instance) + } + + b.Components = components + + return nil +} diff --git a/internal/block/component.go b/internal/block/component.go new file mode 100644 index 0000000..ab534d0 --- /dev/null +++ b/internal/block/component.go @@ -0,0 +1,27 @@ +package block + +import ( + "blocky/internal/block/validation" + "encoding/json" +) + +type ComponentType int8 + +const ( + _ ComponentType = iota + TextComponentType + NumberComponentType + Date +) + +type Component interface { + Type() ComponentType + Identifier() string + Name() string + NewInstance() ComponentInstance + Validators() []validation.Validator +} + +type ComponentInstance interface { + FromJSON(component Component, data json.RawMessage) error +} diff --git a/internal/block/number_component.go b/internal/block/number_component.go new file mode 100644 index 0000000..72f2de0 --- /dev/null +++ b/internal/block/number_component.go @@ -0,0 +1,54 @@ +package block + +import ( + "blocky/internal/block/validation" + "encoding/json" +) + +type NumberComponent struct { + identifier string + name string + validators []validation.Validator +} + +func (t NumberComponent) Type() ComponentType { + return NumberComponentType +} + +func (t NumberComponent) Name() string { + return t.name +} + +func (t NumberComponent) Identifier() string { + return t.identifier +} + +func (t NumberComponent) Validators() []validation.Validator { + return t.validators +} + +func NewNumberComponent(identifier, name string, opts ...validation.ComponentOption) NumberComponent { + cfg := &validation.ComponentConfig{} + for _, opt := range opts { + opt(cfg) + } + return NumberComponent{identifier: identifier, name: name, validators: cfg.Validators} +} + +func (t NumberComponent) NewInstance() ComponentInstance { + return &NumberComponentInstance{} +} + +type NumberComponentInstance struct { + value int64 +} + +func (t *NumberComponentInstance) FromJSON(component Component, data json.RawMessage) error { + var s int64 + err := json.Unmarshal(data, &s) + if err != nil { + return err + } + t.value = s + return nil +} diff --git a/internal/block/text_component/text_component.go b/internal/block/text_component/text_component.go new file mode 100644 index 0000000..7dbef60 --- /dev/null +++ b/internal/block/text_component/text_component.go @@ -0,0 +1,85 @@ +package text_component + +import ( + "blocky/internal/block" + "blocky/internal/block/validation" + "encoding/json" + "fmt" +) + +type TextComponent struct { + identifier string + name string + validators []validation.Validator + TextValidators []TextValidator +} + +func (t TextComponent) Type() block.ComponentType { + return block.TextComponentType +} + +func (t TextComponent) Name() string { + return t.name +} + +func (t TextComponent) Identifier() string { + return t.identifier +} + +func (t TextComponent) Validators() []validation.Validator { + return t.validators +} + +func NewTextComponent(identifier, name string, validators []validation.ComponentOption, textValidators []TextComponentOption) TextComponent { + cfg := &validation.ComponentConfig{} + for _, opt := range validators { + opt(cfg) + } + + textCfg := &TextComponentConfig{} + for _, opt := range textValidators { + opt(textCfg) + } + + return TextComponent{identifier: identifier, name: name, validators: cfg.Validators, TextValidators: textCfg.Validators} +} + +func (t TextComponent) NewInstance() block.ComponentInstance { + return &TextComponentInstance{} +} + +type TextComponentInstance struct { + value string +} + +func (t *TextComponentInstance) FromJSON(component block.Component, data json.RawMessage) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return err + } + t.value = s + + for _, v := range component.Validators() { + if err := v(s); err != nil { + return fmt.Errorf("%s: %w", component.Identifier(), err) + } + } + + if tc, ok := component.(TextComponent); ok { + for _, v := range tc.TextValidators { + if err := v(s); err != nil { + return fmt.Errorf("%s: %w", component.Identifier(), err) + } + } + } + + return nil +} + +type TextValidator func(value any) error +type TextComponentConfig struct { + Validators []TextValidator +} + +type TextComponentOption func(*TextComponentConfig) diff --git a/internal/block/text_component/validators/max_length.go b/internal/block/text_component/validators/max_length.go new file mode 100644 index 0000000..2a31b21 --- /dev/null +++ b/internal/block/text_component/validators/max_length.go @@ -0,0 +1,21 @@ +package text_component_validators + +import ( + "blocky/internal/block/text_component" + "fmt" +) + +func MaxLength(max int) text_component.TextComponentOption { + return func(c *text_component.TextComponentConfig) { + c.Validators = append(c.Validators, func(value any) error { + s, ok := value.(string) + if !ok { + return fmt.Errorf("provided value bust be a string: %v", value) + } + if len(s) > max { + return fmt.Errorf("must be at most %d characters", max) + } + return nil + }) + } +} diff --git a/internal/block/text_component/validators/max_length_test.go b/internal/block/text_component/validators/max_length_test.go new file mode 100644 index 0000000..44aa9b8 --- /dev/null +++ b/internal/block/text_component/validators/max_length_test.go @@ -0,0 +1,36 @@ +package text_component_validators + +import ( + "blocky/internal/block/text_component" + "testing" +) + +func TestMaxLength(t *testing.T) { + cfg := &text_component.TextComponentConfig{} + MaxLength(10)(cfg) + + if len(cfg.Validators) != 1 { + t.Fatalf("expect 1 validator, got %d", len(cfg.Validators)) + } + + validate := cfg.Validators[0] + + tests := []struct { + name string + value string + wantErr bool + }{ + {"below value", "01234", false}, + {"equal to value", "0123456789", false}, + {"above value", "abcdefghjilkmnopqrstuvwxyz", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validate(tt.value) + if (err != nil) != tt.wantErr { + t.Errorf("validate(%v) error = %v, wantErr %v", tt.value, err, tt.wantErr) + } + }) + } +} diff --git a/internal/block/text_component/validators/min_length.go b/internal/block/text_component/validators/min_length.go new file mode 100644 index 0000000..db86ccf --- /dev/null +++ b/internal/block/text_component/validators/min_length.go @@ -0,0 +1,21 @@ +package text_component_validators + +import ( + "blocky/internal/block/text_component" + "fmt" +) + +func MinLength(min int) text_component.TextComponentOption { + return func(c *text_component.TextComponentConfig) { + c.Validators = append(c.Validators, func(value any) error { + s, ok := value.(string) + if !ok { + return fmt.Errorf("provided value bust be a string: %v", value) + } + if len(s) < min { + return fmt.Errorf("must be at least %d characters", min) + } + return nil + }) + } +} diff --git a/internal/block/text_component/validators/min_length_test.go b/internal/block/text_component/validators/min_length_test.go new file mode 100644 index 0000000..9f9bd9b --- /dev/null +++ b/internal/block/text_component/validators/min_length_test.go @@ -0,0 +1,36 @@ +package text_component_validators + +import ( + "blocky/internal/block/text_component" + "testing" +) + +func TestMinLength(t *testing.T) { + cfg := &text_component.TextComponentConfig{} + MinLength(10)(cfg) + + if len(cfg.Validators) != 1 { + t.Fatalf("expect 1 validator, got %d", len(cfg.Validators)) + } + + validate := cfg.Validators[0] + + tests := []struct { + name string + value string + wantErr bool + }{ + {"below value", "01234", true}, + {"equal to value", "0123456789", false}, + {"above value", "abcdefghjilkmnopqrstuvwxyz", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validate(tt.value) + if (err != nil) != tt.wantErr { + t.Errorf("validate(%v) error = %v, wantErr %v", tt.value, err, tt.wantErr) + } + }) + } +} diff --git a/internal/block/text_component/validators/regex.go b/internal/block/text_component/validators/regex.go new file mode 100644 index 0000000..4950a8b --- /dev/null +++ b/internal/block/text_component/validators/regex.go @@ -0,0 +1,24 @@ +package text_component_validators + +import ( + "blocky/internal/block/text_component" + "fmt" + "regexp" +) + +func Regex(pattern string) text_component.TextComponentOption { + re := regexp.MustCompile(pattern) + + return func(c *text_component.TextComponentConfig) { + c.Validators = append(c.Validators, func(value any) error { + s, ok := value.(string) + if !ok { + return fmt.Errorf("provided value bust be a string: %v", value) + } + if !re.MatchString(s) { + return fmt.Errorf("must match pattern %s", pattern) + } + return nil + }) + } +} diff --git a/internal/block/text_component/validators/regex_test.go b/internal/block/text_component/validators/regex_test.go new file mode 100644 index 0000000..738853a --- /dev/null +++ b/internal/block/text_component/validators/regex_test.go @@ -0,0 +1,33 @@ +package text_component_validators + +import ( + "blocky/internal/block/text_component" + "testing" +) + +func TestRegex(t *testing.T) { + cfg := &text_component.TextComponentConfig{} + Regex("^\\d+$")(cfg) + + validate := cfg.Validators[0] + + tests := []struct { + name string + value string + wantErr bool + }{ + {"just numbers", "123", false}, + {"just letters", "abc", true}, + {"numbers and letters", "abc", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validate(tt.value) + if (err != nil) != tt.wantErr { + t.Errorf("validate(%v) error = %v, wantErr %v", tt.value, err, tt.wantErr) + } + }) + } + +} diff --git a/internal/block/validation/component_option.go b/internal/block/validation/component_option.go new file mode 100644 index 0000000..8f4fb23 --- /dev/null +++ b/internal/block/validation/component_option.go @@ -0,0 +1,7 @@ +package validation + +type ComponentOption func(*ComponentConfig) + +type ComponentConfig struct { + Validators []Validator +} diff --git a/internal/block/validation/required.go b/internal/block/validation/required.go new file mode 100644 index 0000000..f9707bc --- /dev/null +++ b/internal/block/validation/required.go @@ -0,0 +1,14 @@ +package validation + +import "fmt" + +func Required() ComponentOption { + return func(c *ComponentConfig) { + c.Validators = append(c.Validators, func(value any) error { + if value == nil || value == "" { + return fmt.Errorf("field is required") + } + return nil + }) + } +} diff --git a/internal/block/validation/required_test.go b/internal/block/validation/required_test.go new file mode 100644 index 0000000..95249f6 --- /dev/null +++ b/internal/block/validation/required_test.go @@ -0,0 +1,35 @@ +package validation + +import ( + "testing" +) + +func TestRequired(t *testing.T) { + cfg := &ComponentConfig{} + Required()(cfg) + + if len(cfg.Validators) != 1 { + t.Fatalf("expect 1 validator, got %d", len(cfg.Validators)) + } + + validate := cfg.Validators[0] + + tests := []struct { + name string + value any + wantErr bool + }{ + {"nil value", nil, true}, + {"empty string", "", true}, + {"non-empty string", "hello", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validate(tt.value) + if (err != nil) != tt.wantErr { + t.Errorf("validate(%v) error = %v, wantErr %v", tt.value, err, tt.wantErr) + } + }) + } +} diff --git a/internal/block/validation/validator.go b/internal/block/validation/validator.go new file mode 100644 index 0000000..6f272c2 --- /dev/null +++ b/internal/block/validation/validator.go @@ -0,0 +1,3 @@ +package validation + +type Validator func(value any) error diff --git a/main.go b/main.go new file mode 100644 index 0000000..b6acfed --- /dev/null +++ b/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "blocky/internal/block" + "blocky/internal/block/text_component" + text_component_validators "blocky/internal/block/text_component/validators" + "blocky/internal/block/validation" + "fmt" +) + +func main() { + textComponent := text_component.NewTextComponent( + "title", + "Title", + // validation.Required(), + // validation.MaxLength(4), + // nil, + []validation.ComponentOption{ + validation.Required(), + validation.MaxLength(4), + }, + []text_component.TextComponentOption{ + text_component_validators.Regex("^\\d+$"), + }, + ) + + b := &block.Block{} + b.Components = append(b.Components, textComponent) + + fmt.Println(textComponent.Name(), textComponent.Type()) + + numberComponent := block.NewNumberComponent("age", "Age") + b.Components = append(b.Components, numberComponent) + + data := []byte(`{"title": "252", "age": 25 }`) + // data := []byte(`{"title": "this is the title", "age": 25 }`) + instance := &block.BlockInstance{} + err := instance.FromJSON(b, data) + if err != nil { + fmt.Println("Error decoding object", err) + return + } + fmt.Println(instance) + fmt.Println(instance.Components[0]) + fmt.Println(instance.Components[1]) + +}