feat: values updaters
This commit is contained in:
parent
08476e5346
commit
0e7e955a58
54
README.md
54
README.md
|
@ -30,17 +30,59 @@ It will download `frmd` to your current directory.
|
||||||
|
|
||||||
Formidable uses URLs to define how to handle schemas/defaults/values.
|
Formidable uses URLs to define how to handle schemas/defaults/values.
|
||||||
|
|
||||||
For example, to edit a web available schema (in YAML), defaults from `stdin` (in JSON) and values from the local file system (in HCL):
|
For example, to edit a schema (in YAML) from an HTTPS server, while readig default values from `stdin` (in JSON) and using effective values from the local file system (in HCL), outputing updates to `stdout`:
|
||||||
|
|
||||||
```shell
|
```bash
|
||||||
echo '{}' | frmd \
|
echo '{}' | frmd \
|
||||||
edit
|
edit
|
||||||
--schema https://example.com/my-schema.yml \
|
--schema 'https://example.com/my-schema.yml' \
|
||||||
--defaults stdin://local?format=json \
|
--defaults 'stdin://local?format=json' \
|
||||||
--values file:///my/file/absolute/path.hcl
|
--values 'file:///my/file/absolute/path.hcl' \
|
||||||
|
--output 'stdout://local?format=json'
|
||||||
```
|
```
|
||||||
|
|
||||||
The `?format=<json|yaml|hcl>` query variable allows to specify the file format when no file extension is available (for example when reading from `stdin`).
|
### Available loaders
|
||||||
|
|
||||||
|
#### `stdin://`
|
||||||
|
|
||||||
|
> TODO: Write doc + example
|
||||||
|
#### `http://` and `https://`
|
||||||
|
|
||||||
|
> TODO: Write doc + example
|
||||||
|
#### `file://`
|
||||||
|
|
||||||
|
> TODO: Write doc + example
|
||||||
|
|
||||||
|
### Available formats
|
||||||
|
|
||||||
|
#### JSON
|
||||||
|
|
||||||
|
- URL Query: `?format=json`
|
||||||
|
- File extension: `.json`
|
||||||
|
|
||||||
|
#### YAML
|
||||||
|
|
||||||
|
- URL Query: `?format=yaml`
|
||||||
|
- File extension: `.yaml` or `.yml`
|
||||||
|
|
||||||
|
#### HCL
|
||||||
|
|
||||||
|
- URL Query: `?format=hcl`
|
||||||
|
- File extension: `.hcl`
|
||||||
|
|
||||||
|
### Available outputs
|
||||||
|
|
||||||
|
#### `stdout://` (default)
|
||||||
|
|
||||||
|
> TODO: Write doc + example
|
||||||
|
|
||||||
|
#### `file://`
|
||||||
|
|
||||||
|
> TODO: Write doc + example
|
||||||
|
|
||||||
|
#### `exec://`
|
||||||
|
|
||||||
|
> TODO: Write doc + example
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
|
|
||||||
encjson "encoding/json"
|
encjson "encoding/json"
|
||||||
|
|
||||||
|
@ -15,6 +14,9 @@ import (
|
||||||
"forge.cadoles.com/wpetit/formidable/internal/data/scheme/file"
|
"forge.cadoles.com/wpetit/formidable/internal/data/scheme/file"
|
||||||
"forge.cadoles.com/wpetit/formidable/internal/data/scheme/http"
|
"forge.cadoles.com/wpetit/formidable/internal/data/scheme/http"
|
||||||
"forge.cadoles.com/wpetit/formidable/internal/data/scheme/stdin"
|
"forge.cadoles.com/wpetit/formidable/internal/data/scheme/stdin"
|
||||||
|
"forge.cadoles.com/wpetit/formidable/internal/data/updater/exec"
|
||||||
|
fileUpdater "forge.cadoles.com/wpetit/formidable/internal/data/updater/file"
|
||||||
|
"forge.cadoles.com/wpetit/formidable/internal/data/updater/stdout"
|
||||||
"forge.cadoles.com/wpetit/formidable/internal/def"
|
"forge.cadoles.com/wpetit/formidable/internal/def"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||||
|
@ -50,8 +52,8 @@ func commonFlags() []cli.Flag {
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "output",
|
Name: "output",
|
||||||
Aliases: []string{"o", "out"},
|
Aliases: []string{"o", "out"},
|
||||||
Value: "-",
|
Value: "stdout://local?format=json",
|
||||||
Usage: "Output modified values to `output_file` (or '-' for stdout, the default)",
|
Usage: "Output modified values to specified URL",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,29 +149,39 @@ func loadSchema(ctx *cli.Context) (*jsonschema.Schema, error) {
|
||||||
return schema, nil
|
return schema, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const OutputStdout = "-"
|
func outputValues(ctx *cli.Context, values interface{}) error {
|
||||||
|
outputFlag := ctx.String("output")
|
||||||
|
|
||||||
type noopWriteCloser struct {
|
url, err := url.Parse(outputFlag)
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return file, nil
|
encoder := newEncoder()
|
||||||
|
|
||||||
|
reader, err := encoder.Encode(url, values)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updater := newUpdater()
|
||||||
|
|
||||||
|
writer, err := updater.Update(url)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err := io.Copy(writer, reader); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLoader() *data.Loader {
|
func newLoader() *data.Loader {
|
||||||
|
@ -187,3 +199,18 @@ func newDecoder() *data.Decoder {
|
||||||
yaml.NewDecoderHandler(),
|
yaml.NewDecoderHandler(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newUpdater() *data.Updater {
|
||||||
|
return data.NewUpdater(
|
||||||
|
stdout.NewUpdaterHandler(),
|
||||||
|
fileUpdater.NewUpdaterHandler(),
|
||||||
|
exec.NewUpdaterHandler(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEncoder() *data.Encoder {
|
||||||
|
return data.NewEncoder(
|
||||||
|
json.NewEncoderHandler(),
|
||||||
|
yaml.NewEncoderHandler(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
@ -49,17 +48,8 @@ func Delete() *cli.Command {
|
||||||
return errors.Wrap(err, "could not validate resulting json")
|
return errors.Wrap(err, "could not validate resulting json")
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := outputWriter(ctx)
|
if err := outputValues(ctx, updatedValues); err != nil {
|
||||||
if err != nil {
|
return errors.Wrap(err, "could not output updated values")
|
||||||
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
|
return nil
|
||||||
|
|
|
@ -52,6 +52,13 @@ func Edit() *cli.Command {
|
||||||
server.WithSchema(schema),
|
server.WithSchema(schema),
|
||||||
server.WithValues(values),
|
server.WithValues(values),
|
||||||
server.WithDefaults(defaults),
|
server.WithDefaults(defaults),
|
||||||
|
server.WithOnUpdate(func(values interface{}) error {
|
||||||
|
if err := outputValues(ctx, values); err != nil {
|
||||||
|
return errors.Wrap(err, "could not output updated values")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
addrs, srvErrs := srv.Start(srvCtx)
|
addrs, srvErrs := srv.Start(srvCtx)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
@ -50,17 +49,8 @@ func Get() *cli.Command {
|
||||||
return errors.Wrapf(err, "could not get value from pointer '%v'", rawPointer)
|
return errors.Wrapf(err, "could not get value from pointer '%v'", rawPointer)
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := outputWriter(ctx)
|
if err := outputValues(ctx, value); err != nil {
|
||||||
if err != nil {
|
return errors.Wrap(err, "could not output updated values")
|
||||||
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
|
return nil
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
@ -44,23 +43,17 @@ func Set() *cli.Command {
|
||||||
|
|
||||||
pointer := jsonpointer.New(rawPointer)
|
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{}
|
var updatedValues interface{}
|
||||||
|
|
||||||
force := ctx.Bool("force")
|
force := ctx.Bool("force")
|
||||||
|
|
||||||
if force {
|
if force {
|
||||||
updatedValues, err = pointer.Force(values, value)
|
updatedValues, err = pointer.Force(values, rawValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "could not force value '%v' to pointer '%v'", rawValue, rawPointer)
|
return errors.Wrapf(err, "could not force value '%v' to pointer '%v'", rawValue, rawPointer)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updatedValues, err = pointer.Set(values, value)
|
updatedValues, err = pointer.Set(values, rawValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "could not set value '%v' to pointer '%v'", rawValue, rawPointer)
|
return errors.Wrapf(err, "could not set value '%v' to pointer '%v'", rawValue, rawPointer)
|
||||||
}
|
}
|
||||||
|
@ -76,17 +69,8 @@ func Set() *cli.Command {
|
||||||
return errors.Wrap(err, "could not validate resulting json")
|
return errors.Wrap(err, "could not validate resulting json")
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := outputWriter(ctx)
|
if err := outputValues(ctx, updatedValues); err != nil {
|
||||||
if err != nil {
|
return errors.Wrap(err, "could not output updated values")
|
||||||
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
|
return nil
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/formidable/internal/data/format"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EncoderHandler struct{}
|
||||||
|
|
||||||
|
func (d *EncoderHandler) Match(url *url.URL) bool {
|
||||||
|
ext := filepath.Ext(path.Join(url.Host, url.Path))
|
||||||
|
|
||||||
|
return ext == ExtensionJSON ||
|
||||||
|
format.MatchURLQueryFormat(url, FormatJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *EncoderHandler) Encode(url *url.URL, data interface{}) (io.Reader, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(&buf)
|
||||||
|
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
|
||||||
|
if err := encoder.Encode(data); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEncoderHandler() *EncoderHandler {
|
||||||
|
return &EncoderHandler{}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"forge.cadoles.com/wpetit/formidable/internal/data/format"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EncoderHandler struct{}
|
||||||
|
|
||||||
|
func (d *EncoderHandler) Match(url *url.URL) bool {
|
||||||
|
ext := filepath.Ext(path.Join(url.Host, url.Path))
|
||||||
|
|
||||||
|
return ExtensionYAML.MatchString(ext) ||
|
||||||
|
format.MatchURLQueryFormat(url, FormatYAML)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *EncoderHandler) Encode(url *url.URL, data interface{}) (io.Reader, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
encoder := yaml.NewEncoder(&buf)
|
||||||
|
|
||||||
|
if err := encoder.Encode(data); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEncoderHandler() *EncoderHandler {
|
||||||
|
return &EncoderHandler{}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdaterHandler interface {
|
||||||
|
URLMatcher
|
||||||
|
Update(url *url.URL) (io.WriteCloser, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Updater struct {
|
||||||
|
handlers []UpdaterHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Updater) Update(url *url.URL) (io.WriteCloser, error) {
|
||||||
|
for _, h := range u.handlers {
|
||||||
|
if !h.Match(url) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wr, err := h.Update(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Wrapf(ErrHandlerNotFound, "could not find matching handler for url '%s'", url.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdater(handlers ...UpdaterHandler) *Updater {
|
||||||
|
return &Updater{handlers}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const SchemeExec = "exec"
|
||||||
|
|
||||||
|
type UpdaterHandler struct{}
|
||||||
|
|
||||||
|
func (h *UpdaterHandler) Match(url *url.URL) bool {
|
||||||
|
return url.Scheme == SchemeExec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UpdaterHandler) Update(url *url.URL) (io.WriteCloser, error) {
|
||||||
|
path := filepath.Join(url.Host, url.Path)
|
||||||
|
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(absPath)
|
||||||
|
|
||||||
|
if url.Query().Get("env") == "yes" {
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
}
|
||||||
|
|
||||||
|
if url.Query().Get("stdout") == "yes" {
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
if url.Query().Get("stderr") == "yes" {
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
writer, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdaterHandler() *UpdaterHandler {
|
||||||
|
return &UpdaterHandler{}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const SchemeFile = "file"
|
||||||
|
|
||||||
|
type UpdaterHandler struct{}
|
||||||
|
|
||||||
|
func (h *UpdaterHandler) Match(url *url.URL) bool {
|
||||||
|
return url.Scheme == SchemeFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UpdaterHandler) Update(url *url.URL) (io.WriteCloser, error) {
|
||||||
|
name := filepath.Join(url.Host, url.Path)
|
||||||
|
|
||||||
|
file, err := os.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not open file '%s'", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdaterHandler() *UpdaterHandler {
|
||||||
|
return &UpdaterHandler{}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package stdout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const SchemeStdout = "stdout"
|
||||||
|
|
||||||
|
type UpdaterHandler struct{}
|
||||||
|
|
||||||
|
func (h *UpdaterHandler) Match(url *url.URL) bool {
|
||||||
|
return url.Scheme == SchemeStdout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UpdaterHandler) Update(url *url.URL) (io.WriteCloser, error) {
|
||||||
|
return os.Stdout, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdaterHandler() *UpdaterHandler {
|
||||||
|
return &UpdaterHandler{}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ type Option struct {
|
||||||
Schema *jsonschema.Schema
|
Schema *jsonschema.Schema
|
||||||
Values interface{}
|
Values interface{}
|
||||||
Defaults interface{}
|
Defaults interface{}
|
||||||
|
OnUpdate OnUpdateFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionFunc func(*Option)
|
type OptionFunc func(*Option)
|
||||||
|
@ -47,3 +48,9 @@ func WithDefaults(defaults interface{}) OptionFunc {
|
||||||
opt.Defaults = defaults
|
opt.Defaults = defaults
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithOnUpdate(onUpdate OnUpdateFunc) OptionFunc {
|
||||||
|
return func(opt *Option) {
|
||||||
|
opt.OnUpdate = onUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package route
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -12,17 +12,16 @@ import (
|
||||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createRenderFormHandlerFunc(schema *jsonschema.Schema, defaults, values interface{}) http.HandlerFunc {
|
func (s *Server) serveFormReq(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
data := &template.FormItemData{
|
data := &template.FormItemData{
|
||||||
Parent: nil,
|
Parent: nil,
|
||||||
Schema: schema,
|
Schema: s.schema,
|
||||||
Property: "",
|
Property: "",
|
||||||
Defaults: defaults,
|
Defaults: s.defaults,
|
||||||
Values: values,
|
Values: s.values,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := schema.Validate(data.Values); err != nil {
|
if err := s.schema.Validate(data.Values); err != nil {
|
||||||
validationErr, ok := err.(*jsonschema.ValidationError)
|
validationErr, ok := err.(*jsonschema.ValidationError)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(errors.Wrap(err, "could not validate values"))
|
panic(errors.Wrap(err, "could not validate values"))
|
||||||
|
@ -34,23 +33,23 @@ func createRenderFormHandlerFunc(schema *jsonschema.Schema, defaults, values int
|
||||||
if err := template.Exec("index.html.tmpl", w, data); err != nil {
|
if err := template.Exec("index.html.tmpl", w, data); err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createHandleFormHandlerFunc(schema *jsonschema.Schema, defaults, values interface{}) http.HandlerFunc {
|
func (s *Server) handleFormReq(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
data := &template.FormItemData{
|
data := &template.FormItemData{
|
||||||
Parent: nil,
|
Parent: nil,
|
||||||
Schema: schema,
|
Schema: s.schema,
|
||||||
Property: "",
|
Property: "",
|
||||||
Defaults: defaults,
|
Defaults: s.defaults,
|
||||||
Values: values,
|
Values: s.values,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var values interface{}
|
||||||
|
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
} else {
|
} else {
|
||||||
values, err = handleForm(r.Form, schema, values)
|
values, err = handleForm(r.Form, s.schema, values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
@ -58,7 +57,7 @@ func createHandleFormHandlerFunc(schema *jsonschema.Schema, defaults, values int
|
||||||
data.Values = values
|
data.Values = values
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := schema.Validate(data.Values); err != nil {
|
if err := s.schema.Validate(data.Values); err != nil {
|
||||||
validationErr, ok := err.(*jsonschema.ValidationError)
|
validationErr, ok := err.(*jsonschema.ValidationError)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(errors.Wrap(err, "could not validate values"))
|
panic(errors.Wrap(err, "could not validate values"))
|
||||||
|
@ -68,13 +67,18 @@ func createHandleFormHandlerFunc(schema *jsonschema.Schema, defaults, values int
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Error == nil {
|
if data.Error == nil {
|
||||||
|
if s.onUpdate != nil {
|
||||||
|
if err := s.onUpdate(data.Values); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not update values"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data.SuccessMessage = "Data updated."
|
data.SuccessMessage = "Data updated."
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := template.Exec("index.html.tmpl", w, data); err != nil {
|
if err := template.Exec("index.html.tmpl", w, data); err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleForm(form url.Values, schema *jsonschema.Schema, values interface{}) (interface{}, error) {
|
func handleForm(form url.Values, schema *jsonschema.Schema, values interface{}) (interface{}, error) {
|
|
@ -1,23 +0,0 @@
|
||||||
package route
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"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{}, assetsHandler http.Handler) (*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))
|
|
||||||
|
|
||||||
router.Handle("/assets/*", assetsHandler)
|
|
||||||
|
|
||||||
return router, nil
|
|
||||||
}
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"forge.cadoles.com/wpetit/formidable/internal/server/route"
|
|
||||||
"forge.cadoles.com/wpetit/formidable/internal/server/template"
|
"forge.cadoles.com/wpetit/formidable/internal/server/template"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||||
)
|
)
|
||||||
|
@ -19,8 +19,11 @@ type Server struct {
|
||||||
schema *jsonschema.Schema
|
schema *jsonschema.Schema
|
||||||
defaults interface{}
|
defaults interface{}
|
||||||
values interface{}
|
values interface{}
|
||||||
|
onUpdate OnUpdateFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OnUpdateFunc func(values interface{}) error
|
||||||
|
|
||||||
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
addrs := make(chan net.Addr)
|
addrs := make(chan net.Addr)
|
||||||
|
@ -71,16 +74,15 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||||
assets := getEmbeddedAssets()
|
assets := getEmbeddedAssets()
|
||||||
assetsHandler := http.FileServer(http.FS(assets))
|
assetsHandler := http.FileServer(http.FS(assets))
|
||||||
|
|
||||||
handler, err := route.NewHandler(s.schema, s.defaults, s.values, assetsHandler)
|
router := chi.NewRouter()
|
||||||
if err != nil {
|
|
||||||
errs <- errors.WithStack(err)
|
|
||||||
|
|
||||||
return
|
router.Get("/", s.serveFormReq)
|
||||||
}
|
router.Post("/", s.handleFormReq)
|
||||||
|
router.Handle("/assets/*", assetsHandler)
|
||||||
|
|
||||||
log.Println("http server listening")
|
log.Println("http server listening")
|
||||||
|
|
||||||
if err := http.Serve(listener, handler); err != nil && !errors.Is(err, net.ErrClosed) {
|
if err := http.Serve(listener, router); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||||
errs <- errors.WithStack(err)
|
errs <- errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,5 +101,6 @@ func New(funcs ...OptionFunc) *Server {
|
||||||
schema: opt.Schema,
|
schema: opt.Schema,
|
||||||
defaults: opt.Defaults,
|
defaults: opt.Defaults,
|
||||||
values: opt.Values,
|
values: opt.Values,
|
||||||
|
onUpdate: opt.OnUpdate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue