mirror of
https://github.com/Bornholm/formidable.git
synced 2024-12-22 14:59:34 +01:00
Initial commit
This commit is contained in:
commit
ada7f18e36
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/.env
|
||||
/bin
|
31
Makefile
Normal file
31
Makefile
Normal file
@ -0,0 +1,31 @@
|
||||
.DEFAULT_GOAL := help
|
||||
LINT_ARGS ?= --timeout 5m
|
||||
FRMD_CMD ?=
|
||||
SHELL = /bin/bash
|
||||
|
||||
.PHONY: help
|
||||
help: ## Display this help
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
watch: ## Watching updated files - live reload
|
||||
go run -mod=readonly github.com/cortesi/modd/cmd/modd@latest
|
||||
|
||||
test: .env ## Executing tests
|
||||
( set -o allexport && source .env && set +o allexport && go test -v -race -count=1 $(GOTEST_ARGS) ./... )
|
||||
|
||||
lint: ## Lint sources code
|
||||
golangci-lint run --enable-all $(LINT_ARGS)
|
||||
|
||||
build: build-frmd ## Build artefacts
|
||||
|
||||
build-frmd: ## Build executable
|
||||
CGO_ENABLED=0 go build -v -o ./bin/frmd ./cmd/frmd
|
||||
|
||||
.env:
|
||||
cp .env.dist .env
|
||||
|
||||
deps:
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
./misc/script/release
|
86
cmd/frmd/main.go
Normal file
86
cmd/frmd/main.go
Normal file
@ -0,0 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"forge.cadoles.com/wpetit/formidable/internal/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// nolint: gochecknoglobals
|
||||
var (
|
||||
GitRef = "unknown"
|
||||
ProjectVersion = "unknown"
|
||||
BuildDate = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
app := &cli.App{
|
||||
Name: "frmd",
|
||||
Usage: "JSON Schema based cli forms",
|
||||
Commands: command.Root(),
|
||||
Before: func(ctx *cli.Context) error {
|
||||
workdir := ctx.String("workdir")
|
||||
// Switch to new working directory if defined
|
||||
if workdir != "" {
|
||||
if err := os.Chdir(workdir); err != nil {
|
||||
return errors.Wrap(err, "could not change working directory")
|
||||
}
|
||||
}
|
||||
|
||||
if err := ctx.Set("projectVersion", ProjectVersion); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := ctx.Set("gitRef", GitRef); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := ctx.Set("buildDate", BuildDate); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "workdir",
|
||||
Value: "",
|
||||
Usage: "The working directory",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "projectVersion",
|
||||
Value: "",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "gitRef",
|
||||
Value: "",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "buildDate",
|
||||
Value: "",
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.ExitErrHandler = func(ctx *cli.Context, err error) {
|
||||
fmt.Printf("%+v", err)
|
||||
}
|
||||
|
||||
sort.Sort(cli.FlagsByName(app.Flags))
|
||||
sort.Sort(cli.CommandsByName(app.Commands))
|
||||
|
||||
err := app.RunContext(ctx, os.Args)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
44
go.mod
Normal file
44
go.mod
Normal file
@ -0,0 +1,44 @@
|
||||
module forge.cadoles.com/wpetit/formidable
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/urfave/cli/v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
||||
github.com/google/uuid v1.1.1 // indirect
|
||||
github.com/huandu/xstrings v1.3.1 // indirect
|
||||
github.com/imdario/mergo v0.3.11 // indirect
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/awesome-gocui/gocui v1.1.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 // indirect
|
||||
github.com/go-chi/chi v1.5.4
|
||||
github.com/jroimartin/gocui v0.5.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/nsf/termbox-go v1.1.1 // indirect
|
||||
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect
|
||||
github.com/skanehira/gocui-component v0.0.0-20190406233618-9b1c71353c96 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 // indirect
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
)
|
88
go.sum
Normal file
88
go.sum
Normal file
@ -0,0 +1,88 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
|
||||
github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
|
||||
github.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII=
|
||||
github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM=
|
||||
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
|
||||
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs=
|
||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/jroimartin/gocui v0.5.0 h1:DCZc97zY9dMnHXJSJLLmx9VqiEnAj0yh0eTNpuEtG/4=
|
||||
github.com/jroimartin/gocui v0.5.0/go.mod h1:l7Hz8DoYoL6NoYnlnaX6XCNR62G7J5FfSW5jEogzaxE=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
|
||||
github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 h1:xe+mmCnDN82KhC010l3NfYlA8ZbOuzbXAzSYBa6wbMc=
|
||||
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/skanehira/gocui-component v0.0.0-20190406233618-9b1c71353c96 h1:iNHvqWqQWIQ0wdWkcHW2sQhvRaVlCrnFmUftk7LcbrE=
|
||||
github.com/skanehira/gocui-component v0.0.0-20190406233618-9b1c71353c96/go.mod h1:uhDvc/srGKwvK9bGt4zlfTiywMLL7ngz44Yp2nQwTjE=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I=
|
||||
github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY=
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
156
internal/command/common.go
Normal file
156
internal/command/common.go
Normal file
@ -0,0 +1,156 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/wpetit/formidable/internal/def"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
filePathPrefix = "@"
|
||||
)
|
||||
|
||||
func commonFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "defaults",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Default values as JSON or file path prefixed by '@'",
|
||||
Value: "{}",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "values",
|
||||
Aliases: []string{"v"},
|
||||
Usage: "Current values as JSON or file path prefixed by '@'",
|
||||
Value: "{}",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "schema",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "Use `schema_file` as schema",
|
||||
TakesFile: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o", "out"},
|
||||
Value: "-",
|
||||
Usage: "Output modified values to `output_file` (or '-' for stdout, the default)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func loadJSONFlag(ctx *cli.Context, flagName string) (interface{}, error) {
|
||||
flagValue := ctx.String(flagName)
|
||||
|
||||
if flagValue == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(flagValue, filePathPrefix) {
|
||||
var value interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(flagValue), &value); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
flagValue = strings.TrimPrefix(flagValue, filePathPrefix)
|
||||
|
||||
file, err := os.Open(flagValue)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}()
|
||||
|
||||
reader := json.NewDecoder(file)
|
||||
|
||||
var values interface{}
|
||||
|
||||
if err := reader.Decode(&values); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func loadValues(ctx *cli.Context) (interface{}, error) {
|
||||
values, err := loadJSONFlag(ctx, "values")
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func loadDefaults(ctx *cli.Context) (interface{}, error) {
|
||||
values, err := loadJSONFlag(ctx, "defaults")
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func loadSchema(ctx *cli.Context) (*jsonschema.Schema, error) {
|
||||
schemaFlag := ctx.String("schema")
|
||||
|
||||
compiler := jsonschema.NewCompiler()
|
||||
|
||||
compiler.ExtractAnnotations = true
|
||||
compiler.AssertFormat = true
|
||||
compiler.AssertContent = true
|
||||
|
||||
var (
|
||||
schema *jsonschema.Schema
|
||||
err error
|
||||
)
|
||||
|
||||
if schemaFlag == "" {
|
||||
schema = def.Schema
|
||||
} else {
|
||||
schema, err = compiler.Compile(schemaFlag)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
const OutputStdout = "-"
|
||||
|
||||
type noopWriteCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (c *noopWriteCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputWriter(ctx *cli.Context) (io.WriteCloser, error) {
|
||||
output := ctx.String("output")
|
||||
|
||||
if output == OutputStdout {
|
||||
return &noopWriteCloser{ctx.App.Writer}, nil
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE, 0o644)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
68
internal/command/delete.go
Normal file
68
internal/command/delete.go
Normal file
@ -0,0 +1,68 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"forge.cadoles.com/wpetit/formidable/internal/jsonpointer"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func Delete() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete value at specific path",
|
||||
Flags: commonFlags(),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
schema, err := loadSchema(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load schema")
|
||||
}
|
||||
|
||||
values, err := loadValues(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load values")
|
||||
}
|
||||
|
||||
rawPointer := ctx.Args().Get(0)
|
||||
|
||||
pointer := jsonpointer.New(rawPointer)
|
||||
|
||||
var updatedValues interface{}
|
||||
|
||||
updatedValues, err = pointer.Delete(values)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not delete pointer '%v'", rawPointer)
|
||||
}
|
||||
|
||||
if err := schema.Validate(updatedValues); err != nil {
|
||||
if _, ok := err.(*jsonschema.ValidationError); ok {
|
||||
fmt.Printf("%#v\n", err)
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "could not validate resulting json")
|
||||
}
|
||||
|
||||
output, err := outputWriter(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create output writer")
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(output)
|
||||
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
if err := encoder.Encode(updatedValues); err != nil {
|
||||
return errors.Wrap(err, "could not write to output")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
98
internal/command/edit.go
Normal file
98
internal/command/edit.go
Normal file
@ -0,0 +1,98 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/wpetit/formidable/internal/server"
|
||||
"github.com/pkg/errors"
|
||||
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func Edit() *cli.Command {
|
||||
flags := commonFlags()
|
||||
|
||||
flags = append(flags, &cli.StringFlag{
|
||||
Name: "browser",
|
||||
EnvVars: []string{"FORMIDABLE_BROWSER"},
|
||||
Value: "w3m",
|
||||
})
|
||||
|
||||
return &cli.Command{
|
||||
Name: "edit",
|
||||
Usage: "Display a form for given schema and values",
|
||||
Flags: flags,
|
||||
Action: func(ctx *cli.Context) error {
|
||||
browser := ctx.String("browser")
|
||||
|
||||
schema, err := loadSchema(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load schema")
|
||||
}
|
||||
|
||||
values, err := loadValues(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load values")
|
||||
}
|
||||
|
||||
defaults, err := loadDefaults(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load defaults")
|
||||
}
|
||||
|
||||
srvCtx, srvCancel := context.WithCancel(ctx.Context)
|
||||
defer srvCancel()
|
||||
|
||||
srv := server.New(
|
||||
server.WithSchema(schema),
|
||||
server.WithValues(values),
|
||||
server.WithDefaults(defaults),
|
||||
)
|
||||
|
||||
addrs, srvErrs := srv.Start(srvCtx)
|
||||
|
||||
url := fmt.Sprintf("http://%s", (<-addrs).String())
|
||||
url = strings.Replace(url, "0.0.0.0", "127.0.0.1", 1)
|
||||
|
||||
log.Printf("listening on %s", url)
|
||||
|
||||
cmdErrs := make(chan error)
|
||||
cmdCtx, cmdCancel := context.WithCancel(ctx.Context)
|
||||
defer cmdCancel()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
close(cmdErrs)
|
||||
}()
|
||||
|
||||
cmd := exec.CommandContext(cmdCtx, browser, url)
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
cmdErrs <- errors.WithStack(err)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-cmdErrs:
|
||||
srvCancel()
|
||||
|
||||
return errors.WithStack(err)
|
||||
|
||||
case err := <-srvErrs:
|
||||
cmdCancel()
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
69
internal/command/get.go
Normal file
69
internal/command/get.go
Normal file
@ -0,0 +1,69 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"forge.cadoles.com/wpetit/formidable/internal/jsonpointer"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func Get() *cli.Command {
|
||||
flags := []cli.Flag{}
|
||||
|
||||
flags = append(flags, commonFlags()...)
|
||||
|
||||
return &cli.Command{
|
||||
Name: "get",
|
||||
Usage: "Get value at specific path",
|
||||
Flags: flags,
|
||||
Action: func(ctx *cli.Context) error {
|
||||
schema, err := loadSchema(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load schema")
|
||||
}
|
||||
|
||||
values, err := loadValues(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load values")
|
||||
}
|
||||
|
||||
if err := schema.Validate(values); err != nil {
|
||||
if _, ok := err.(*jsonschema.ValidationError); ok {
|
||||
fmt.Printf("%#v\n", err)
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "could not validate resulting json")
|
||||
}
|
||||
|
||||
rawPointer := ctx.Args().Get(0)
|
||||
pointer := jsonpointer.New(rawPointer)
|
||||
|
||||
value, err := pointer.Get(values)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not get value from pointer '%v'", rawPointer)
|
||||
}
|
||||
|
||||
output, err := outputWriter(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create output writer")
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(output)
|
||||
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
if err := encoder.Encode(value); err != nil {
|
||||
return errors.Wrap(err, "could not write to output")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
12
internal/command/root.go
Normal file
12
internal/command/root.go
Normal file
@ -0,0 +1,12 @@
|
||||
package command
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
|
||||
func Root() []*cli.Command {
|
||||
return []*cli.Command{
|
||||
Edit(),
|
||||
Set(),
|
||||
Get(),
|
||||
Delete(),
|
||||
}
|
||||
}
|
95
internal/command/set.go
Normal file
95
internal/command/set.go
Normal file
@ -0,0 +1,95 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"forge.cadoles.com/wpetit/formidable/internal/jsonpointer"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func Set() *cli.Command {
|
||||
flags := []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Force data tree creation",
|
||||
Value: false,
|
||||
},
|
||||
}
|
||||
|
||||
flags = append(flags, commonFlags()...)
|
||||
|
||||
return &cli.Command{
|
||||
Name: "set",
|
||||
Usage: "Set value at specific path",
|
||||
Flags: flags,
|
||||
Action: func(ctx *cli.Context) error {
|
||||
schema, err := loadSchema(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load schema")
|
||||
}
|
||||
|
||||
values, err := loadValues(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load values")
|
||||
}
|
||||
|
||||
rawPointer := ctx.Args().Get(0)
|
||||
rawValue := ctx.Args().Get(1)
|
||||
|
||||
pointer := jsonpointer.New(rawPointer)
|
||||
|
||||
var value interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(rawValue), &value); err != nil {
|
||||
return errors.Wrapf(err, "could not parse json '%s'", rawValue)
|
||||
}
|
||||
|
||||
var updatedValues interface{}
|
||||
|
||||
force := ctx.Bool("force")
|
||||
|
||||
if force {
|
||||
updatedValues, err = pointer.Force(values, value)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not force value '%v' to pointer '%v'", rawValue, rawPointer)
|
||||
}
|
||||
} else {
|
||||
updatedValues, err = pointer.Set(values, value)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not set value '%v' to pointer '%v'", rawValue, rawPointer)
|
||||
}
|
||||
}
|
||||
|
||||
if err := schema.Validate(updatedValues); err != nil {
|
||||
if _, ok := err.(*jsonschema.ValidationError); ok {
|
||||
fmt.Printf("%#v\n", err)
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "could not validate resulting json")
|
||||
}
|
||||
|
||||
output, err := outputWriter(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create output writer")
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(output)
|
||||
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
if err := encoder.Encode(updatedValues); err != nil {
|
||||
return errors.Wrap(err, "could not write to output")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
12
internal/def/schema.go
Normal file
12
internal/def/schema.go
Normal file
@ -0,0 +1,12 @@
|
||||
package def
|
||||
|
||||
import "github.com/santhosh-tekuri/jsonschema/v5"
|
||||
|
||||
const rawSchema = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Formidable default schema",
|
||||
"type": ["null", "boolean", "object", "array", "number", "string"]
|
||||
}`
|
||||
|
||||
var Schema = jsonschema.MustCompileString("", rawSchema)
|
73
internal/jsonpointer/delete.go
Normal file
73
internal/jsonpointer/delete.go
Normal file
@ -0,0 +1,73 @@
|
||||
package jsonpointer
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func del(doc interface{}, tokens []string) (interface{}, error) {
|
||||
currentToken := tokens[0]
|
||||
|
||||
switch typedDoc := doc.(type) {
|
||||
|
||||
case map[string]interface{}:
|
||||
nestedDoc, exists := typedDoc[currentToken]
|
||||
if !exists {
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
if len(tokens) == 1 {
|
||||
delete(typedDoc, currentToken)
|
||||
|
||||
return typedDoc, nil
|
||||
}
|
||||
|
||||
nestedDoc, err := del(nestedDoc, tokens[1:])
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
typedDoc[currentToken] = nestedDoc
|
||||
|
||||
return typedDoc, nil
|
||||
|
||||
case []interface{}:
|
||||
var (
|
||||
index uint64
|
||||
nestedDoc interface{}
|
||||
err error
|
||||
)
|
||||
|
||||
if currentToken == NonExistentMemberToken {
|
||||
index = uint64(len(typedDoc) - 1)
|
||||
} else {
|
||||
index, err = strconv.ParseUint(currentToken, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(typedDoc) <= int(index) {
|
||||
return typedDoc, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(tokens) == 1 {
|
||||
typedDoc = append(typedDoc[:index], typedDoc[index+1:]...)
|
||||
|
||||
return typedDoc, nil
|
||||
}
|
||||
|
||||
nestedDoc, err = del(nestedDoc, tokens[1:])
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
typedDoc[index] = nestedDoc
|
||||
|
||||
return typedDoc, nil
|
||||
|
||||
default:
|
||||
return typedDoc, nil
|
||||
}
|
||||
}
|
95
internal/jsonpointer/delete_test.go
Normal file
95
internal/jsonpointer/delete_test.go
Normal file
@ -0,0 +1,95 @@
|
||||
package jsonpointer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type pointerDeleteTestCase struct {
|
||||
DocPath string
|
||||
Pointer string
|
||||
ExpectedRawDocument string
|
||||
}
|
||||
|
||||
func TestPointerDelete(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []pointerDeleteTestCase{
|
||||
{
|
||||
DocPath: "./testdata/set/basic.json",
|
||||
Pointer: "/foo",
|
||||
ExpectedRawDocument: `{}`,
|
||||
},
|
||||
{
|
||||
DocPath: "./testdata/set/nested.json",
|
||||
Pointer: "/nestedObject/foo/1",
|
||||
ExpectedRawDocument: `
|
||||
{
|
||||
"nestedObject": {
|
||||
"foo": [
|
||||
"bar"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
DocPath: "./testdata/set/nested.json",
|
||||
Pointer: "/nestedObject/foo/-",
|
||||
ExpectedRawDocument: `
|
||||
{
|
||||
"nestedObject": {
|
||||
"foo": [
|
||||
"bar"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
func(index int, tc pointerDeleteTestCase) {
|
||||
t.Run(fmt.Sprintf("#%d: '%s'", i, tc.Pointer), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
baseRawDocument, err := ioutil.ReadFile(tc.DocPath)
|
||||
if err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
var baseDoc interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(baseRawDocument), &baseDoc); err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
pointer := New(tc.Pointer)
|
||||
|
||||
updatedDoc, err := pointer.Delete(baseDoc)
|
||||
if err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
rawDoc, err := json.MarshalIndent(updatedDoc, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
var expectedDoc interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(tc.ExpectedRawDocument), &expectedDoc); err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectedDoc, updatedDoc) {
|
||||
t.Errorf("Delete pointer '%s': expected document \n'%s', got \n'%s'", tc.Pointer, strings.TrimSpace(tc.ExpectedRawDocument), rawDoc)
|
||||
}
|
||||
})
|
||||
}(i, tc)
|
||||
}
|
||||
}
|
9
internal/jsonpointer/error.go
Normal file
9
internal/jsonpointer/error.go
Normal file
@ -0,0 +1,9 @@
|
||||
package jsonpointer
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrUnexpectedType = errors.New("unexpected type")
|
||||
ErrOutOfBounds = errors.New("out of bounds")
|
||||
)
|
111
internal/jsonpointer/force.go
Normal file
111
internal/jsonpointer/force.go
Normal file
@ -0,0 +1,111 @@
|
||||
package jsonpointer
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func force(doc interface{}, tokens []string, value interface{}) (interface{}, error) {
|
||||
if len(tokens) == 0 {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
currentToken := tokens[0]
|
||||
|
||||
switch typedDoc := doc.(type) {
|
||||
|
||||
case map[string]interface{}:
|
||||
nestedDoc, exists := typedDoc[currentToken]
|
||||
if !exists {
|
||||
if len(tokens) == 1 {
|
||||
typedDoc[currentToken] = value
|
||||
|
||||
return typedDoc, nil
|
||||
}
|
||||
|
||||
nextToken := tokens[1]
|
||||
if isArrayIndexToken(nextToken) {
|
||||
nestedDoc = make([]interface{}, 0)
|
||||
} else {
|
||||
nestedDoc = make(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
nestedDoc, err := force(nestedDoc, tokens[1:], value)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
typedDoc[currentToken] = nestedDoc
|
||||
|
||||
return typedDoc, nil
|
||||
|
||||
case []interface{}:
|
||||
var (
|
||||
index uint64
|
||||
nestedDoc interface{}
|
||||
err error
|
||||
)
|
||||
|
||||
if currentToken == NonExistentMemberToken {
|
||||
typedDoc = append(typedDoc, value)
|
||||
index = uint64(len(typedDoc) - 1)
|
||||
} else {
|
||||
index, err = strconv.ParseUint(currentToken, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(typedDoc) <= int(index) {
|
||||
for i := len(typedDoc); i <= int(index); i++ {
|
||||
typedDoc = append(typedDoc, nil)
|
||||
}
|
||||
}
|
||||
|
||||
nestedDoc = typedDoc[index]
|
||||
}
|
||||
|
||||
nestedDoc, err = force(nestedDoc, tokens[1:], value)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
typedDoc[index] = nestedDoc
|
||||
|
||||
return typedDoc, nil
|
||||
|
||||
default:
|
||||
overrideDoc := map[string]interface{}{}
|
||||
overrideDoc[currentToken] = value
|
||||
|
||||
var nestedDoc interface{}
|
||||
|
||||
if len(tokens) > 1 && isArrayIndexToken(tokens[1]) {
|
||||
nestedDoc = make([]interface{}, 0)
|
||||
} else {
|
||||
nestedDoc = make(map[string]interface{})
|
||||
}
|
||||
|
||||
nestedDoc, err := force(nestedDoc, tokens[1:], value)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
overrideDoc[currentToken] = nestedDoc
|
||||
|
||||
return overrideDoc, nil
|
||||
}
|
||||
}
|
||||
|
||||
func isArrayIndexToken(token string) bool {
|
||||
if token == NonExistentMemberToken {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, err := strconv.ParseUint(token, 10, 64); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
60
internal/jsonpointer/get.go
Normal file
60
internal/jsonpointer/get.go
Normal file
@ -0,0 +1,60 @@
|
||||
package jsonpointer
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func get(doc interface{}, tokens []string) (interface{}, error) {
|
||||
if len(tokens) == 0 {
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
currentToken := tokens[0]
|
||||
|
||||
if doc == nil {
|
||||
return nil, errors.Wrapf(ErrNotFound, "pointer '%s' not found on document", tokensToString(tokens))
|
||||
}
|
||||
|
||||
switch typedDoc := doc.(type) {
|
||||
case map[string]interface{}:
|
||||
value, exists := typedDoc[currentToken]
|
||||
if !exists {
|
||||
return nil, errors.Wrapf(ErrNotFound, "pointer '%s' not found on document", tokensToString(tokens))
|
||||
}
|
||||
|
||||
value, err := get(value, tokens[1:])
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
|
||||
case []interface{}:
|
||||
if currentToken == NonExistentMemberToken {
|
||||
return nil, errors.WithStack(ErrOutOfBounds)
|
||||
}
|
||||
|
||||
index, err := strconv.ParseUint(currentToken, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(typedDoc) <= int(index) {
|
||||
return nil, errors.WithStack(ErrOutOfBounds)
|
||||
}
|
||||
|
||||
value := typedDoc[index]
|
||||
|
||||
value, err = get(value, tokens[1:])
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
|
||||
default:
|
||||
return nil, errors.Wrapf(ErrUnexpectedType, "unexpected type '%T'", typedDoc)
|
||||
}
|
||||
}
|
140
internal/jsonpointer/get_test.go
Normal file
140
internal/jsonpointer/get_test.go
Normal file
@ -0,0 +1,140 @@
|
||||
package jsonpointer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type pointerGetTestCase struct {
|
||||
Document interface{}
|
||||
Pointer string
|
||||
ExpectedRawValue string
|
||||
}
|
||||
|
||||
func TestPointerGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ietfRawDocument, err := ioutil.ReadFile("./testdata/ietf.json")
|
||||
if err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
var ietfDoc interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(ietfRawDocument), &ietfDoc); err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
// IETF tests cases
|
||||
// From https://datatracker.ietf.org/doc/html/rfc6901
|
||||
//
|
||||
// "" // the whole document
|
||||
// "/foo" ["bar", "baz"]
|
||||
// "/foo/0" "bar"
|
||||
// "/" 0
|
||||
// "/a~1b" 1
|
||||
// "/c%d" 2
|
||||
// "/e^f" 3
|
||||
// "/g|h" 4
|
||||
// "/i\\j" 5
|
||||
// "/k\"l" 6
|
||||
// "/ " 7
|
||||
// "/m~0n" 8
|
||||
testCases := []pointerGetTestCase{
|
||||
{
|
||||
Document: ietfDoc,
|
||||
Pointer: "",
|
||||
ExpectedRawValue: string(ietfRawDocument),
|
||||
},
|
||||
{
|
||||
Document: ietfDoc,
|
||||
Pointer: "/foo",
|
||||
ExpectedRawValue: "[\"bar\", \"baz\"]",
|
||||
},
|
||||
{
|
||||
Document: ietfDoc,
|
||||
Pointer: "/foo/0",
|
||||
ExpectedRawValue: "\"bar\"",
|
||||
},
|
||||
{
|
||||
Document: ietfDoc,
|
||||
Pointer: `/`,
|
||||
ExpectedRawValue: `0`,
|
||||
},
|
||||
{
|
||||
Document: ietfDoc,
|
||||
Pointer: "/a~1b",
|
||||
ExpectedRawValue: "1",
|
||||
},
|
||||
{
|
||||
Document: ietfDoc,
|
||||
Pointer: "/c%d",
|
||||
ExpectedRawValue: "2",
|
||||
},
|
||||
{
|
||||
Document: ietfDoc,
|
||||
Pointer: "/e^f",
|
||||
ExpectedRawValue: "3",
|
||||
},
|
||||
{
|
||||
Document: ietfDoc,
|
||||
Pointer: "/g|h",
|
||||
ExpectedRawValue: "4",
|
||||
},
|
||||
{
|
||||
Document: ietfDoc,
|
||||
Pointer: "/i\\j",
|
||||
ExpectedRawValue: "5",
|
||||
},
|
||||
{
|
||||
Document: ietfDoc,
|
||||
Pointer: "/k\"l",
|
||||
ExpectedRawValue: "6",
|
||||
},
|
||||
{
|
||||
Document: ietfDoc,
|
||||
Pointer: "/ ",
|
||||
ExpectedRawValue: "7",
|
||||
},
|
||||
{
|
||||
Document: ietfDoc,
|
||||
Pointer: "/m~0n",
|
||||
ExpectedRawValue: "8",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
func(index int, tc pointerGetTestCase) {
|
||||
t.Run(fmt.Sprintf("#%d: '%s'", i, tc.Pointer), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pointer := New(tc.Pointer)
|
||||
|
||||
value, err := pointer.Get(tc.Document)
|
||||
if err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
rawValue, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
var expectedValue interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(tc.ExpectedRawValue), &expectedValue); err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectedValue, value) {
|
||||
t.Errorf("Pointer '%s': expected value '%s', got '%s'", tc.Pointer, tc.ExpectedRawValue, rawValue)
|
||||
}
|
||||
})
|
||||
}(i, tc)
|
||||
}
|
||||
}
|
92
internal/jsonpointer/pointer.go
Normal file
92
internal/jsonpointer/pointer.go
Normal file
@ -0,0 +1,92 @@
|
||||
package jsonpointer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenSeparator = "/"
|
||||
NonExistentMemberToken = "-"
|
||||
)
|
||||
|
||||
type Pointer struct {
|
||||
tokens []string
|
||||
}
|
||||
|
||||
func (p *Pointer) Get(doc interface{}) (interface{}, error) {
|
||||
value, err := get(doc, p.tokens)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (p *Pointer) Set(doc interface{}, value interface{}) (interface{}, error) {
|
||||
doc, err := set(doc, p.tokens, value)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func (p *Pointer) Force(doc interface{}, value interface{}) (interface{}, error) {
|
||||
doc, err := force(doc, p.tokens, value)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func (p *Pointer) Delete(doc interface{}) (interface{}, error) {
|
||||
doc, err := del(doc, p.tokens)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func New(raw string) *Pointer {
|
||||
tokens := decodeTokens(raw)
|
||||
|
||||
return &Pointer{tokens}
|
||||
}
|
||||
|
||||
func tokensToString(tokens []string) string {
|
||||
escapedTokens := make([]string, 0)
|
||||
|
||||
for _, t := range tokens {
|
||||
escapedTokens = append(escapedTokens, escapeToken(t))
|
||||
}
|
||||
|
||||
return TokenSeparator + strings.Join(escapedTokens, TokenSeparator)
|
||||
}
|
||||
|
||||
func escapeToken(token string) string {
|
||||
token = strings.ReplaceAll(token, "/", "~1")
|
||||
token = strings.ReplaceAll(token, "~", "~0")
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func unescapeToken(token string) string {
|
||||
token = strings.ReplaceAll(token, "~1", "/")
|
||||
token = strings.ReplaceAll(token, "~0", "~")
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func decodeTokens(raw string) []string {
|
||||
tokens := strings.Split(raw, TokenSeparator)
|
||||
|
||||
for i, t := range tokens {
|
||||
tokens[i] = unescapeToken(t)
|
||||
}
|
||||
|
||||
return tokens[1:]
|
||||
}
|
67
internal/jsonpointer/set.go
Normal file
67
internal/jsonpointer/set.go
Normal file
@ -0,0 +1,67 @@
|
||||
package jsonpointer
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func set(doc interface{}, tokens []string, value interface{}) (interface{}, error) {
|
||||
if len(tokens) == 0 {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
currentToken := tokens[0]
|
||||
|
||||
switch typedDoc := doc.(type) {
|
||||
case map[string]interface{}:
|
||||
nestedDoc, exists := typedDoc[currentToken]
|
||||
if !exists {
|
||||
return nil, errors.Wrapf(ErrNotFound, "pointer '%s' not found on document", tokensToString(tokens))
|
||||
}
|
||||
|
||||
nestedDoc, err := set(nestedDoc, tokens[1:], value)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
typedDoc[currentToken] = nestedDoc
|
||||
|
||||
return typedDoc, nil
|
||||
|
||||
case []interface{}:
|
||||
var (
|
||||
index uint64
|
||||
nestedDoc interface{}
|
||||
err error
|
||||
)
|
||||
|
||||
if currentToken == NonExistentMemberToken {
|
||||
typedDoc = append(typedDoc, value)
|
||||
index = uint64(len(typedDoc) - 1)
|
||||
} else {
|
||||
index, err = strconv.ParseUint(currentToken, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(typedDoc) <= int(index) {
|
||||
return nil, errors.WithStack(ErrOutOfBounds)
|
||||
}
|
||||
|
||||
nestedDoc = typedDoc[index]
|
||||
}
|
||||
|
||||
nestedDoc, err = set(nestedDoc, tokens[1:], value)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
typedDoc[index] = nestedDoc
|
||||
|
||||
return typedDoc, nil
|
||||
|
||||
default:
|
||||
return nil, errors.Wrapf(ErrUnexpectedType, "unexpected type '%T'", typedDoc)
|
||||
}
|
||||
}
|
102
internal/jsonpointer/set_test.go
Normal file
102
internal/jsonpointer/set_test.go
Normal file
@ -0,0 +1,102 @@
|
||||
package jsonpointer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type pointerSetTestCase struct {
|
||||
DocPath string
|
||||
Pointer string
|
||||
Value interface{}
|
||||
ExpectedRawDocument string
|
||||
}
|
||||
|
||||
func TestPointerSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []pointerSetTestCase{
|
||||
{
|
||||
DocPath: "./testdata/set/basic.json",
|
||||
Pointer: "/foo",
|
||||
Value: "bar",
|
||||
ExpectedRawDocument: `{"foo":"bar"}`,
|
||||
},
|
||||
{
|
||||
DocPath: "./testdata/set/nested.json",
|
||||
Pointer: "/nestedObject/foo/1",
|
||||
Value: "test",
|
||||
ExpectedRawDocument: `
|
||||
{
|
||||
"nestedObject": {
|
||||
"foo": [
|
||||
"bar",
|
||||
"test"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
DocPath: "./testdata/set/nested.json",
|
||||
Pointer: "/nestedObject/foo/-",
|
||||
Value: "baz",
|
||||
ExpectedRawDocument: `
|
||||
{
|
||||
"nestedObject": {
|
||||
"foo": [
|
||||
"bar",
|
||||
0,
|
||||
"baz"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
func(index int, tc pointerSetTestCase) {
|
||||
t.Run(fmt.Sprintf("#%d: '%s'", i, tc.Pointer), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
baseRawDocument, err := ioutil.ReadFile(tc.DocPath)
|
||||
if err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
var baseDoc interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(baseRawDocument), &baseDoc); err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
pointer := New(tc.Pointer)
|
||||
|
||||
updatedDoc, err := pointer.Set(baseDoc, tc.Value)
|
||||
if err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
rawDoc, err := json.MarshalIndent(updatedDoc, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
var expectedDoc interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(tc.ExpectedRawDocument), &expectedDoc); err != nil {
|
||||
t.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectedDoc, updatedDoc) {
|
||||
t.Errorf("Set pointer '%s' -> '%v': expected document '%s', got '%s'", tc.Pointer, tc.Value, strings.TrimSpace(tc.ExpectedRawDocument), rawDoc)
|
||||
}
|
||||
})
|
||||
}(i, tc)
|
||||
}
|
||||
}
|
12
internal/jsonpointer/testdata/ietf.json
vendored
Normal file
12
internal/jsonpointer/testdata/ietf.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"foo": ["bar", "baz"],
|
||||
"": 0,
|
||||
"a/b": 1,
|
||||
"c%d": 2,
|
||||
"e^f": 3,
|
||||
"g|h": 4,
|
||||
"i\\j": 5,
|
||||
"k\"l": 6,
|
||||
" ": 7,
|
||||
"m~n": 8
|
||||
}
|
1
internal/jsonpointer/testdata/set/basic.json
vendored
Normal file
1
internal/jsonpointer/testdata/set/basic.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"foo":null}
|
8
internal/jsonpointer/testdata/set/nested.json
vendored
Normal file
8
internal/jsonpointer/testdata/set/nested.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"nestedObject": {
|
||||
"foo": [
|
||||
"bar",
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
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
|
||||
}
|
106
misc/schema/card.json
Normal file
106
misc/schema/card.json
Normal file
@ -0,0 +1,106 @@
|
||||
{
|
||||
"$id": "https://example.com/card.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "A representation of a person, company, organization, or place",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"familyName",
|
||||
"givenName"
|
||||
],
|
||||
"properties": {
|
||||
"fn": {
|
||||
"description": "Formatted Name",
|
||||
"type": "string"
|
||||
},
|
||||
"familyName": {
|
||||
"type": "string"
|
||||
},
|
||||
"givenName": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalName": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"honorificPrefix": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"honorificSuffix": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"nickname": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"adr": {
|
||||
"$ref": "https://json-schema.org/learn/examples/address.schema.json"
|
||||
},
|
||||
"geo": {
|
||||
"$ref": "https://json-schema.org/learn/examples/geographical-location.schema.json"
|
||||
},
|
||||
"tz": {
|
||||
"type": "string"
|
||||
},
|
||||
"photo": {
|
||||
"type": "string"
|
||||
},
|
||||
"logo": {
|
||||
"type": "string"
|
||||
},
|
||||
"sound": {
|
||||
"type": "string"
|
||||
},
|
||||
"bday": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"org": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"organizationName": {
|
||||
"type": "string"
|
||||
},
|
||||
"organizationUnit": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
136
misc/schema/filesystem.json
Normal file
136
misc/schema/filesystem.json
Normal file
@ -0,0 +1,136 @@
|
||||
{
|
||||
"$id": "https://example.com/entry-schema",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "JSON Schema for an fstab entry",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"storage"
|
||||
],
|
||||
"properties": {
|
||||
"storage": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/$defs/diskDevice"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/diskUUID"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/nfs"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/tmpfs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fstype": {
|
||||
"enum": [
|
||||
"ext3",
|
||||
"ext4",
|
||||
"btrfs"
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"readonly": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"diskDevice": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disk"
|
||||
]
|
||||
},
|
||||
"device": {
|
||||
"type": "string",
|
||||
"pattern": "^/dev/[^/]+(/[^/]+)*$"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"device"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"diskUUID": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disk"
|
||||
]
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"label"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"nfs": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"nfs"
|
||||
]
|
||||
},
|
||||
"remotePath": {
|
||||
"type": "string",
|
||||
"pattern": "^(/[^/]+)+$"
|
||||
},
|
||||
"server": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"format": "hostname"
|
||||
},
|
||||
{
|
||||
"format": "ipv4"
|
||||
},
|
||||
{
|
||||
"format": "ipv6"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"server",
|
||||
"remotePath"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"tmpfs": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"tmpfs"
|
||||
]
|
||||
},
|
||||
"sizeInMB": {
|
||||
"type": "integer",
|
||||
"minimum": 16,
|
||||
"maximum": 512
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"sizeInMB"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
12
modd.conf
Normal file
12
modd.conf
Normal file
@ -0,0 +1,12 @@
|
||||
**/*.go
|
||||
misc/pipeline/dist/*.js
|
||||
misc/schemas/*.json
|
||||
**/testdata/**/*
|
||||
internal/server/template/blocks/*.tmpl
|
||||
internal/server/template/layouts/*.tmpl
|
||||
modd.conf
|
||||
.env {
|
||||
prep: make build-frmd
|
||||
prep: [ -e .env ] || ( cp .env.dist .env )
|
||||
prep: make test
|
||||
}
|
39
schema.json
Normal file
39
schema.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"$id": "https://example.com/custom.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "Test",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"foo"
|
||||
],
|
||||
"properties": {
|
||||
"foo": {
|
||||
"description": "Ça fait des trucs",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bar": {
|
||||
"type": "string",
|
||||
"minLength": 5
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"myItems": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stringProp": {
|
||||
"type": "string",
|
||||
"minLength": 10
|
||||
},
|
||||
"numericProp": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
104
script/release
Executable file
104
script/release
Executable file
@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xeo pipefail
|
||||
|
||||
OS_TARGETS=(linux)
|
||||
ARCH_TARGETS=${ARCH_TARGETS:-amd64}
|
||||
ARM_TARGETS=${ARM_TARGETS:-5 6 7}
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
||||
PROJECT_DIR="$DIR/../.."
|
||||
|
||||
function build {
|
||||
local name=$1
|
||||
local srcdir=$2
|
||||
local os=$3
|
||||
local arch=$4
|
||||
|
||||
local dirname="$name-$os-$arch$GOARM"
|
||||
local destdir="$PROJECT_DIR/release/$dirname"
|
||||
|
||||
rm -rf "$destdir"
|
||||
mkdir -p "$destdir"
|
||||
|
||||
echo "building $dirname..."
|
||||
|
||||
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build \
|
||||
-ldflags="-s -w -X 'main.GitRef=$(current_commit_ref)' -X 'main.ProjectVersion=$(current_version)' -X 'main.BuildDate=$(current_date)'" \
|
||||
-gcflags=-trimpath="${PWD}" \
|
||||
-asmflags=-trimpath="${PWD}" \
|
||||
-o "$destdir/bin/$name" \
|
||||
"$srcdir"
|
||||
|
||||
if [ ! -z "$(which upx)" ]; then
|
||||
upx --brute -9 "$destdir/bin/$name"
|
||||
fi
|
||||
}
|
||||
|
||||
function current_date {
|
||||
date '+%Y-%m-%d %H:%M'
|
||||
}
|
||||
|
||||
function current_commit_ref {
|
||||
git log -n 1 --pretty="format:%h"
|
||||
}
|
||||
|
||||
function current_version {
|
||||
local latest_tag=$(git describe --abbrev=0 | rev | cut -d '/' -f 1 | rev)
|
||||
echo ${latest_tag:-0.0.0}
|
||||
}
|
||||
|
||||
function copy {
|
||||
local name=$1
|
||||
local os=$2
|
||||
local arch=$3
|
||||
local src=$4
|
||||
local dest=$5
|
||||
|
||||
local dirname="$name-$os-$arch$GOARM"
|
||||
local destdir="$PROJECT_DIR/release/$dirname"
|
||||
|
||||
echo "copying '$src' to '$destdir/$dest'..."
|
||||
|
||||
mkdir -p "$(dirname $destdir/$dest)"
|
||||
|
||||
cp -rfL $src "$destdir/$dest"
|
||||
}
|
||||
|
||||
function compress {
|
||||
local name=$1
|
||||
local os=$2
|
||||
local arch=$3
|
||||
|
||||
local dirname="$name-$os-$arch$GOARM"
|
||||
local destdir="$PROJECT_DIR/release/$dirname"
|
||||
|
||||
echo "compressing $dirname..."
|
||||
tar -czf "$destdir.tar.gz" -C "$destdir/../" "$dirname"
|
||||
}
|
||||
|
||||
function release_frmd {
|
||||
local os=$1
|
||||
local arch=$2
|
||||
|
||||
build 'frmd' "$PROJECT_DIR/cmd/frmd" $os $arch
|
||||
copy 'frmd' $os $arch "$PROJECT_DIR/README.md" "README.md"
|
||||
|
||||
compress 'frmd' $os $arch
|
||||
}
|
||||
|
||||
function main {
|
||||
for os in ${OS_TARGETS[@]}; do
|
||||
for arch in ${ARCH_TARGETS[@]}; do
|
||||
if [ "$arch" == "arm" ]; then
|
||||
for arm_target in $ARM_TARGETS; do
|
||||
GOARM=$arm_target release_frmd $os $arch
|
||||
done
|
||||
else
|
||||
release_frmd $os $arch
|
||||
fi
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
main
|
6
values.json
Normal file
6
values.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"foo": {
|
||||
"bar": "toto",
|
||||
"enabled": true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user