add Block + Component, add TextComponent with min, max and regex validators and early NumberComponent
This commit is contained in:
42
internal/block/block.go
Normal file
42
internal/block/block.go
Normal 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
|
||||||
|
}
|
||||||
27
internal/block/component.go
Normal file
27
internal/block/component.go
Normal 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
|
||||||
|
}
|
||||||
54
internal/block/number_component.go
Normal file
54
internal/block/number_component.go
Normal 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
|
||||||
|
}
|
||||||
85
internal/block/text_component/text_component.go
Normal file
85
internal/block/text_component/text_component.go
Normal 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)
|
||||||
21
internal/block/text_component/validators/max_length.go
Normal file
21
internal/block/text_component/validators/max_length.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
36
internal/block/text_component/validators/max_length_test.go
Normal file
36
internal/block/text_component/validators/max_length_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
21
internal/block/text_component/validators/min_length.go
Normal file
21
internal/block/text_component/validators/min_length.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
36
internal/block/text_component/validators/min_length_test.go
Normal file
36
internal/block/text_component/validators/min_length_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
24
internal/block/text_component/validators/regex.go
Normal file
24
internal/block/text_component/validators/regex.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
33
internal/block/text_component/validators/regex_test.go
Normal file
33
internal/block/text_component/validators/regex_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
internal/block/validation/component_option.go
Normal file
7
internal/block/validation/component_option.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type ComponentOption func(*ComponentConfig)
|
||||||
|
|
||||||
|
type ComponentConfig struct {
|
||||||
|
Validators []Validator
|
||||||
|
}
|
||||||
14
internal/block/validation/required.go
Normal file
14
internal/block/validation/required.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
35
internal/block/validation/required_test.go
Normal file
35
internal/block/validation/required_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
3
internal/block/validation/validator.go
Normal file
3
internal/block/validation/validator.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type Validator func(value any) error
|
||||||
47
main.go
Normal file
47
main.go
Normal 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])
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user