add Block + Component, add TextComponent with min, max and regex validators and early NumberComponent

This commit is contained in:
2026-02-12 15:47:52 +00:00
parent 4f77702125
commit 6099538fa8
16 changed files with 488 additions and 0 deletions

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module blocky
go 1.25

42
internal/block/block.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
})
}
}

View File

@@ -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)
}
})
}
}

View File

@@ -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
})
}
}

View File

@@ -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)
}
})
}
}

View File

@@ -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
})
}
}

View File

@@ -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)
}
})
}
}

View File

@@ -0,0 +1,7 @@
package validation
type ComponentOption func(*ComponentConfig)
type ComponentConfig struct {
Validators []Validator
}

View File

@@ -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
})
}
}

View File

@@ -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)
}
})
}
}

View File

@@ -0,0 +1,3 @@
package validation
type Validator func(value any) error

47
main.go Normal file
View File

@@ -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])
}