From dc6db954edce9a9776f388d77098da46f1d9dbc8 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Tue, 29 Jul 2025 13:45:36 +0200 Subject: [PATCH 01/31] fix(input/choice): add specific validation func --- CHANGELOG.md | 6 ++++++ form/field_choice.go | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2a2bf1..58de243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## [Unreleased] +## v1.1.5 + +### Fixed + +- fix(input/choice): add specific validation func + ## v1.1.4 ### Fixed diff --git a/form/field_choice.go b/form/field_choice.go index 6a4014d..c74ae7a 100644 --- a/form/field_choice.go +++ b/form/field_choice.go @@ -19,6 +19,7 @@ import ( "reflect" "github.com/spf13/cast" + "gitnet.fr/deblan/go-form/validation" ) type Choice struct { @@ -124,6 +125,28 @@ func NewFieldChoice(name string) *Field { NewOption("empty_choice_label", "None"), ) + f.Validate = func(field *Field) bool { + isValid := FieldValidation(field) + + if len(validation.NewNotBlank().Validate(field.Data)) == 0 { + choices := field.GetOption("choices").Value.(*Choices) + isValidChoice := true + + for _, choice := range choices.GetChoices() { + if !choices.Match(field, choice.Value) { + isValidChoice = false + } + } + + if !isValidChoice { + field.Errors = append(field.Errors, validation.Error("This value is not valid.")) + isValid = false + } + } + + return isValid + } + f.WithBeforeBind(func(data any) (any, error) { choices := f.GetOption("choices").Value.(*Choices) From 781fd04ffbb97a3d687f4cf0a2a77535126c5b34 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Tue, 29 Jul 2025 13:49:46 +0200 Subject: [PATCH 02/31] fix(input/choice): add specific validation func --- CHANGELOG.md | 6 ++++++ form/field_choice.go | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58de243..f207094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## [Unreleased] +## v1.1.6 + +### Fixed + +- fix(input/choice): add specific validation func + ## v1.1.5 ### Fixed diff --git a/form/field_choice.go b/form/field_choice.go index c74ae7a..5572344 100644 --- a/form/field_choice.go +++ b/form/field_choice.go @@ -130,11 +130,11 @@ func NewFieldChoice(name string) *Field { if len(validation.NewNotBlank().Validate(field.Data)) == 0 { choices := field.GetOption("choices").Value.(*Choices) - isValidChoice := true + isValidChoice := false for _, choice := range choices.GetChoices() { - if !choices.Match(field, choice.Value) { - isValidChoice = false + if choices.Match(field, choice.Value) { + isValidChoice = true } } From d154000625ae21a87e2ba50db88413ecc93f8b66 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 30 Jul 2025 14:06:09 +0200 Subject: [PATCH 03/31] doc: add link to the documentation and the demo --- README.md | 73 +++++-------------------------------------------------- 1 file changed, 6 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 767ec58..c788bd4 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ Creating and processing HTML forms is hard and repetitive. You need to deal with rendering HTML form fields, validating submitted data, mapping the form data into objects and a lot more. [`go-form`][go-form] includes a powerful form feature that provides all these features. -## Introduction - -[`go-form`][go-form] is heavily influenced by [Symfony Form](https://symfony.com/doc/current/forms.html). It includes: +[`go-form`][go-form] is heavily influenced by [Symfony Form](https://symfony.com/doc/current/forms.html). It includes: * A form builder based on fields declarations and independent of structs * Validation based on constraints @@ -12,70 +10,11 @@ Creating and processing HTML forms is hard and repetitive. You need to deal with * Data binding to populate a struct instance from a submitted form * Form renderer with customizable themes -## Installation +## Documentation -```shell -go get gitnet.fr/deblan/go-form -``` - -## Quick Start - -```go -package main - -import ( - "html/template" - "log" - "net/http" - - "gitnet.fr/deblan/go-form/form" - "gitnet.fr/deblan/go-form/theme" - "gitnet.fr/deblan/go-form/validation" -) - -func main() { - type Person struct { - Name string - } - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - data := new(Person) - - f := form.NewForm( - form.NewFieldText("Name"). - WithOptions( - form.NewOption("label", "Your name"), - ). - WithConstraints( - validation.NewNotBlank(), - ), - form.NewSubmit("submit"), - ). - End(). - WithMethod(http.MethodPost). - WithAction("/") - - f.Mount(data) - - if r.Method == f.Method { - f.HandleRequest(r) - - if f.IsSubmitted() && f.IsValid() { - f.Bind(data) - } - } - - render := theme.NewRenderer(theme.Html5) - tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - tpl.Execute(w, map[string]any{ - "Form": f, - }) - }) - - log.Fatal(http.ListenAndServe(":1324", nil)) -} -``` +* [Officiel documentation][doc] +* [Demo][demo] [go-form]: https://gitnet.fr/deblan/go-form +[demo]: https://gitnet.fr/deblan/go-form-demo +[doc]: https://deblan.gitnet.page/go-form/ From 17da3f7aee2e0b7d1c17f05e63d20a29042f4b25 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 30 Jul 2025 14:06:47 +0200 Subject: [PATCH 04/31] doc: add link to the documentation and the demo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c788bd4..3754083 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Creating and processing HTML forms is hard and repetitive. You need to deal with ## Documentation -* [Officiel documentation][doc] +* [Official documentation][doc] * [Demo][demo] [go-form]: https://gitnet.fr/deblan/go-form From 5a3ec773d5d8333330d328201298400244cad591 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 1 Aug 2025 17:41:28 +0200 Subject: [PATCH 05/31] feat: add tag `field` to specify the naming strategy (case) --- util/inspect.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/util/inspect.go b/util/inspect.go index 76c8e9f..6f2369d 100644 --- a/util/inspect.go +++ b/util/inspect.go @@ -17,7 +17,10 @@ package util import ( "errors" + "fmt" "reflect" + + "github.com/iancoleman/strcase" ) func InspectStruct(input interface{}) (map[string]interface{}, error) { @@ -37,8 +40,22 @@ func InspectStruct(input interface{}) (map[string]interface{}, error) { for i := 0; i < val.NumField(); i++ { field := typ.Field(i) value := val.Field(i) + tags := typ.Field(i).Tag + name := field.Name - result[field.Name] = value.Interface() + fieldTag := tags.Get("field") + + fmt.Printf("%+v\n", fieldTag) + + if fieldTag == "snake" { + name = strcase.ToSnake(name) + } else if fieldTag == "lowerCamel" { + name = strcase.ToLowerCamel(name) + } + + fmt.Printf("%+v\n", name) + + result[name] = value.Interface() } return result, nil From f0a94dec933f8331c464b5439dac3b2b6cdf637a Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 1 Aug 2025 17:41:41 +0200 Subject: [PATCH 06/31] feat: add tag `field` to specify the naming strategy (case) --- go.mod | 1 + go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/go.mod b/go.mod index 0b43ae4..3973949 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( ) require ( + github.com/iancoleman/strcase v0.3.0 // indirect github.com/samber/lo v1.51.0 // indirect golang.org/x/text v0.22.0 // indirect maragu.dev/gomponents v1.1.0 // indirect diff --git a/go.sum b/go.sum index fd3f0f9..f6698b1 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= From c6fc6e45e4e5c0957ef446ebba8d942c6bfef375 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 1 Aug 2025 17:42:27 +0200 Subject: [PATCH 07/31] fix(validation/notblank): nil value validation --- validation/notblank.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validation/notblank.go b/validation/notblank.go index a417e22..1962cb2 100644 --- a/validation/notblank.go +++ b/validation/notblank.go @@ -39,7 +39,7 @@ func (c NotBlank) Validate(data any) []Error { v := reflect.ValueOf(data) - if v.IsZero() { + if data == nil || v.IsZero() { errors = append(errors, Error(c.Message)) return errors From 37eaf6b3485ab0a337639721b1e34eab2f582472 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 1 Aug 2025 17:42:44 +0200 Subject: [PATCH 08/31] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f207094..1629054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +### Added + +- feat: add tag `field` to specify the naming strategy (case) + ## v1.1.6 ### Fixed From 1a063823871ccffa1ae0e2b11f141d1b48fd774e Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 1 Aug 2025 18:02:25 +0200 Subject: [PATCH 09/31] feat: remove 'snake' as field case choice --- util/inspect.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/util/inspect.go b/util/inspect.go index 6f2369d..17c0839 100644 --- a/util/inspect.go +++ b/util/inspect.go @@ -17,7 +17,6 @@ package util import ( "errors" - "fmt" "reflect" "github.com/iancoleman/strcase" @@ -45,16 +44,10 @@ func InspectStruct(input interface{}) (map[string]interface{}, error) { fieldTag := tags.Get("field") - fmt.Printf("%+v\n", fieldTag) - - if fieldTag == "snake" { - name = strcase.ToSnake(name) - } else if fieldTag == "lowerCamel" { + if fieldTag == "lowerCamel" { name = strcase.ToLowerCamel(name) } - fmt.Printf("%+v\n", name) - result[name] = value.Interface() } From b4ec7c61789f7f905cb19bcf7ed77264b5266d33 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 1 Aug 2025 19:41:40 +0200 Subject: [PATCH 10/31] feat: add `ErrorsTree()` on form and field --- CHANGELOG.md | 1 + form/field.go | 43 +++++++++++++++++++++++++++++++++++++++++++ form/form.go | 16 ++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1629054..ef4bcfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Added - feat: add tag `field` to specify the naming strategy (case) +- feat: add `ErrorsTree` methods on form and field ## v1.1.6 diff --git a/form/field.go b/form/field.go index 7abae57..b6c875f 100644 --- a/form/field.go +++ b/form/field.go @@ -310,3 +310,46 @@ func (f *Field) Bind(data map[string]any, key *string) error { return nil } + +func (f *Field) ErrorsTree(tree map[string]any, key *string) { + var index string + + if key != nil { + index = *key + } else { + index = f.Name + } + + if len(f.Children) == 0 { + if len(f.Errors) > 0 { + tree[index] = map[string]any{ + "meta": map[string]any{ + "id": f.GetId(), + "name": f.Name, + "formName": f.GetName(), + }, + "errors": f.Errors, + } + } + } else { + errors := make(map[string]any) + + for _, child := range f.Children { + if len(child.Errors) > 0 { + child.ErrorsTree(errors, &child.Name) + } + } + + if len(errors) > 0 { + tree[index] = map[string]any{ + "meta": map[string]any{ + "id": f.GetId(), + "name": f.Name, + "formName": f.GetName(), + }, + "errors": []validation.Error{}, + "children": errors, + } + } + } +} diff --git a/form/form.go b/form/form.go index 742d192..31bcca0 100644 --- a/form/form.go +++ b/form/form.go @@ -244,3 +244,19 @@ func (f *Form) HandleRequest(req *http.Request) { func (f *Form) IsSubmitted() bool { return f.RequestData != nil } + +func (f *Form) ErrorsTree() map[string]any { + tree := make(map[string]any) + + if len(f.Errors) > 0 { + tree["_form"] = map[string]any{ + "errors": f.Errors, + } + } + + for _, field := range f.Fields { + field.ErrorsTree(tree, nil) + } + + return tree +} From 27b7cb63edc9042adaf7952456b272998e7f4fa1 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 1 Aug 2025 21:11:47 +0200 Subject: [PATCH 11/31] feat: add meta and children in form.ErrorsTree() --- form/form.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/form/form.go b/form/form.go index 31bcca0..b4cfe37 100644 --- a/form/form.go +++ b/form/form.go @@ -246,17 +246,14 @@ func (f *Form) IsSubmitted() bool { } func (f *Form) ErrorsTree() map[string]any { - tree := make(map[string]any) - - if len(f.Errors) > 0 { - tree["_form"] = map[string]any{ - "errors": f.Errors, - } - } + errors := make(map[string]any) for _, field := range f.Fields { - field.ErrorsTree(tree, nil) + field.ErrorsTree(errors, nil) } - return tree + return map[string]any{ + "errors": f.Errors, + "children": errors, + } } From 722d55106b0d4a695b7812e03913b27f1154c466 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 1 Aug 2025 22:07:39 +0200 Subject: [PATCH 12/31] feat: set children as [int]any --- form/field.go | 4 +++- form/form.go | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/form/field.go b/form/field.go index b6c875f..90b5154 100644 --- a/form/field.go +++ b/form/field.go @@ -17,6 +17,8 @@ package form import ( "fmt" + "maps" + "slices" "strings" "gitnet.fr/deblan/go-form/util" @@ -348,7 +350,7 @@ func (f *Field) ErrorsTree(tree map[string]any, key *string) { "formName": f.GetName(), }, "errors": []validation.Error{}, - "children": errors, + "children": slices.Collect(maps.Values(errors)), } } } diff --git a/form/form.go b/form/form.go index b4cfe37..348869f 100644 --- a/form/form.go +++ b/form/form.go @@ -16,8 +16,10 @@ package form // along with this program. If not, see . import ( + "maps" "net/http" "net/url" + "slices" "github.com/mitchellh/mapstructure" "gitnet.fr/deblan/go-form/util" @@ -254,6 +256,6 @@ func (f *Form) ErrorsTree() map[string]any { return map[string]any{ "errors": f.Errors, - "children": errors, + "children": slices.Collect(maps.Values(errors)), } } From 627182d141d00abc549bfb596144e32fa22db5cc Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 2 Aug 2025 14:55:37 +0200 Subject: [PATCH 13/31] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef4bcfc..aa92ff4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [Unreleased] +## v1.2.0 + ### Added - feat: add tag `field` to specify the naming strategy (case) From eb8ecafea12016c92b950b903f0afc899019485b Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Thu, 7 Aug 2025 18:10:17 +0200 Subject: [PATCH 14/31] feat: add map to url.Values transformer --- util/transformer.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 util/transformer.go diff --git a/util/transformer.go b/util/transformer.go new file mode 100644 index 0000000..b3788a1 --- /dev/null +++ b/util/transformer.go @@ -0,0 +1,36 @@ +package util + +import ( + "fmt" + "net/url" +) + +func MapToUrlValues(values *url.Values, prefix string, data map[string]any) { + keyFormater := "%s" + + if prefix != "" { + keyFormater = prefix + "[%s]" + } + + for key, value := range data { + keyValue := fmt.Sprintf(keyFormater, key) + + switch v := value.(type) { + case string: + values.Add(keyValue, v) + case []string: + case []int: + case []int32: + case []int64: + case []any: + for _, s := range v { + values.Add(keyValue, fmt.Sprintf("%v", s)) + } + case int, int64, float64, bool: + values.Add(keyValue, fmt.Sprintf("%v", v)) + case map[string]any: + MapToUrlValues(values, keyValue, v) + default: + } + } +} From 6668356b4c0812070a038e532c3baaccae46942a Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Thu, 7 Aug 2025 18:11:30 +0200 Subject: [PATCH 15/31] chore: update dependencies --- go.mod | 9 ++------- go.sum | 4 ---- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 3973949..20dda1e 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,9 @@ module gitnet.fr/deblan/go-form go 1.23.0 require ( + github.com/iancoleman/strcase v0.3.0 github.com/mitchellh/mapstructure v1.5.0 github.com/spf13/cast v1.9.2 github.com/yassinebenaid/godump v0.11.1 -) - -require ( - github.com/iancoleman/strcase v0.3.0 // indirect - github.com/samber/lo v1.51.0 // indirect - golang.org/x/text v0.22.0 // indirect - maragu.dev/gomponents v1.1.0 // indirect + maragu.dev/gomponents v1.1.0 ) diff --git a/go.sum b/go.sum index f6698b1..c6c8a71 100644 --- a/go.sum +++ b/go.sum @@ -12,13 +12,9 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= -github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/yassinebenaid/godump v0.11.1 h1:SPujx/XaYqGDfmNh7JI3dOyCUVrG0bG2duhO3Eh2EhI= github.com/yassinebenaid/godump v0.11.1/go.mod h1:dc/0w8wmg6kVIvNGAzbKH1Oa54dXQx8SNKh4dPRyW44= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= maragu.dev/gomponents v1.1.0 h1:iCybZZChHr1eSlvkWp/JP3CrZGzctLudQ/JI3sBcO4U= maragu.dev/gomponents v1.1.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM= From 21e0cdc733bb551a025cf8a85674c2b8ce468c39 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Thu, 7 Aug 2025 18:11:59 +0200 Subject: [PATCH 16/31] feat: allow to handle request using json body --- form/form.go | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/form/form.go b/form/form.go index 348869f..168c7f2 100644 --- a/form/form.go +++ b/form/form.go @@ -16,6 +16,8 @@ package form // along with this program. If not, see . import ( + "encoding/json" + "io/ioutil" "maps" "net/http" "net/url" @@ -32,6 +34,7 @@ type Form struct { GlobalFields []*Field Errors []validation.Error Method string + JsonRequest bool Action string Name string Options []*Option @@ -201,6 +204,12 @@ func (f *Form) Mount(data any) error { return nil } +func (f *Form) WithJsonRequest() *Form { + f.JsonRequest = true + + return f +} + // Copies datas from the form to a struct func (f *Form) Bind(data any) error { toBind := make(map[string]any) @@ -216,11 +225,30 @@ func (f *Form) Bind(data any) error { func (f *Form) HandleRequest(req *http.Request) { var data url.Values - if f.Method != "GET" { - req.ParseForm() - data = req.Form + if f.JsonRequest { + body, err := ioutil.ReadAll(req.Body) + + if err != nil { + return + } + + mapping := make(map[string]any) + err = json.Unmarshal(body, &mapping) + + if err != nil { + return + } + + data = url.Values{} + util.MapToUrlValues(&data, f.Name, mapping) } else { - data = req.URL.Query() + switch f.Method { + case "GET": + data = req.URL.Query() + default: + req.ParseForm() + data = req.Form + } } isSubmitted := false From e76ada793038afe5fa545f6bcf27d33edc2032da Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 8 Aug 2025 08:56:15 +0200 Subject: [PATCH 17/31] feat(util/transformer): transform booleans to 0 and 1 --- util/transformer.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/util/transformer.go b/util/transformer.go index b3788a1..743b117 100644 --- a/util/transformer.go +++ b/util/transformer.go @@ -26,7 +26,13 @@ func MapToUrlValues(values *url.Values, prefix string, data map[string]any) { for _, s := range v { values.Add(keyValue, fmt.Sprintf("%v", s)) } - case int, int64, float64, bool: + case bool: + if v { + values.Add(keyValue, "1") + } else { + values.Add(keyValue, "0") + } + case int, int64, float64: values.Add(keyValue, fmt.Sprintf("%v", v)) case map[string]any: MapToUrlValues(values, keyValue, v) From 28dc55d920fc78955cc51916c48dc66f766651de Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 8 Aug 2025 08:56:21 +0200 Subject: [PATCH 18/31] update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa92ff4..abb6271 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## [Unreleased] +## v1.3.0 + +### Added + +- feat: allow to handle request using json body" +- feat: add `WithJsonRequest` on form +- feat: add `MapToUrlValues` transformer + ## v1.2.0 ### Added From fdf94ba319450649e7475018d2a49ec5fc7af980 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 18 Aug 2025 09:14:48 +0200 Subject: [PATCH 19/31] fix: reset `GlobalFields` in `End()` --- CHANGELOG.md | 4 ++++ form/form.go | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abb6271..320e9fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +### Fixed + +- fix: reset `GlobalFields` in `End()` + ## v1.3.0 ### Added diff --git a/form/form.go b/form/form.go index 168c7f2..85325fa 100644 --- a/form/form.go +++ b/form/form.go @@ -95,6 +95,8 @@ func (f *Form) Add(fields ...*Field) { // Configures its children deeply // This function must be called after adding all fields func (f *Form) End() *Form { + f.GlobalFields = []*Field{} + for _, c := range f.Fields { f.AddGlobalField(c) } From 56c7ac9d048fd36c1cae28ce656d4e00fd6ba89b Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 18 Aug 2025 09:45:38 +0200 Subject: [PATCH 20/31] feat: add json configuration --- form/field.go | 28 ++++++++++++++-------------- form/field_choice.go | 25 ++++++++++++++++++++++--- form/form.go | 18 +++++++++--------- form/option.go | 4 ++-- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/form/field.go b/form/field.go index 90b5154..1a3f5e5 100644 --- a/form/field.go +++ b/form/field.go @@ -56,20 +56,20 @@ func FieldValidation(f *Field) bool { // Field represents a field in a form type Field struct { - Name string - Widget string - Data any - Options []*Option - Children []*Field - Constraints []validation.Constraint - Errors []validation.Error - BeforeMount func(data any) (any, error) - BeforeBind func(data any) (any, error) - Validate func(f *Field) bool - IsSlice bool - IsFixedName bool - Form *Form - Parent *Field + Name string `json:"name"` + Widget string `json:"widget"` + Data any `json:"-"` + Options []*Option `json:"options"` + Children []*Field `json:"children"` + Constraints []validation.Constraint `json:"-"` + Errors []validation.Error `json:"-"` + BeforeMount func(data any) (any, error) `json:"-"` + BeforeBind func(data any) (any, error) `json:"-"` + Validate func(f *Field) bool `json:"-"` + IsSlice bool `json:"is_slice"` + IsFixedName bool `json:"is_fixed_name"` + Form *Form `json:"-"` + Parent *Field `json:"-"` } // Generates a new field with default properties diff --git a/form/field_choice.go b/form/field_choice.go index 5572344..70d2545 100644 --- a/form/field_choice.go +++ b/form/field_choice.go @@ -16,6 +16,7 @@ package form // along with this program. If not, see . import ( + "encoding/json" "reflect" "github.com/spf13/cast" @@ -33,9 +34,9 @@ func (c Choice) Match(value string) bool { } type Choices struct { - Data any - ValueBuilder func(key int, item any) string - LabelBuilder func(key int, item any) string + Data any `json:"data"` + ValueBuilder func(key int, item any) string `json:"-"` + LabelBuilder func(key int, item any) string `json:"-"` } func (c *Choices) Match(f *Field, value string) bool { @@ -100,6 +101,24 @@ func (c *Choices) GetChoices() []Choice { return choices } +func (c Choices) MarshalJSON() ([]byte, error) { + var choices []map[string]string + + v := reflect.ValueOf(c.Data) + + switch v.Kind() { + case reflect.Slice, reflect.Array, reflect.String, reflect.Map: + for i := 0; i < v.Len(); i++ { + choices = append(choices, map[string]string{ + "value": c.ValueBuilder(i, v.Index(i).Interface()), + "label": c.LabelBuilder(i, v.Index(i).Interface()), + }) + } + } + + return json.Marshal(choices) +} + // Generates an instance of Choices func NewChoices(items any) *Choices { builder := func(key int, item any) string { diff --git a/form/form.go b/form/form.go index 85325fa..a2dda2a 100644 --- a/form/form.go +++ b/form/form.go @@ -30,15 +30,15 @@ import ( // Field represents a form type Form struct { - Fields []*Field - GlobalFields []*Field - Errors []validation.Error - Method string - JsonRequest bool - Action string - Name string - Options []*Option - RequestData *url.Values + Fields []*Field `json:"children"` + GlobalFields []*Field `json:"-"` + Errors []validation.Error `json:"-"` + Method string `json:"method"` + JsonRequest bool `json:"json_request"` + Action string `json:"action"` + Name string `json:"name"` + Options []*Option `json:"options"` + RequestData *url.Values `json:"-"` } // Generates a new form with default properties diff --git a/form/option.go b/form/option.go index 9fccd27..95eee67 100644 --- a/form/option.go +++ b/form/option.go @@ -18,8 +18,8 @@ import "strings" // along with this program. If not, see . type Option struct { - Name string - Value any + Name string `json:"name"` + Value any `json:"value"` } func NewOption(name string, value any) *Option { From fcff10cec0e3af01745bb815e2edc28a3fb3e551 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 18 Aug 2025 09:46:17 +0200 Subject: [PATCH 21/31] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 320e9fe..b3a6954 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +### Added + +- feat: add json configuration + ### Fixed - fix: reset `GlobalFields` in `End()` From f25f823265fc1e7c35b62cc21a323c582785f1c6 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 18 Aug 2025 09:46:50 +0200 Subject: [PATCH 22/31] release v1.4.0 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a6954..f052f98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [Unreleased] +## v1.4.0 + ### Added - feat: add json configuration From a06afe583d379f7ed457ec04e940c98edeae29f6 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 24 Sep 2025 09:03:50 +0200 Subject: [PATCH 23/31] [WIP] feat: add collection widget --- example/form.go | 31 ++++++++++--- form/field.go | 77 +++++++++++++++++++++++++------- form/field_collection.go | 73 ++++++++++++++++++++++++++++++ form/form.go | 40 +++++++++++++++-- main.go | 28 +++++++++--- theme/html5.go | 48 ++++++++++++++++++++ util/collection.go | 96 ++++++++++++++++++++++++++++++++++++++++ util/inspect.go | 4 ++ 8 files changed, 367 insertions(+), 30 deletions(-) create mode 100644 form/field_collection.go create mode 100644 util/collection.go diff --git a/example/form.go b/example/form.go index 458f33b..4fd035c 100644 --- a/example/form.go +++ b/example/form.go @@ -36,12 +36,18 @@ type ExampleDates struct { } type ExampleData struct { - Bytes []byte - Text string - Checkbox bool - Dates ExampleDates - Choices ExampleChoices - Inputs ExampleOtherInputs + Collection []CollectionItem + Bytes []byte + Text string + Checkbox bool + Dates ExampleDates + Choices ExampleChoices + Inputs ExampleOtherInputs +} + +type CollectionItem struct { + ValueA string + ValueB string } func CreateDataForm() *form.Form { @@ -60,6 +66,19 @@ func CreateDataForm() *form.Form { }) return form.NewForm( + form.NewFieldCollection("Collection"). + WithOptions( + form.NewOption("label", "Collection"), + form.NewOption("form", form.NewForm( + form.NewFieldText("ValueA"). + WithOptions(form.NewOption("label", "Value A")). + WithConstraints( + validation.NewNotBlank(), + ), + form.NewFieldText("ValueB"). + WithOptions(form.NewOption("label", "Value B")), + )), + ), form.NewFieldText("Bytes"). WithOptions( form.NewOption("label", "Bytes"), diff --git a/form/field.go b/form/field.go index 1a3f5e5..dee37bf 100644 --- a/form/field.go +++ b/form/field.go @@ -27,6 +27,14 @@ import ( // Generic function for field.Validation func FieldValidation(f *Field) bool { + // if f.IsCollection { + // if formOption := f.GetOption("form"); formOption != nil { + // if formValue, ok := formOption.Value.(*Form); ok { + // godump.Dump(formValue) + // } + // } + // } + if len(f.Children) > 0 { isValid := true @@ -56,20 +64,22 @@ func FieldValidation(f *Field) bool { // Field represents a field in a form type Field struct { - Name string `json:"name"` - Widget string `json:"widget"` - Data any `json:"-"` - Options []*Option `json:"options"` - Children []*Field `json:"children"` - Constraints []validation.Constraint `json:"-"` - Errors []validation.Error `json:"-"` - BeforeMount func(data any) (any, error) `json:"-"` - BeforeBind func(data any) (any, error) `json:"-"` - Validate func(f *Field) bool `json:"-"` - IsSlice bool `json:"is_slice"` - IsFixedName bool `json:"is_fixed_name"` - Form *Form `json:"-"` - Parent *Field `json:"-"` + Name string `json:"name"` + NamePrefix string `json:"name_prefix"` + Widget string `json:"widget"` + Data any `json:"-"` + Options []*Option `json:"options"` + Children []*Field `json:"children"` + Constraints []validation.Constraint `json:"-"` + Errors []validation.Error `json:"-"` + BeforeMount func(data any) (any, error) `json:"-"` + BeforeBind func(data any) (any, error) `json:"-"` + Validate func(f *Field) bool `json:"-"` + IsSlice bool `json:"is_slice"` + IsCollection bool `json:"is_collection"` + IsFixedName bool `json:"is_fixed_name"` + Form *Form `json:"-"` + Parent *Field `json:"-"` } // Generates a new field with default properties @@ -102,6 +112,20 @@ func NewField(name, widget string) *Field { return f } +func (f *Field) Copy() *Field { + return &Field{ + Name: f.Name, + Widget: f.Widget, + Options: f.Options, + Constraints: f.Constraints, + BeforeMount: f.BeforeMount, + BeforeBind: f.BeforeBind, + Validate: f.Validate, + IsSlice: f.IsSlice, + IsFixedName: f.IsFixedName, + } +} + // Checks if the field contains an option using its name func (f *Field) HasOption(name string) bool { for _, option := range f.Options { @@ -137,6 +161,20 @@ func (f *Field) WithOptions(options ...*Option) *Field { return f } +func (f *Field) RemoveOption(name string) *Field { + var options []*Option + + for _, option := range f.Options { + if option.Name != name { + options = append(options, option) + } + } + + f.Options = options + + return f +} + // Sets data the field func (f *Field) WithData(data any) *Field { f.Data = data @@ -158,6 +196,13 @@ func (f *Field) WithSlice() *Field { return f } +// Sets that the field represents a collection +func (f *Field) WithCollection() *Field { + f.IsCollection = true + + return f +} + // Sets that the name of the field is not computed func (f *Field) WithFixedName() *Field { f.IsFixedName = true @@ -233,9 +278,9 @@ func (f *Field) GetName() string { } if f.Form != nil && f.Form.Name != "" { - name = fmt.Sprintf("%s[%s]", f.Form.Name, f.Name) + name = fmt.Sprintf("%s%s[%s]", f.Form.Name, f.NamePrefix, f.Name) } else if f.Parent != nil { - name = fmt.Sprintf("%s[%s]", f.Parent.GetName(), f.Name) + name = fmt.Sprintf("%s%s[%s]", f.Parent.GetName(), f.NamePrefix, f.Name) } else { name = f.Name } diff --git a/form/field_collection.go b/form/field_collection.go new file mode 100644 index 0000000..436ea27 --- /dev/null +++ b/form/field_collection.go @@ -0,0 +1,73 @@ +package form + +import ( + "fmt" + "reflect" +) + +// @license GNU AGPL version 3 or any later version +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Generates a sub form +func NewFieldCollection(name string) *Field { + f := NewField(name, "collection"). + WithOptions( + NewOption("allow_add", true), + NewOption("allow_delete", true), + NewOption("form", nil), + ). + WithCollection() + + f.WithBeforeMount(func(data any) (any, error) { + if opt := f.GetOption("form"); opt != nil { + if src, ok := opt.Value.(*Form); ok { + src.Name = f.GetName() + t := reflect.TypeOf(data) + + switch t.Kind() { + case reflect.Slice: + slice := reflect.ValueOf(data) + + for i := 0; i < slice.Len(); i++ { + form := src.Copy() + form.Mount(slice.Index(i).Interface()) + + field := f.Copy() + field.Widget = "sub_form" + field.Name = fmt.Sprintf("%d", i) + field.Add(form.Fields...) + field. + RemoveOption("form"). + RemoveOption("label") + + for _, c := range field.Children { + c.NamePrefix = fmt.Sprintf("[%d]", i) + } + + f.Add(field) + } + } + } + } + + return data, nil + }) + + return f +} + +func NewCollection(name string) *Field { + return NewFieldCollection(name) +} diff --git a/form/form.go b/form/form.go index a2dda2a..b96df95 100644 --- a/form/form.go +++ b/form/form.go @@ -22,8 +22,10 @@ import ( "net/http" "net/url" "slices" + "strings" "github.com/mitchellh/mapstructure" + "github.com/yassinebenaid/godump" "gitnet.fr/deblan/go-form/util" "gitnet.fr/deblan/go-form/validation" ) @@ -108,8 +110,10 @@ func (f *Form) End() *Form { func (f *Form) AddGlobalField(field *Field) { f.GlobalFields = append(f.GlobalFields, field) - for _, c := range field.Children { - f.AddGlobalField(c) + if field.Widget != "collection" { + for _, c := range field.Children { + f.AddGlobalField(c) + } } } @@ -220,6 +224,8 @@ func (f *Form) Bind(data any) error { field.Bind(toBind, nil) } + godump.Dump(toBind) + return mapstructure.Decode(toBind, data) } @@ -255,8 +261,23 @@ func (f *Form) HandleRequest(req *http.Request) { isSubmitted := false + type collectionData map[string]any + for _, c := range f.GlobalFields { - if data.Has(c.GetName()) { + if c.IsCollection { + collection := util.NewCollection() + + for key, _ := range data { + if strings.HasPrefix(key, c.GetName()) { + root := strings.Replace(key, c.GetName(), "", 1) + indexes := util.ExtractDataIndexes(root) + + collection.Add(indexes, data.Get(key)) + } + } + + c.Mount(collection.Slice()) + } else if data.Has(c.GetName()) { isSubmitted = true if c.IsSlice { @@ -289,3 +310,16 @@ func (f *Form) ErrorsTree() map[string]any { "children": slices.Collect(maps.Values(errors)), } } + +func (f *Form) Copy() *Form { + var fields []*Field + + for _, i := range f.Fields { + f := *i + fields = append(fields, &f) + } + + return &Form{ + Fields: fields, + } +} diff --git a/main.go b/main.go index 55e9229..da58d53 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "html/template" "log" "net/http" @@ -13,6 +14,11 @@ import ( func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { data := example.ExampleData{} + data.Collection = []example.CollectionItem{ + {"Value a 1", "Value b 1"}, + {"Value a 2", "Value b 2"}, + {"Value a 3", "Value b 3"}, + } f := example.CreateDataForm() f.Mount(data) @@ -22,7 +28,7 @@ func main() { if f.IsSubmitted() && f.IsValid() { f.Bind(&data) - godump.Dump(data) + // godump.Dump(data) } } @@ -91,10 +97,19 @@ func main() { Valid {{ .Form.IsValid }} -
- Data -
{{ .Dump }}
-
+ + + + + +
+
{{ .Dump }}
+
+
+ JSON +
{{ .Json }}
+
+
{{ form .Form }} @@ -105,9 +120,12 @@ func main() { var dump godump.Dumper dump.Theme = godump.Theme{} + j, _ := json.MarshalIndent(f, " ", " ") + w.Header().Set("Content-Type", "text/html; charset=utf-8") tpl.Execute(w, map[string]any{ "Form": f, + "Json": string(j), "Dump": template.HTML(dump.Sprint(data)), }) }) diff --git a/theme/html5.go b/theme/html5.go index a441e48..a709e4e 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -1,6 +1,7 @@ package theme import ( + "bytes" "fmt" "github.com/spf13/cast" @@ -348,6 +349,53 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { ) } + theme["collection"] = func(parent map[string]RenderFunc, args ...any) Node { + field := args[0].(*form.Field) + + var prototype string + + if opt := field.GetOption("form"); opt != nil { + if val, ok := opt.Value.(*form.Form); ok { + var buffer bytes.Buffer + dest := form.NewFieldSubForm(field.Name) + + for _, c := range val.Fields { + child := c.Copy() + child.NamePrefix = "[__name__]" + dest.Add(child) + } + + fieldPrototype := parent["form_row"](parent, dest) + fieldPrototype.Render(&buffer) + + prototype = buffer.String() + } + } + + field.WithOptions(form.NewOption("prototype", prototype)) + field.Widget = "collection_build" + + return Div( + Attr("data-prototype", prototype), + parent["form_widget"](parent, field), + ) + } + + theme["collection_build"] = func(parent map[string]RenderFunc, args ...any) Node { + field := args[0].(*form.Field) + prototype := field.GetOption("prototype").AsString() + var items []Node + + for _, child := range field.Children { + items = append(items, parent["form_row"](parent, child)) + } + + return Div( + Attr("data-prototype", prototype), + Group(items), + ) + } + theme["form_widget"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) diff --git a/util/collection.go b/util/collection.go new file mode 100644 index 0000000..7ee4d66 --- /dev/null +++ b/util/collection.go @@ -0,0 +1,96 @@ +package util + +import ( + "regexp" + "strings" + + "github.com/spf13/cast" +) + +type CollectionValue struct { + Name string + Value string + Children map[string]*CollectionValue +} + +type Collection struct { + Children map[int]*CollectionValue +} + +func NewCollection() *Collection { + return &Collection{ + Children: make(map[int]*CollectionValue), + } +} + +func NewCollectionValue(name string) *CollectionValue { + return &CollectionValue{ + Name: name, + Children: make(map[string]*CollectionValue), + } +} + +func (c *Collection) Add(indexes []string, value string) { + firstIndex := cast.ToInt(indexes[0]) + size := len(indexes) + child := c.Children[firstIndex] + + if child == nil { + child = NewCollectionValue(indexes[0]) + c.Children[firstIndex] = child + } + + child.Add(indexes[1:size], value, nil) +} + +func (c *Collection) Slice() []any { + var result []any + + for _, child := range c.Children { + result = append(result, child.Map()) + } + + return result +} + +func (c *CollectionValue) Map() any { + if len(c.Children) == 0 { + return c.Value + } + + results := make(map[string]any) + + for _, child := range c.Children { + results[child.Name] = child.Map() + } + + return results +} + +func (c *CollectionValue) Add(indexes []string, value string, lastChild *CollectionValue) { + size := len(indexes) + + if size > 0 { + firstIndex := indexes[0] + child := c.Children[firstIndex] + + child = NewCollectionValue(indexes[0]) + c.Children[firstIndex] = child + + child.Add(indexes[1:size], value, child) + } else { + lastChild.Value = value + } +} + +func ExtractDataIndexes(value string) []string { + re := regexp.MustCompile(`\[[^\]]+\]`) + items := re.FindAll([]byte(value), -1) + var results []string + + for _, i := range items { + results = append(results, strings.Trim(string(i), "[]")) + } + + return results +} diff --git a/util/inspect.go b/util/inspect.go index 17c0839..22fa4f4 100644 --- a/util/inspect.go +++ b/util/inspect.go @@ -29,6 +29,10 @@ func InspectStruct(input interface{}) (map[string]interface{}, error) { val = val.Elem() } + if val.Kind() == reflect.Map { + return input.(map[string]interface{}), nil + } + if val.Kind() != reflect.Struct { return nil, errors.New("Invalid type") } From 97f5cf321560b7abd32bd58a77d590aa54f94cf9 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 26 Sep 2025 17:49:39 +0200 Subject: [PATCH 24/31] [WIP] feat: handle collection in form rendering, mount and bind --- form/field.go | 41 ++++++++++++++++++++++++++++++---------- form/field_collection.go | 33 +++++++++++++++++++------------- form/form.go | 11 +++-------- main.go | 30 +++++++++++++++++++++++++++-- theme/html5.go | 5 +---- 5 files changed, 83 insertions(+), 37 deletions(-) diff --git a/form/field.go b/form/field.go index dee37bf..b4484df 100644 --- a/form/field.go +++ b/form/field.go @@ -21,20 +21,13 @@ import ( "slices" "strings" + "github.com/yassinebenaid/godump" "gitnet.fr/deblan/go-form/util" "gitnet.fr/deblan/go-form/validation" ) // Generic function for field.Validation func FieldValidation(f *Field) bool { - // if f.IsCollection { - // if formOption := f.GetOption("form"); formOption != nil { - // if formValue, ok := formOption.Value.(*Form); ok { - // godump.Dump(formValue) - // } - // } - // } - if len(f.Children) > 0 { isValid := true @@ -47,6 +40,10 @@ func FieldValidation(f *Field) bool { } isValid = isValid && isChildValid + + for _, sc := range c.Children { + isValid = isValid && FieldValidation(sc) + } } return isValid @@ -115,6 +112,7 @@ func NewField(name, widget string) *Field { func (f *Field) Copy() *Field { return &Field{ Name: f.Name, + Form: f.Form, Widget: f.Widget, Options: f.Options, Constraints: f.Constraints, @@ -332,7 +330,7 @@ func (f *Field) Mount(data any) error { } // Bind the data into the given map -func (f *Field) Bind(data map[string]any, key *string) error { +func (f *Field) Bind(data map[string]any, key *string, parentIsSlice bool) error { if len(f.Children) == 0 { v, err := f.BeforeBind(f.Data) @@ -352,7 +350,30 @@ func (f *Field) Bind(data map[string]any, key *string) error { data[f.Name] = make(map[string]any) for _, child := range f.Children { - child.Bind(data[f.Name].(map[string]any), key) + child.Bind(data[f.Name].(map[string]any), key, f.IsCollection) + } + + if f.IsCollection { + var nextData []any + values := data[f.Name].(map[string]any) + var keys []string + + for key, _ := range values { + keys = append(keys, key) + } + + slices.Sort(keys) + + for _, key := range keys { + for valueKey, value := range values { + if valueKey == key { + godump.Dump([]string{valueKey, key}) + nextData = append(nextData, value) + } + } + } + + data[f.Name] = nextData } return nil diff --git a/form/field_collection.go b/form/field_collection.go index 436ea27..52b8e02 100644 --- a/form/field_collection.go +++ b/form/field_collection.go @@ -41,22 +41,29 @@ func NewFieldCollection(name string) *Field { slice := reflect.ValueOf(data) for i := 0; i < slice.Len(); i++ { - form := src.Copy() - form.Mount(slice.Index(i).Interface()) + name := fmt.Sprintf("%d", i) + value := slice.Index(i).Interface() - field := f.Copy() - field.Widget = "sub_form" - field.Name = fmt.Sprintf("%d", i) - field.Add(form.Fields...) - field. - RemoveOption("form"). - RemoveOption("label") + if f.HasChild(name) { + f.GetChild(name).Mount(value) + } else { + form := src.Copy() + form.Mount(value) - for _, c := range field.Children { - c.NamePrefix = fmt.Sprintf("[%d]", i) + field := f.Copy() + field.Widget = "sub_form" + field.Name = name + field.Add(form.Fields...) + field. + RemoveOption("form"). + RemoveOption("label") + + for _, c := range field.Children { + c.NamePrefix = fmt.Sprintf("[%d]", i) + } + + f.Add(field) } - - f.Add(field) } } } diff --git a/form/form.go b/form/form.go index b96df95..c17da05 100644 --- a/form/form.go +++ b/form/form.go @@ -25,7 +25,6 @@ import ( "strings" "github.com/mitchellh/mapstructure" - "github.com/yassinebenaid/godump" "gitnet.fr/deblan/go-form/util" "gitnet.fr/deblan/go-form/validation" ) @@ -110,10 +109,8 @@ func (f *Form) End() *Form { func (f *Form) AddGlobalField(field *Field) { f.GlobalFields = append(f.GlobalFields, field) - if field.Widget != "collection" { - for _, c := range field.Children { - f.AddGlobalField(c) - } + for _, c := range field.Children { + f.AddGlobalField(c) } } @@ -221,11 +218,9 @@ func (f *Form) Bind(data any) error { toBind := make(map[string]any) for _, field := range f.Fields { - field.Bind(toBind, nil) + field.Bind(toBind, nil, false) } - godump.Dump(toBind) - return mapstructure.Decode(toBind, data) } diff --git a/main.go b/main.go index da58d53..7c4e97a 100644 --- a/main.go +++ b/main.go @@ -16,8 +16,6 @@ func main() { data := example.ExampleData{} data.Collection = []example.CollectionItem{ {"Value a 1", "Value b 1"}, - {"Value a 2", "Value b 2"}, - {"Value a 3", "Value b 3"}, } f := example.CreateDataForm() @@ -113,6 +111,34 @@ func main() { {{ form .Form }} + + `) diff --git a/theme/html5.go b/theme/html5.go index a709e4e..a4728e0 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -375,10 +375,7 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { field.WithOptions(form.NewOption("prototype", prototype)) field.Widget = "collection_build" - return Div( - Attr("data-prototype", prototype), - parent["form_widget"](parent, field), - ) + return parent["form_widget"](parent, field) } theme["collection_build"] = func(parent map[string]RenderFunc, args ...any) Node { From b9c5f6a2fd77130d02b5cc0d2a908047564a1bca Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 16:30:27 +0200 Subject: [PATCH 25/31] fix(collection): always validate fields when another field is invalid" --- form/field.go | 10 ++++------ form/field_choice.go | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/form/field.go b/form/field.go index b4484df..f1558c2 100644 --- a/form/field.go +++ b/form/field.go @@ -21,13 +21,12 @@ import ( "slices" "strings" - "github.com/yassinebenaid/godump" "gitnet.fr/deblan/go-form/util" "gitnet.fr/deblan/go-form/validation" ) // Generic function for field.Validation -func FieldValidation(f *Field) bool { +func DefaultFieldValidation(f *Field) bool { if len(f.Children) > 0 { isValid := true @@ -39,10 +38,10 @@ func FieldValidation(f *Field) bool { c.Errors = errs } - isValid = isValid && isChildValid + isValid = isChildValid && isValid for _, sc := range c.Children { - isValid = isValid && FieldValidation(sc) + isValid = DefaultFieldValidation(sc) && isValid } } @@ -104,7 +103,7 @@ func NewField(name, widget string) *Field { NewOption("help_attr", Attrs{}), ) - f.Validate = FieldValidation + f.Validate = DefaultFieldValidation return f } @@ -367,7 +366,6 @@ func (f *Field) Bind(data map[string]any, key *string, parentIsSlice bool) error for _, key := range keys { for valueKey, value := range values { if valueKey == key { - godump.Dump([]string{valueKey, key}) nextData = append(nextData, value) } } diff --git a/form/field_choice.go b/form/field_choice.go index 70d2545..16c06fc 100644 --- a/form/field_choice.go +++ b/form/field_choice.go @@ -145,7 +145,7 @@ func NewFieldChoice(name string) *Field { ) f.Validate = func(field *Field) bool { - isValid := FieldValidation(field) + isValid := field.Validate(field) if len(validation.NewNotBlank().Validate(field.Data)) == 0 { choices := field.GetOption("choices").Value.(*Choices) From 1d40aa6b09135368915c10216dcb11dce98bb05c Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 18:13:43 +0200 Subject: [PATCH 26/31] refactor: refactor the example --- example.go | 76 +++++++++++++ example/form.go | 32 +++--- example/view/bootstrap.html | 113 +++++++++++++++++++ example/view/html5.html | 133 ++++++++++++++++++++++ main.go | 219 ------------------------------------ 5 files changed, 338 insertions(+), 235 deletions(-) create mode 100644 example.go create mode 100644 example/view/bootstrap.html create mode 100644 example/view/html5.html delete mode 100644 main.go diff --git a/example.go b/example.go new file mode 100644 index 0000000..2aa6727 --- /dev/null +++ b/example.go @@ -0,0 +1,76 @@ +package main + +import ( + "embed" + "encoding/json" + "html/template" + "log" + "net/http" + + "github.com/yassinebenaid/godump" + "gitnet.fr/deblan/go-form/example" + "gitnet.fr/deblan/go-form/theme" +) + +//go:embed example/view/*.html +var templates embed.FS + +func handler(view, action string, formRenderer *theme.Renderer, w http.ResponseWriter, r *http.Request) { + entity := example.ExampleData{} + form := example.CreateDataForm(action) + + form.Mount(entity) + + if r.Method == form.Method { + form.HandleRequest(r) + + if form.IsSubmitted() && form.IsValid() { + form.Bind(&entity) + } + } + + content, _ := templates.ReadFile(view) + + formAsJson, _ := json.MarshalIndent(form, " ", " ") + + tpl, _ := template.New("page"). + Funcs(formRenderer.FuncMap()). + Parse(string(content)) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + var dump godump.Dumper + dump.Theme = godump.Theme{} + + tpl.Execute(w, map[string]any{ + "isSubmitted": form.IsSubmitted(), + "isValid": form.IsValid(), + "form": form, + "json": string(formAsJson), + "dump": template.HTML(dump.Sprint(entity)), + }) +} + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + handler( + "example/view/html5.html", + "/", + theme.NewRenderer(theme.Html5), + w, + r, + ) + }) + + http.HandleFunc("/bootstrap", func(w http.ResponseWriter, r *http.Request) { + handler( + "example/view/bootstrap.html", + "/bootstrap", + theme.NewRenderer(theme.Bootstrap5), + w, + r, + ) + }) + + log.Fatal(http.ListenAndServe(":1122", nil)) +} diff --git a/example/form.go b/example/form.go index 4fd035c..8c516ba 100644 --- a/example/form.go +++ b/example/form.go @@ -50,7 +50,7 @@ type CollectionItem struct { ValueB string } -func CreateDataForm() *form.Form { +func CreateDataForm(action string) *form.Form { items := []Item{ Item{Id: 1, Name: "Item 1"}, Item{Id: 2, Name: "Item 2"}, @@ -66,19 +66,6 @@ func CreateDataForm() *form.Form { }) return form.NewForm( - form.NewFieldCollection("Collection"). - WithOptions( - form.NewOption("label", "Collection"), - form.NewOption("form", form.NewForm( - form.NewFieldText("ValueA"). - WithOptions(form.NewOption("label", "Value A")). - WithConstraints( - validation.NewNotBlank(), - ), - form.NewFieldText("ValueB"). - WithOptions(form.NewOption("label", "Value B")), - )), - ), form.NewFieldText("Bytes"). WithOptions( form.NewOption("label", "Bytes"), @@ -196,6 +183,19 @@ func CreateDataForm() *form.Form { form.NewOption("multiple", true), ), ), + form.NewFieldCollection("Collection"). + WithOptions( + form.NewOption("label", "Collection"), + form.NewOption("form", form.NewForm( + form.NewFieldText("ValueA"). + WithOptions(form.NewOption("label", "Value A")). + WithConstraints( + validation.NewNotBlank(), + ), + form.NewFieldText("ValueB"). + WithOptions(form.NewOption("label", "Value B")), + )), + ), form.NewFieldCsrf("_csrf_token").WithData("my-token"), form.NewSubmit("submit"). WithOptions( @@ -206,8 +206,8 @@ func CreateDataForm() *form.Form { ). End(). WithOptions( - form.NewOption("help", "Form help"), + form.NewOption("help", "Form global help"), ). WithMethod(http.MethodPost). - WithAction("/") + WithAction(action) } diff --git a/example/view/bootstrap.html b/example/view/bootstrap.html new file mode 100644 index 0000000..7b848f1 --- /dev/null +++ b/example/view/bootstrap.html @@ -0,0 +1,113 @@ + + + + + Form with Bootstrap + + + + +
+
+ Debug view +
+ Submitted: + {{ .isSubmitted }} +
+
+ Valid: + {{ .isValid }} +
+ +
+ Dump of data +
{{ .dump }}
+
+ +
+ Form as JSON +
{{ .json }}
+
+
+ + {{if .isValid}} +
The form is valid!
+ {{else}} +
The form is invalid!
+ {{end}} + + {{ form .form }} +
+ + + + diff --git a/example/view/html5.html b/example/view/html5.html new file mode 100644 index 0000000..90c33fb --- /dev/null +++ b/example/view/html5.html @@ -0,0 +1,133 @@ + + + + + Form HTML5 (with Pico) + + + + + + + + +
+ Debug view + +
+ Submitted: + {{ .isSubmitted }} +
+
+ Valid: + {{ .isValid }} +
+ +
+ Dump of data +
{{ .dump }}
+
+ +
+ Form as JSON +
{{ .json }}
+
+ +
+ + {{if .isValid}} +

The form is valid!

+ {{else}} +

The form is invalid!

+ {{end}} + + {{ form .form }} + + + + + diff --git a/main.go b/main.go deleted file mode 100644 index 7c4e97a..0000000 --- a/main.go +++ /dev/null @@ -1,219 +0,0 @@ -package main - -import ( - "encoding/json" - "html/template" - "log" - "net/http" - - "github.com/yassinebenaid/godump" - "gitnet.fr/deblan/go-form/example" - "gitnet.fr/deblan/go-form/theme" -) - -func main() { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - data := example.ExampleData{} - data.Collection = []example.CollectionItem{ - {"Value a 1", "Value b 1"}, - } - - f := example.CreateDataForm() - f.Mount(data) - - if r.Method == f.Method { - f.HandleRequest(r) - - if f.IsSubmitted() && f.IsValid() { - f.Bind(&data) - // godump.Dump(data) - } - } - - render := theme.NewRenderer(theme.Html5) - - tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` - - - - - Form - - - -
-
- Submitted - {{ .Form.IsSubmitted }} -
-
- Valid - {{ .Form.IsValid }} -
- - - - - -
-
{{ .Dump }}
-
-
- JSON -
{{ .Json }}
-
-
-
- - {{ form .Form }} - - - - - `) - - var dump godump.Dumper - dump.Theme = godump.Theme{} - - j, _ := json.MarshalIndent(f, " ", " ") - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - tpl.Execute(w, map[string]any{ - "Form": f, - "Json": string(j), - "Dump": template.HTML(dump.Sprint(data)), - }) - }) - - http.HandleFunc("/bootstrap", func(w http.ResponseWriter, r *http.Request) { - data := example.ExampleData{} - - f := example.CreateDataForm() - f.WithAction("/bootstrap") - f.Mount(data) - - if r.Method == f.Method { - f.HandleRequest(r) - - if f.IsSubmitted() && f.IsValid() { - f.Bind(&data) - godump.Dump(data) - } - } - - render := theme.NewRenderer(theme.Bootstrap5) - - tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` - - - - - Form - - - -
-
-
- Submitted - {{ .Form.IsSubmitted }} -
-
- Valid - {{ .Form.IsValid }} -
-
- Data -
{{ .Dump }}
-
-
- - {{ form .Form }} -
- - - `) - - var dump godump.Dumper - dump.Theme = godump.Theme{} - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - tpl.Execute(w, map[string]any{ - "Form": f, - "Dump": template.HTML(dump.Sprint(data)), - }) - }) - - log.Fatal(http.ListenAndServe(":1122", nil)) -} From bec0acd2f2e333e7317eb36cfc1211f09c43fb5f Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 18:14:08 +0200 Subject: [PATCH 27/31] feat(theme/html5): add classes for help and errors html nodes --- theme/html5.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/theme/html5.go b/theme/html5.go index a4728e0..2d7889e 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -59,6 +59,7 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { } return Ul( + Class("gf-errors"), Group(result), ) } @@ -94,6 +95,7 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { } return Div( + Class("gf-help"), Text(help), extra, ) @@ -452,6 +454,7 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { form := args[0].(*form.Form) return Form( + Class("gf-form"), Action(form.Action), Method(form.Method), parent["form_attributes"](parent, form), From 7373b1921235164584cd9a9d292118a61012d3e6 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 18:15:23 +0200 Subject: [PATCH 28/31] doc: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f052f98..12a7ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +### Added + +- feat: add collection widget + ## v1.4.0 ### Added From f70f0a1f9d191307163268fe864aa5476eedc6ed Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 23:23:13 +0200 Subject: [PATCH 29/31] feat(example): add theme picker --- example.go | 25 ++++++++++++++---------- example/form.go | 38 +++++++++++++++++++++++++++++++++++++ example/view/bootstrap.html | 12 +++++++++--- example/view/html5.html | 9 +++++++-- 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/example.go b/example.go index 2aa6727..91918d4 100644 --- a/example.go +++ b/example.go @@ -17,21 +17,24 @@ var templates embed.FS func handler(view, action string, formRenderer *theme.Renderer, w http.ResponseWriter, r *http.Request) { entity := example.ExampleData{} - form := example.CreateDataForm(action) + entityForm := example.CreateDataForm(action) + entityForm.Mount(entity) - form.Mount(entity) + style := example.NewTheme(action) + styleForm := example.CreateThemeSelectorForm() + styleForm.Mount(style) - if r.Method == form.Method { - form.HandleRequest(r) + if r.Method == entityForm.Method { + entityForm.HandleRequest(r) - if form.IsSubmitted() && form.IsValid() { - form.Bind(&entity) + if entityForm.IsSubmitted() && entityForm.IsValid() { + entityForm.Bind(&entity) } } content, _ := templates.ReadFile(view) - formAsJson, _ := json.MarshalIndent(form, " ", " ") + formAsJson, _ := json.MarshalIndent(entityForm, " ", " ") tpl, _ := template.New("page"). Funcs(formRenderer.FuncMap()). @@ -43,9 +46,10 @@ func handler(view, action string, formRenderer *theme.Renderer, w http.ResponseW dump.Theme = godump.Theme{} tpl.Execute(w, map[string]any{ - "isSubmitted": form.IsSubmitted(), - "isValid": form.IsValid(), - "form": form, + "isSubmitted": entityForm.IsSubmitted(), + "isValid": entityForm.IsValid(), + "form": entityForm, + "styleForm": styleForm, "json": string(formAsJson), "dump": template.HTML(dump.Sprint(entity)), }) @@ -72,5 +76,6 @@ func main() { ) }) + log.Println("Browse: http://localhost:1122") log.Fatal(http.ListenAndServe(":1122", nil)) } diff --git a/example/form.go b/example/form.go index 8c516ba..fc6e8bf 100644 --- a/example/form.go +++ b/example/form.go @@ -50,6 +50,10 @@ type CollectionItem struct { ValueB string } +type Theme struct { + Value string `field:"lowerCamel"` +} + func CreateDataForm(action string) *form.Form { items := []Item{ Item{Id: 1, Name: "Item 1"}, @@ -211,3 +215,37 @@ func CreateDataForm(action string) *form.Form { WithMethod(http.MethodPost). WithAction(action) } + +func NewTheme(value string) *Theme { + return &Theme{Value: value} +} + +func CreateThemeSelectorForm() *form.Form { + choices := form.NewChoices([]map[string]string{ + map[string]string{"value": "/", "label": "Html5"}, + map[string]string{"value": "/bootstrap", "label": "Bootstrap5"}, + }) + + choices.LabelBuilder = func(key int, item any) string { + return item.(map[string]string)["label"] + } + + choices.ValueBuilder = func(key int, item any) string { + return item.(map[string]string)["value"] + } + + return form.NewForm( + form.NewFieldChoice("value"). + WithOptions( + form.NewOption("choices", choices), + form.NewOption("label", "Select a theme"), + form.NewOption("required", true), + form.NewOption("attr", form.Attrs{ + "onchange": "document.location.href = this.value", + }), + ), + ). + End(). + WithName(""). + WithMethod(http.MethodGet) +} diff --git a/example/view/bootstrap.html b/example/view/bootstrap.html index 7b848f1..23543fd 100644 --- a/example/view/bootstrap.html +++ b/example/view/bootstrap.html @@ -2,11 +2,12 @@ - Form with Bootstrap + Demo of go-form with Bootstrap -
+
+ {{ form_widget (.styleForm.GetField "value") }} +
+ +

Demo of go-form with Bootstrap

+
Debug view
diff --git a/example/view/html5.html b/example/view/html5.html index 90c33fb..88db2d8 100644 --- a/example/view/html5.html +++ b/example/view/html5.html @@ -2,7 +2,7 @@ - Form HTML5 (with Pico) + Demo of go-form (pure HTML5 and Pico) @@ -41,7 +41,12 @@ } - + + {{ form_widget (.styleForm.GetField "value") }} +
+ +

Demo of go-form (pure HTML5 and Pico)

+
Debug view From fe5d84d200a2e7dd53d2b46870fd7f31656d4813 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 23:24:42 +0200 Subject: [PATCH 30/31] update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12a7ade..ba56fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ ## [Unreleased] +## v1.5.0 + ### Added - feat: add collection widget +- feat: refactoring and improvement of example ## v1.4.0 From f451d69d702b350f7361de930153b88f4638015f Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 23:30:16 +0200 Subject: [PATCH 31/31] doc: add documentation of functions and licence --- form/field.go | 2 ++ form/field_collection.go | 10 +++++----- form/form.go | 1 + util/collection.go | 15 +++++++++++++++ util/transformer.go | 15 +++++++++++++++ 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/form/field.go b/form/field.go index f1558c2..fe63326 100644 --- a/form/field.go +++ b/form/field.go @@ -158,6 +158,7 @@ func (f *Field) WithOptions(options ...*Option) *Field { return f } +// Remove an option if exists func (f *Field) RemoveOption(name string) *Field { var options []*Option @@ -377,6 +378,7 @@ func (f *Field) Bind(data map[string]any, key *string, parentIsSlice bool) error return nil } +// Generates a tree of errors func (f *Field) ErrorsTree(tree map[string]any, key *string) { var index string diff --git a/form/field_collection.go b/form/field_collection.go index 52b8e02..3f3ece1 100644 --- a/form/field_collection.go +++ b/form/field_collection.go @@ -1,10 +1,5 @@ package form -import ( - "fmt" - "reflect" -) - // @license GNU AGPL version 3 or any later version // // This program is free software: you can redistribute it and/or modify @@ -20,6 +15,11 @@ import ( // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +import ( + "fmt" + "reflect" +) + // Generates a sub form func NewFieldCollection(name string) *Field { f := NewField(name, "collection"). diff --git a/form/form.go b/form/form.go index c17da05..04016d3 100644 --- a/form/form.go +++ b/form/form.go @@ -293,6 +293,7 @@ func (f *Form) IsSubmitted() bool { return f.RequestData != nil } +// Generates a tree of errors func (f *Form) ErrorsTree() map[string]any { errors := make(map[string]any) diff --git a/util/collection.go b/util/collection.go index 7ee4d66..f0aa649 100644 --- a/util/collection.go +++ b/util/collection.go @@ -1,5 +1,20 @@ package util +// @license GNU AGPL version 3 or any later version +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + import ( "regexp" "strings" diff --git a/util/transformer.go b/util/transformer.go index 743b117..a942fa1 100644 --- a/util/transformer.go +++ b/util/transformer.go @@ -1,5 +1,20 @@ package util +// @license GNU AGPL version 3 or any later version +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + import ( "fmt" "net/url"