mirror of
https://github.com/Bornholm/formidable.git
synced 2025-07-13 17:11:34 +02:00
Initial commit
This commit is contained in:
49
internal/server/option.go
Normal file
49
internal/server/option.go
Normal file
@ -0,0 +1,49 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/wpetit/formidable/internal/def"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
Host string
|
||||
Port uint
|
||||
Schema *jsonschema.Schema
|
||||
Values interface{}
|
||||
Defaults interface{}
|
||||
}
|
||||
|
||||
type OptionFunc func(*Option)
|
||||
|
||||
func defaultOption() *Option {
|
||||
return &Option{
|
||||
Host: "",
|
||||
Port: 0,
|
||||
Schema: def.Schema,
|
||||
}
|
||||
}
|
||||
|
||||
func WithAddress(host string, port uint) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.Host = host
|
||||
opt.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithSchema(schema *jsonschema.Schema) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.Schema = schema
|
||||
}
|
||||
}
|
||||
|
||||
func WithValues(values interface{}) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.Values = values
|
||||
}
|
||||
}
|
||||
|
||||
func WithDefaults(defaults interface{}) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.Defaults = defaults
|
||||
}
|
||||
}
|
161
internal/server/route/form.go
Normal file
161
internal/server/route/form.go
Normal file
@ -0,0 +1,161 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/wpetit/formidable/internal/jsonpointer"
|
||||
"forge.cadoles.com/wpetit/formidable/internal/server/template"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
)
|
||||
|
||||
func createRenderFormHandlerFunc(schema *jsonschema.Schema, defaults, values interface{}) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data := &template.FormItemData{
|
||||
Parent: nil,
|
||||
Schema: schema,
|
||||
Property: "",
|
||||
Defaults: defaults,
|
||||
Values: values,
|
||||
}
|
||||
|
||||
if err := schema.Validate(data.Values); err != nil {
|
||||
validationErr, ok := err.(*jsonschema.ValidationError)
|
||||
if !ok {
|
||||
panic(errors.Wrap(err, "could not validate values"))
|
||||
}
|
||||
|
||||
data.Error = validationErr
|
||||
}
|
||||
|
||||
if err := template.Exec("index.html.tmpl", w, data); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createHandleFormHandlerFunc(schema *jsonschema.Schema, defaults, values interface{}) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data := &template.FormItemData{
|
||||
Parent: nil,
|
||||
Schema: schema,
|
||||
Property: "",
|
||||
Defaults: defaults,
|
||||
Values: values,
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
} else {
|
||||
values, err = handleForm(r.Form, schema, values)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
data.Values = values
|
||||
}
|
||||
|
||||
if err := schema.Validate(data.Values); err != nil {
|
||||
validationErr, ok := err.(*jsonschema.ValidationError)
|
||||
if !ok {
|
||||
panic(errors.Wrap(err, "could not validate values"))
|
||||
}
|
||||
|
||||
data.Error = validationErr
|
||||
}
|
||||
|
||||
if err := template.Exec("index.html.tmpl", w, data); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleForm(form url.Values, schema *jsonschema.Schema, values interface{}) (interface{}, error) {
|
||||
pendingDeletes := make([]string, 0)
|
||||
|
||||
var err error
|
||||
|
||||
for name, fieldValues := range form {
|
||||
if name == "submit" {
|
||||
continue
|
||||
}
|
||||
|
||||
prefix, property, err := parseFieldName(name)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
switch prefix {
|
||||
case "bool":
|
||||
booVal, err := parseBoolean(fieldValues[0])
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not parse boolean field '%s'", property)
|
||||
}
|
||||
|
||||
pointer := jsonpointer.New(property)
|
||||
|
||||
values, err = pointer.Force(values, booVal)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not set property '%s' with value '%v'", property, fieldValues[0])
|
||||
}
|
||||
|
||||
case "add":
|
||||
pointer := jsonpointer.New(property)
|
||||
|
||||
values, err = pointer.Force(values, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not add item '%s'", property)
|
||||
}
|
||||
|
||||
case "del":
|
||||
// Mark property for deletion pass
|
||||
pendingDeletes = append(pendingDeletes, property)
|
||||
|
||||
default:
|
||||
pointer := jsonpointer.New(property)
|
||||
|
||||
values, err = pointer.Force(values, fieldValues[0])
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not set property '%s' with value '%v'", property, fieldValues[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, property := range pendingDeletes {
|
||||
pointer := jsonpointer.New(property)
|
||||
|
||||
values, err = pointer.Delete(values)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not delete property '%s'", property)
|
||||
}
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func parseBoolean(value string) (bool, error) {
|
||||
switch value {
|
||||
case "yes":
|
||||
return true, nil
|
||||
case "no":
|
||||
return false, nil
|
||||
default:
|
||||
return false, errors.Errorf("unexpected boolean value '%s'", value)
|
||||
}
|
||||
}
|
||||
|
||||
func parseFieldName(name string) (string, string, error) {
|
||||
tokens := strings.SplitN(name, ":", 2)
|
||||
|
||||
if len(tokens) == 1 {
|
||||
return "", tokens[0], nil
|
||||
}
|
||||
|
||||
if len(tokens) == 2 {
|
||||
return tokens[0], tokens[1], nil
|
||||
}
|
||||
|
||||
return "", "", errors.Errorf("unexpected field name '%s'", name)
|
||||
}
|
19
internal/server/route/handler.go
Normal file
19
internal/server/route/handler.go
Normal file
@ -0,0 +1,19 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
)
|
||||
|
||||
func NewHandler(schema *jsonschema.Schema, defaults, values interface{}) (*chi.Mux, error) {
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Use(middleware.RequestID)
|
||||
// router.Use(middleware.Logger)
|
||||
|
||||
router.Get("/", createRenderFormHandlerFunc(schema, defaults, values))
|
||||
router.Post("/", createHandleFormHandlerFunc(schema, defaults, values))
|
||||
|
||||
return router, nil
|
||||
}
|
98
internal/server/server.go
Normal file
98
internal/server/server.go
Normal file
@ -0,0 +1,98 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/formidable/internal/server/route"
|
||||
"forge.cadoles.com/wpetit/formidable/internal/server/template"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
host string
|
||||
port uint
|
||||
schema *jsonschema.Schema
|
||||
defaults interface{}
|
||||
values interface{}
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
||||
errs := make(chan error)
|
||||
addrs := make(chan net.Addr)
|
||||
|
||||
go s.run(ctx, addrs, errs)
|
||||
|
||||
return addrs, errs
|
||||
}
|
||||
|
||||
func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan error) {
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
defer cancel()
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.host, s.port))
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
addrs <- listener.Addr()
|
||||
|
||||
defer func() {
|
||||
if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
errs <- errors.WithStack(err)
|
||||
}
|
||||
|
||||
close(errs)
|
||||
close(addrs)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
|
||||
if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
log.Printf("%+v", errors.WithStack(err))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := template.Load(); err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
handler, err := route.NewHandler(s.schema, s.defaults, s.values)
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("http server listening")
|
||||
|
||||
if err := http.Serve(listener, handler); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
errs <- errors.WithStack(err)
|
||||
}
|
||||
|
||||
log.Println("http server exiting")
|
||||
}
|
||||
|
||||
func New(funcs ...OptionFunc) *Server {
|
||||
opt := defaultOption()
|
||||
for _, fn := range funcs {
|
||||
fn(opt)
|
||||
}
|
||||
|
||||
return &Server{
|
||||
host: opt.Host,
|
||||
port: opt.Port,
|
||||
schema: opt.Schema,
|
||||
defaults: opt.Defaults,
|
||||
values: opt.Values,
|
||||
}
|
||||
}
|
19
internal/server/template/blocks/form.html.tmpl
Normal file
19
internal/server/template/blocks/form.html.tmpl
Normal file
@ -0,0 +1,19 @@
|
||||
{{define "form"}}
|
||||
<form method="post">
|
||||
<table width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" nowrap=""></td>
|
||||
<td align="right" nowrap="">
|
||||
<input type="submit" name="submit" value="Enregistrer" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<strong>{{ .Schema.Title }}</strong>
|
||||
<em>{{ .Schema.Description }}</em>
|
||||
<hr />
|
||||
{{template "form_item" .}}
|
||||
</form>
|
||||
{{end}}
|
10
internal/server/template/blocks/form_input.html.tmpl
Normal file
10
internal/server/template/blocks/form_input.html.tmpl
Normal file
@ -0,0 +1,10 @@
|
||||
{{define "form_input"}}
|
||||
{{ if .Schema.Types }}
|
||||
{{ $root := . }}
|
||||
{{range .Schema.Types}}
|
||||
{{ $inputBlock := printf "%s_%s" "form_input" . }}
|
||||
{{ include $inputBlock $root }}
|
||||
{{end}}
|
||||
{{ else }}
|
||||
{{ end }}
|
||||
{{end}}
|
31
internal/server/template/blocks/form_input_array.html.tmpl
Normal file
31
internal/server/template/blocks/form_input_array.html.tmpl
Normal file
@ -0,0 +1,31 @@
|
||||
{{ define "form_input_array" }}
|
||||
{{ $root := . }}
|
||||
{{ $fullProperty := getFullProperty .Parent .Property }}
|
||||
{{ $values := getValue .Defaults .Values $fullProperty }}
|
||||
<table width="100%">
|
||||
<tbody>
|
||||
{{ range $index, $value := $values }}
|
||||
{{ $itemFullProperty := printf "%s/%d" $fullProperty $index }}
|
||||
{{ $itemProperty := printf "%d" $index }}
|
||||
{{ $itemSchema := getItemSchema $root.Schema }}
|
||||
{{ $formItemData := formItemData $root $itemProperty $itemSchema }}
|
||||
|
||||
<tr>
|
||||
{{ template "form_row" $formItemData }}
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<input type="submit" name="del:{{ $fullProperty }}/{{$index}}" value="Supprimer" />
|
||||
<hr />
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
<tr>
|
||||
<td colspan="2"></td>
|
||||
<td align="right">
|
||||
<input type="submit" name="add:{{ $fullProperty }}/-" value="Ajouter" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
12
internal/server/template/blocks/form_input_boolean.html.tmpl
Normal file
12
internal/server/template/blocks/form_input_boolean.html.tmpl
Normal file
@ -0,0 +1,12 @@
|
||||
{{define "form_input_boolean"}}
|
||||
{{ $fullProperty := getFullProperty .Parent .Property }}
|
||||
{{ $checked := getValue .Defaults .Values $fullProperty }}
|
||||
<label for="yes:{{ $fullProperty }}">
|
||||
Yes
|
||||
<input type="radio" id="yes:{{ $fullProperty }}" name="bool:{{ $fullProperty }}" value="yes" {{if $checked}}checked="yes"{{end}} />
|
||||
</label>
|
||||
<label for="no:{{ $fullProperty }}">
|
||||
No
|
||||
<input type="radio" id="no:{{ $fullProperty }}" name="bool:{{ $fullProperty }}" value="no" {{if not $checked}}checked{{end}} />
|
||||
</label>
|
||||
{{end}}
|
@ -0,0 +1,3 @@
|
||||
{{define "form_input_integer"}}
|
||||
{{template "form_input_number" .}}
|
||||
{{end}}
|
@ -0,0 +1 @@
|
||||
{{define "form_input_null"}}{{end}}
|
@ -0,0 +1,5 @@
|
||||
{{define "form_input_number"}}
|
||||
{{ $fullProperty := getFullProperty .Parent .Property }}
|
||||
{{ $value := getValue .Defaults .Values $fullProperty }}
|
||||
<input type="number" name="{{ $fullProperty }}" value="{{ $value }}" />
|
||||
{{end}}
|
@ -0,0 +1,5 @@
|
||||
{{define "form_input_object"}}
|
||||
<br />
|
||||
{{ $formItemData := formItemData . "" .Schema }}
|
||||
{{template "form_item" $formItemData}}
|
||||
{{end}}
|
@ -0,0 +1,5 @@
|
||||
{{define "form_input_string"}}
|
||||
{{ $fullProperty := getFullProperty .Parent .Property }}
|
||||
{{ $value := getValue .Defaults .Values $fullProperty }}
|
||||
<input type="text" name="{{ $fullProperty }}" id="{{ $fullProperty }}" value="{{ $value }}" />
|
||||
{{end}}
|
11
internal/server/template/blocks/form_item.html.tmpl
Normal file
11
internal/server/template/blocks/form_item.html.tmpl
Normal file
@ -0,0 +1,11 @@
|
||||
{{define "form_item"}}
|
||||
<table width="100%">
|
||||
<tbody>
|
||||
{{ $root := .}}
|
||||
{{ range $property, $schema := .Schema.Properties}}
|
||||
{{ $formItemData := formItemData $root $property $schema }}
|
||||
{{template "form_row" $formItemData}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
23
internal/server/template/blocks/form_row.html.tmpl
Normal file
23
internal/server/template/blocks/form_row.html.tmpl
Normal file
@ -0,0 +1,23 @@
|
||||
{{define "form_row"}}
|
||||
{{ $fullProperty := getFullProperty .Parent .Property }}
|
||||
<tr>
|
||||
<td align="left" nowrap="">
|
||||
<label for="{{ $fullProperty }}">
|
||||
<strong>
|
||||
{{ if .Schema.Title }}{{ .Schema.Title }}{{ else }}{{ .Property }}{{ end }}
|
||||
</strong>
|
||||
<br />
|
||||
<span>{{ .Schema.Description }}</span>
|
||||
</label>
|
||||
</td>
|
||||
<td align="left" nowrap="">
|
||||
{{template "form_input" .}}
|
||||
</td>
|
||||
<td>
|
||||
{{ $err := getPropertyError .Error $fullProperty }}
|
||||
{{if $err}}
|
||||
<em>{{ $err.Message }}</em>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
10
internal/server/template/blocks/head.html.tmpl
Normal file
10
internal/server/template/blocks/head.html.tmpl
Normal file
@ -0,0 +1,10 @@
|
||||
{{define "head"}}
|
||||
<head>
|
||||
<title>Formidable</title>
|
||||
<style>
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
{{end}}
|
6
internal/server/template/layouts/index.html.tmpl
Normal file
6
internal/server/template/layouts/index.html.tmpl
Normal file
@ -0,0 +1,6 @@
|
||||
<html>
|
||||
{{ template "head" . }}
|
||||
<body>
|
||||
{{ template "form" . }}
|
||||
</body>
|
||||
</html>
|
233
internal/server/template/template.go
Normal file
233
internal/server/template/template.go
Normal file
@ -0,0 +1,233 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/wpetit/formidable/internal/jsonpointer"
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed layouts/* blocks/*
|
||||
files embed.FS
|
||||
layouts map[string]*template.Template
|
||||
blocks map[string]string
|
||||
)
|
||||
|
||||
func Load() error {
|
||||
if blocks == nil {
|
||||
blocks = make(map[string]string)
|
||||
}
|
||||
|
||||
blockFiles, err := fs.ReadDir(files, "blocks")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, f := range blockFiles {
|
||||
templateData, err := fs.ReadFile(files, "blocks/"+f.Name())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blocks[f.Name()] = string(templateData)
|
||||
}
|
||||
|
||||
layoutFiles, err := fs.ReadDir(files, "layouts")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, f := range layoutFiles {
|
||||
templateData, err := fs.ReadFile(files, "layouts/"+f.Name())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := loadLayout(f.Name(), string(templateData)); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadLayout(name string, rawTemplate string) error {
|
||||
if layouts == nil {
|
||||
layouts = make(map[string]*template.Template)
|
||||
}
|
||||
|
||||
tmpl := template.New(name)
|
||||
funcMap := mergeHelpers(
|
||||
sprig.FuncMap(),
|
||||
customHelpers(tmpl),
|
||||
)
|
||||
|
||||
tmpl.Funcs(funcMap)
|
||||
|
||||
for blockName, b := range blocks {
|
||||
if _, err := tmpl.Parse(b); err != nil {
|
||||
return errors.Wrapf(err, "could not parse template block '%s'", blockName)
|
||||
}
|
||||
}
|
||||
|
||||
tmpl, err := tmpl.Parse(rawTemplate)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not parse template '%s'", name)
|
||||
}
|
||||
|
||||
layouts[name] = tmpl
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Exec(name string, w io.Writer, data interface{}) error {
|
||||
tmpl, exists := layouts[name]
|
||||
if !exists {
|
||||
return errors.Errorf("could not find template '%s'", name)
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeHelpers(helpers ...template.FuncMap) template.FuncMap {
|
||||
merged := template.FuncMap{}
|
||||
|
||||
for _, help := range helpers {
|
||||
for name, fn := range help {
|
||||
merged[name] = fn
|
||||
}
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
type FormItemData struct {
|
||||
Parent *FormItemData
|
||||
Schema *jsonschema.Schema
|
||||
Property string
|
||||
Error *jsonschema.ValidationError
|
||||
Values interface{}
|
||||
Defaults interface{}
|
||||
}
|
||||
|
||||
func customHelpers(tpl *template.Template) template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"formItemData": func(parent *FormItemData, property string, schema *jsonschema.Schema) *FormItemData {
|
||||
return &FormItemData{
|
||||
Parent: parent,
|
||||
Property: property,
|
||||
Schema: schema,
|
||||
Defaults: parent.Defaults,
|
||||
Values: parent.Values,
|
||||
Error: parent.Error,
|
||||
}
|
||||
},
|
||||
"dump": func(data interface{}) string {
|
||||
spew.Dump(data)
|
||||
|
||||
return ""
|
||||
},
|
||||
"include": func(name string, data interface{}) (template.HTML, error) {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
|
||||
if err := tpl.ExecuteTemplate(buf, name, data); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return template.HTML(buf.String()), nil
|
||||
},
|
||||
"getFullProperty": func(parent *FormItemData, property string) string {
|
||||
fullProperty := property
|
||||
for {
|
||||
fullProperty = fmt.Sprintf("%s/%s", parent.Property, strings.TrimPrefix(fullProperty, "/"))
|
||||
parent = parent.Parent
|
||||
if parent == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return fullProperty
|
||||
},
|
||||
"getValue": func(defaults, values interface{}, path string) (interface{}, error) {
|
||||
if defaults == nil {
|
||||
defaults = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if values == nil {
|
||||
values = make(map[string]interface{})
|
||||
}
|
||||
|
||||
pointer := jsonpointer.New(path)
|
||||
|
||||
val, err := pointer.Get(values)
|
||||
if err != nil && !errors.Is(err, jsonpointer.ErrNotFound) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if errors.Is(err, jsonpointer.ErrNotFound) {
|
||||
val, err = pointer.Get(defaults)
|
||||
if err != nil && !errors.Is(err, jsonpointer.ErrNotFound) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return val, nil
|
||||
},
|
||||
"getItemSchema": func(arraySchema *jsonschema.Schema) (*jsonschema.Schema, error) {
|
||||
itemSchema := arraySchema.Items
|
||||
if itemSchema == nil {
|
||||
itemSchema = arraySchema.Items2020
|
||||
}
|
||||
|
||||
if itemSchema == nil {
|
||||
return nil, errors.New("item schema not found")
|
||||
}
|
||||
|
||||
switch schema := itemSchema.(type) {
|
||||
case *jsonschema.Schema:
|
||||
return schema, nil
|
||||
case []*jsonschema.Schema:
|
||||
if len(schema) > 0 {
|
||||
return schema[0], nil
|
||||
}
|
||||
|
||||
return nil, errors.New("no item schema found")
|
||||
default:
|
||||
return nil, errors.Errorf("unexpected schema type '%T'", schema)
|
||||
}
|
||||
},
|
||||
"getPropertyError": findPropertyValidationError,
|
||||
}
|
||||
}
|
||||
|
||||
func findPropertyValidationError(err *jsonschema.ValidationError, property string) *jsonschema.ValidationError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if property == err.InstanceLocation {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cause := range err.Causes {
|
||||
if err := findPropertyValidationError(cause, property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user