refactor(10.2-01): nest framework packages under modules
- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
This commit is contained in:
632
modules/cabana/form_schema.go
Normal file
632
modules/cabana/form_schema.go
Normal file
@@ -0,0 +1,632 @@
|
||||
package cabana
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"context"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/phrasebook"
|
||||
"git.golem15.com/golem15/summercms/modules/towel"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/goccy/go-yaml/ast"
|
||||
)
|
||||
|
||||
var (
|
||||
formFieldTypes = map[string]struct{}{
|
||||
"text": {}, "textarea": {}, "number": {}, "checkbox": {},
|
||||
"switch": {}, "dropdown": {}, "relation": {}, "relation-manager": {},
|
||||
}
|
||||
formSpans = map[string]struct{}{
|
||||
"left": {}, "right": {}, "full": {}, "auto": {}, "row": {},
|
||||
}
|
||||
formSizes = map[string]struct{}{
|
||||
"tiny": {}, "small": {}, "large": {}, "huge": {}, "giant": {},
|
||||
}
|
||||
formFieldKeys = map[string]struct{}{
|
||||
"label": {}, "comment": {}, "span": {}, "type": {}, "required": {},
|
||||
"tab": {}, "context": {}, "attributes": {}, "size": {}, "default": {},
|
||||
"nameFrom": {}, "emptyOption": {}, "options": {}, "relation": {},
|
||||
}
|
||||
)
|
||||
|
||||
type formConfigDocument struct {
|
||||
Name string `yaml:"name"`
|
||||
Form string `yaml:"form"`
|
||||
ModelClass string `yaml:"modelClass"`
|
||||
DefaultRedirect string `yaml:"defaultRedirect"`
|
||||
Create *formRedirects `yaml:"create"`
|
||||
Update *formRedirects `yaml:"update"`
|
||||
Messages *formMessageKeys `yaml:"messages"`
|
||||
}
|
||||
|
||||
type formRedirects struct {
|
||||
Redirect string `yaml:"redirect"`
|
||||
RedirectClose string `yaml:"redirectClose"`
|
||||
}
|
||||
|
||||
type formFieldsFile struct {
|
||||
Fields fieldMap `yaml:"fields"`
|
||||
}
|
||||
|
||||
type fieldMap struct {
|
||||
items []FormField
|
||||
}
|
||||
|
||||
// CompileForm compiles config_form.yaml and the model fields.yaml it names.
|
||||
// The returned schema is safe to cache: labels stay as source keys.
|
||||
func CompileForm(pluginID string, ctl pact.AdminController, fsys fs.FS) (*FormSchema, error) {
|
||||
if ctl == nil {
|
||||
return nil, fmt.Errorf("cabana: admin schema %s: controller is nil", pluginID)
|
||||
}
|
||||
dir := strings.Trim(path.Clean(ctl.ConfigDir()), "/")
|
||||
if dir == "." || strings.HasPrefix(dir, "..") {
|
||||
return nil, bootErr(pluginID, ctl.ID(), ctl.ConfigDir(), fmt.Errorf("config directory escapes the plugin"))
|
||||
}
|
||||
cfgPath := dir + "/config_form.yaml"
|
||||
raw, err := readAsset(fsys, cfgPath)
|
||||
if err != nil {
|
||||
return nil, bootErr(pluginID, ctl.ID(), cfgPath, err)
|
||||
}
|
||||
var doc formConfigDocument
|
||||
if err := decodeStrict(raw, &doc); err != nil {
|
||||
return nil, bootErr(pluginID, ctl.ID(), cfgPath, err)
|
||||
}
|
||||
if doc.ModelClass != ctl.ModelName() {
|
||||
return nil, bootErr(pluginID, ctl.ID(), cfgPath, fmt.Errorf("modelClass %q does not match %q", doc.ModelClass, ctl.ModelName()))
|
||||
}
|
||||
if strings.TrimSpace(doc.Form) == "" {
|
||||
return nil, bootErr(pluginID, ctl.ID(), cfgPath, fmt.Errorf("form file is empty"))
|
||||
}
|
||||
fieldsPath, err := assetPath(pluginID, doc.Form)
|
||||
if err != nil {
|
||||
return nil, bootErr(pluginID, ctl.ID(), cfgPath, err)
|
||||
}
|
||||
fieldsRaw, err := readAsset(fsys, fieldsPath)
|
||||
if err != nil {
|
||||
return nil, bootErr(pluginID, ctl.ID(), fieldsPath, err)
|
||||
}
|
||||
fields, err := decodeFields(fieldsRaw)
|
||||
if err != nil {
|
||||
return nil, bootErr(pluginID, ctl.ID(), fieldsPath, err)
|
||||
}
|
||||
if fields == nil {
|
||||
fields = []FormField{}
|
||||
}
|
||||
if err := requireDropdownProvider(ctl, fields); err != nil {
|
||||
return nil, bootErr(pluginID, ctl.ID(), fieldsPath, err)
|
||||
}
|
||||
schema := &FormSchema{
|
||||
Name: doc.Name,
|
||||
ModelClass: doc.ModelClass,
|
||||
Fields: fields,
|
||||
redirects: FormRedirects{Default: doc.DefaultRedirect},
|
||||
}
|
||||
if doc.Messages != nil {
|
||||
schema.messageKeys = *doc.Messages
|
||||
}
|
||||
if doc.Create != nil {
|
||||
schema.redirects.Create = FormRedirect{Redirect: doc.Create.Redirect, RedirectClose: doc.Create.RedirectClose}
|
||||
}
|
||||
if doc.Update != nil {
|
||||
schema.redirects.Update = FormRedirect{Redirect: doc.Update.Redirect, RedirectClose: doc.Update.RedirectClose}
|
||||
}
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
// Localize copies the cached schema and resolves display strings for one request.
|
||||
// It does not mutate s. Option values keep the scalar captured at boot.
|
||||
func (s *FormSchema) Localize(ctx context.Context, tr *phrasebook.Translator, provider pact.DropdownOptionsProvider) (*FormView, error) {
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("cabana: nil form schema")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
locale := schemaLocale(ctx, tr)
|
||||
ctx = towel.WithLocale(ctx, locale)
|
||||
fields := make([]FormField, len(s.Fields))
|
||||
for i, src := range s.Fields {
|
||||
field := src
|
||||
field.Label = translateKey(ctx, tr, src.Label)
|
||||
field.Comment = translateKey(ctx, tr, src.Comment)
|
||||
field.Tab = translateKey(ctx, tr, src.Tab)
|
||||
field.EmptyOption = translateKey(ctx, tr, src.EmptyOption)
|
||||
options, err := localizeOptions(ctx, tr, src, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
field.Options = options
|
||||
fields[i] = field
|
||||
}
|
||||
if fields == nil {
|
||||
fields = []FormField{}
|
||||
}
|
||||
return &FormView{
|
||||
Name: translateKey(ctx, tr, s.Name),
|
||||
ModelClass: s.ModelClass,
|
||||
Fields: fields,
|
||||
Messages: localizeMessages[formMessageKeys, FormMessages](ctx, tr, s.formMessageKeySet()),
|
||||
Redirects: s.redirects,
|
||||
Meta: FormMeta{Locale: locale},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CanonicalJSON is one localized schema response.
|
||||
func (v *FormView) CanonicalJSON() ([]byte, error) {
|
||||
if v == nil {
|
||||
return nil, fmt.Errorf("cabana: nil form view")
|
||||
}
|
||||
out := *v
|
||||
if out.Fields == nil {
|
||||
out.Fields = []FormField{}
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// CanonicalJSON is the stable, locale-neutral form contract.
|
||||
func (s *FormSchema) CanonicalJSON() ([]byte, error) {
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("cabana: nil form schema")
|
||||
}
|
||||
out := *s
|
||||
if out.Fields == nil {
|
||||
out.Fields = []FormField{}
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
func requireDropdownProvider(ctl pact.AdminController, fields []FormField) error {
|
||||
var method string
|
||||
for _, field := range fields {
|
||||
if field.optionsMethod != "" {
|
||||
method = field.optionsMethod
|
||||
break
|
||||
}
|
||||
}
|
||||
if method == "" || dropdownProvider(ctl) != nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("dropdown method %s requires DropdownOptions", method)
|
||||
}
|
||||
|
||||
func dropdownProvider(ctl pact.AdminController) pact.DropdownOptionsProvider {
|
||||
if ctl == nil {
|
||||
return nil
|
||||
}
|
||||
if p, ok := ctl.(pact.DropdownOptionsProvider); ok && p != nil {
|
||||
return p
|
||||
}
|
||||
src, ok := ctl.(pact.AdminRecordSource)
|
||||
if !ok || src == nil {
|
||||
return nil
|
||||
}
|
||||
rec := src.NewRecord()
|
||||
p, ok := rec.(pact.DropdownOptionsProvider)
|
||||
if !ok || p == nil {
|
||||
return nil
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func schemaLocale(ctx context.Context, tr *phrasebook.Translator) string {
|
||||
if loc, ok := towel.Locale(ctx); ok {
|
||||
if primary := primaryLanguage(loc); primary != "" {
|
||||
return primary
|
||||
}
|
||||
}
|
||||
if tr != nil {
|
||||
if loc := strings.TrimSpace(tr.Locale()); loc != "" {
|
||||
return loc
|
||||
}
|
||||
}
|
||||
return "en"
|
||||
}
|
||||
|
||||
func primaryLanguage(header string) string {
|
||||
header = strings.TrimSpace(header)
|
||||
if header == "" {
|
||||
return ""
|
||||
}
|
||||
if i := strings.IndexAny(header, ",;"); i >= 0 {
|
||||
header = strings.TrimSpace(header[:i])
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
func translateKey(ctx context.Context, tr *phrasebook.Translator, key string) string {
|
||||
if key == "" || tr == nil {
|
||||
return key
|
||||
}
|
||||
return tr.Get(ctx, key, nil)
|
||||
}
|
||||
|
||||
func localizeOptions(ctx context.Context, tr *phrasebook.Translator, src FormField, provider pact.DropdownOptionsProvider) ([]FormOption, error) {
|
||||
if src.optionsMethod != "" {
|
||||
if provider == nil {
|
||||
return nil, fmt.Errorf("cabana: dropdown method %s requires DropdownOptions", src.optionsMethod)
|
||||
}
|
||||
provided := provider.DropdownOptions(src.Name)
|
||||
opts := make([]FormOption, len(provided))
|
||||
for i, opt := range provided {
|
||||
raw, err := json.Marshal(opt.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts[i] = FormOption{Value: jsonScalar{raw: raw}, Label: translateKey(ctx, tr, opt.Label)}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
if len(src.Options) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
opts := make([]FormOption, len(src.Options))
|
||||
for i, opt := range src.Options {
|
||||
opt.Label = translateKey(ctx, tr, opt.Label)
|
||||
opts[i] = opt
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func decodeFields(raw []byte) ([]FormField, error) {
|
||||
dec := yaml.NewDecoder(bytes.NewReader(raw), yaml.DisallowUnknownField())
|
||||
var doc formFieldsFile
|
||||
if err := dec.Decode(&doc); err != nil {
|
||||
return nil, normalizeYAMLError(err)
|
||||
}
|
||||
if doc.Fields.items == nil {
|
||||
return []FormField{}, nil
|
||||
}
|
||||
return doc.Fields.items, nil
|
||||
}
|
||||
|
||||
func (m *fieldMap) UnmarshalYAML(node ast.Node) error {
|
||||
node = unwrapNode(node)
|
||||
if _, ok := node.(*ast.NullNode); ok || node == nil {
|
||||
m.items = []FormField{}
|
||||
return nil
|
||||
}
|
||||
mapping, ok := node.(*ast.MappingNode)
|
||||
if !ok {
|
||||
return fmt.Errorf("fields must be a mapping")
|
||||
}
|
||||
items := make([]FormField, 0, len(mapping.Values))
|
||||
seen := map[string]struct{}{}
|
||||
for _, entry := range mapping.Values {
|
||||
name, err := nodeString(unwrapNode(entry.Key))
|
||||
if err != nil || !identifier(name) {
|
||||
return fmt.Errorf("field name %q is not an identifier", nodeText(entry.Key))
|
||||
}
|
||||
if _, dup := seen[name]; dup {
|
||||
return fmt.Errorf("duplicate field %s", name)
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
field, err := compileFieldNode(name, unwrapNode(entry.Value))
|
||||
if err != nil {
|
||||
return fmt.Errorf("field %s: %w", name, err)
|
||||
}
|
||||
items = append(items, field)
|
||||
}
|
||||
m.items = items
|
||||
return nil
|
||||
}
|
||||
|
||||
func compileFieldNode(name string, node ast.Node) (FormField, error) {
|
||||
mapping, ok := node.(*ast.MappingNode)
|
||||
if !ok {
|
||||
return FormField{}, fmt.Errorf("must be a mapping")
|
||||
}
|
||||
values := map[string]ast.Node{}
|
||||
for _, entry := range mapping.Values {
|
||||
key, err := nodeString(unwrapNode(entry.Key))
|
||||
if err != nil {
|
||||
return FormField{}, err
|
||||
}
|
||||
if _, known := formFieldKeys[key]; !known {
|
||||
return FormField{}, fmt.Errorf("unknown field %s", key)
|
||||
}
|
||||
if _, dup := values[key]; dup {
|
||||
return FormField{}, fmt.Errorf("duplicate field %s", key)
|
||||
}
|
||||
values[key] = unwrapNode(entry.Value)
|
||||
}
|
||||
typ, err := nodeString(values["type"])
|
||||
if err != nil || typ == "" {
|
||||
return FormField{}, fmt.Errorf("type is required")
|
||||
}
|
||||
if typ == "partial" {
|
||||
return FormField{}, fmt.Errorf("type partial is not supported")
|
||||
}
|
||||
if _, ok := formFieldTypes[typ]; !ok {
|
||||
return FormField{}, fmt.Errorf("unsupported type %s", typ)
|
||||
}
|
||||
field := FormField{Name: name, Type: typ}
|
||||
if node, ok := values["label"]; ok {
|
||||
field.Label, err = nodeString(node)
|
||||
if err != nil {
|
||||
return FormField{}, fmt.Errorf("label: %w", err)
|
||||
}
|
||||
}
|
||||
if node, ok := values["comment"]; ok {
|
||||
field.Comment, err = nodeString(node)
|
||||
if err != nil {
|
||||
return FormField{}, fmt.Errorf("comment: %w", err)
|
||||
}
|
||||
}
|
||||
if node, ok := values["span"]; ok {
|
||||
field.Span, err = nodeString(node)
|
||||
if err != nil {
|
||||
return FormField{}, fmt.Errorf("span: %w", err)
|
||||
}
|
||||
if _, ok := formSpans[field.Span]; !ok {
|
||||
return FormField{}, fmt.Errorf("unsupported span %s", field.Span)
|
||||
}
|
||||
}
|
||||
if node, ok := values["tab"]; ok {
|
||||
field.Tab, err = nodeString(node)
|
||||
if err != nil {
|
||||
return FormField{}, fmt.Errorf("tab: %w", err)
|
||||
}
|
||||
}
|
||||
if node, ok := values["size"]; ok {
|
||||
field.Size, err = nodeString(node)
|
||||
if err != nil {
|
||||
return FormField{}, fmt.Errorf("size: %w", err)
|
||||
}
|
||||
if _, ok := formSizes[field.Size]; !ok {
|
||||
return FormField{}, fmt.Errorf("unsupported size %s", field.Size)
|
||||
}
|
||||
}
|
||||
if node, ok := values["context"]; ok {
|
||||
ctx, err := compileContext(node)
|
||||
if err != nil {
|
||||
return FormField{}, err
|
||||
}
|
||||
field.Context = ctx
|
||||
}
|
||||
if node, ok := values["nameFrom"]; ok {
|
||||
field.NameFrom, err = nodeString(node)
|
||||
if err != nil || !identifier(field.NameFrom) {
|
||||
return FormField{}, fmt.Errorf("nameFrom %q is not an identifier", field.NameFrom)
|
||||
}
|
||||
}
|
||||
if node, ok := values["emptyOption"]; ok {
|
||||
field.EmptyOption, err = nodeString(node)
|
||||
if err != nil {
|
||||
return FormField{}, fmt.Errorf("emptyOption: %w", err)
|
||||
}
|
||||
}
|
||||
if node, ok := values["relation"]; ok {
|
||||
field.Relation, err = nodeString(node)
|
||||
if err != nil || !identifier(field.Relation) {
|
||||
return FormField{}, fmt.Errorf("relation %q is not an identifier", field.Relation)
|
||||
}
|
||||
}
|
||||
if typ == "relation-manager" && field.Relation == "" {
|
||||
return FormField{}, fmt.Errorf("relation is required")
|
||||
}
|
||||
if node, ok := values["required"]; ok {
|
||||
field.Required, err = nodeBool(node)
|
||||
if err != nil {
|
||||
return FormField{}, fmt.Errorf("required: %w", err)
|
||||
}
|
||||
}
|
||||
if node, ok := values["default"]; ok {
|
||||
scalar, err := nodeScalar(node)
|
||||
if err != nil {
|
||||
return FormField{}, fmt.Errorf("default: %w", err)
|
||||
}
|
||||
field.Default = &scalar
|
||||
}
|
||||
if node, ok := values["attributes"]; ok {
|
||||
attrs, err := compileAttributes(node)
|
||||
if err != nil {
|
||||
return FormField{}, fmt.Errorf("attributes: %w", err)
|
||||
}
|
||||
field.Attributes = attrs
|
||||
}
|
||||
if node, ok := values["options"]; ok {
|
||||
if err := compileOptions(node, &field); err != nil {
|
||||
return FormField{}, fmt.Errorf("options: %w", err)
|
||||
}
|
||||
}
|
||||
return field, nil
|
||||
}
|
||||
|
||||
func compileContext(node ast.Node) (*fieldContext, error) {
|
||||
switch n := node.(type) {
|
||||
case *ast.StringNode:
|
||||
if !identifier(n.Value) {
|
||||
return nil, fmt.Errorf("context %q is not an identifier", n.Value)
|
||||
}
|
||||
return &fieldContext{single: true, values: []string{n.Value}}, nil
|
||||
case *ast.SequenceNode:
|
||||
values := sequenceValues(n)
|
||||
out := make([]string, 0, len(values))
|
||||
for _, item := range values {
|
||||
text, err := nodeString(unwrapNode(item))
|
||||
if err != nil || !identifier(text) {
|
||||
return nil, fmt.Errorf("context %q is not an identifier", nodeText(item))
|
||||
}
|
||||
out = append(out, text)
|
||||
}
|
||||
return &fieldContext{values: out}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("context must be a string or a list")
|
||||
}
|
||||
}
|
||||
|
||||
func compileAttributes(node ast.Node) (map[string]jsonScalar, error) {
|
||||
mapping, ok := node.(*ast.MappingNode)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("must be a mapping")
|
||||
}
|
||||
out := make(map[string]jsonScalar, len(mapping.Values))
|
||||
for _, entry := range mapping.Values {
|
||||
key, err := nodeString(unwrapNode(entry.Key))
|
||||
if err != nil || !identifier(key) {
|
||||
return nil, fmt.Errorf("key %q is not an identifier", nodeText(entry.Key))
|
||||
}
|
||||
scalar, err := nodeScalar(unwrapNode(entry.Value))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", key, err)
|
||||
}
|
||||
out[key] = scalar
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func compileOptions(node ast.Node, field *FormField) error {
|
||||
switch n := node.(type) {
|
||||
case *ast.StringNode:
|
||||
if !identifier(n.Value) {
|
||||
return fmt.Errorf("method %q is not an identifier", n.Value)
|
||||
}
|
||||
field.optionsMethod = n.Value
|
||||
return nil
|
||||
case *ast.MappingNode:
|
||||
options := make([]FormOption, 0, len(n.Values))
|
||||
for _, entry := range n.Values {
|
||||
value, err := nodeScalar(unwrapNode(entry.Key))
|
||||
if err != nil {
|
||||
return fmt.Errorf("value: %w", err)
|
||||
}
|
||||
label, err := nodeString(unwrapNode(entry.Value))
|
||||
if err != nil {
|
||||
return fmt.Errorf("label: %w", err)
|
||||
}
|
||||
options = append(options, FormOption{Value: value, Label: label})
|
||||
}
|
||||
field.Options = options
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("must be a map or a method name")
|
||||
}
|
||||
}
|
||||
|
||||
func sequenceValues(n *ast.SequenceNode) []ast.Node {
|
||||
if len(n.Values) > 0 {
|
||||
return n.Values
|
||||
}
|
||||
out := make([]ast.Node, 0, len(n.Entries))
|
||||
for _, entry := range n.Entries {
|
||||
if entry != nil && entry.Value != nil {
|
||||
out = append(out, entry.Value)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func unwrapNode(node ast.Node) ast.Node {
|
||||
for node != nil {
|
||||
switch n := node.(type) {
|
||||
case *ast.MappingKeyNode:
|
||||
node = n.Value
|
||||
case *ast.TagNode:
|
||||
node = n.Value
|
||||
case *ast.AnchorNode:
|
||||
node = n.Value
|
||||
default:
|
||||
return node
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func nodeString(node ast.Node) (string, error) {
|
||||
switch n := unwrapNode(node).(type) {
|
||||
case *ast.StringNode:
|
||||
return n.Value, nil
|
||||
case nil:
|
||||
return "", fmt.Errorf("missing string")
|
||||
default:
|
||||
return "", fmt.Errorf("want a string, got %T", n)
|
||||
}
|
||||
}
|
||||
|
||||
func nodeBool(node ast.Node) (bool, error) {
|
||||
n, ok := unwrapNode(node).(*ast.BoolNode)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("want a boolean")
|
||||
}
|
||||
return n.Value, nil
|
||||
}
|
||||
|
||||
func nodeScalar(node ast.Node) (jsonScalar, error) {
|
||||
switch n := unwrapNode(node).(type) {
|
||||
case *ast.StringNode:
|
||||
raw, err := json.Marshal(n.Value)
|
||||
return jsonScalar{raw: raw}, err
|
||||
case *ast.BoolNode:
|
||||
if n.Value {
|
||||
return jsonScalar{raw: []byte("true")}, nil
|
||||
}
|
||||
return jsonScalar{raw: []byte("false")}, nil
|
||||
case *ast.IntegerNode:
|
||||
return integerScalar(n.Value)
|
||||
case *ast.FloatNode:
|
||||
if n.Value == float64(int64(n.Value)) {
|
||||
return jsonScalar{raw: []byte(strconv.FormatInt(int64(n.Value), 10))}, nil
|
||||
}
|
||||
raw, err := json.Marshal(n.Value)
|
||||
return jsonScalar{raw: raw}, err
|
||||
default:
|
||||
return jsonScalar{}, fmt.Errorf("want a scalar, got %T", n)
|
||||
}
|
||||
}
|
||||
|
||||
func integerScalar(v any) (jsonScalar, error) {
|
||||
switch n := v.(type) {
|
||||
case int64:
|
||||
return jsonScalar{raw: []byte(strconv.FormatInt(n, 10))}, nil
|
||||
case uint64:
|
||||
return jsonScalar{raw: []byte(strconv.FormatUint(n, 10))}, nil
|
||||
case int:
|
||||
return jsonScalar{raw: []byte(strconv.Itoa(n))}, nil
|
||||
default:
|
||||
return jsonScalar{}, fmt.Errorf("want an integer, got %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func nodeText(node ast.Node) string {
|
||||
if node == nil {
|
||||
return ""
|
||||
}
|
||||
if token := node.GetToken(); token != nil {
|
||||
return token.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeYAMLError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, "already defined") {
|
||||
return fmt.Errorf("duplicate field: %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func compileFormIfPresent(pluginID string, ctl pact.AdminController, fsys fs.FS) (*FormSchema, error) {
|
||||
dir := strings.Trim(path.Clean(ctl.ConfigDir()), "/")
|
||||
if dir == "." || strings.HasPrefix(dir, "..") {
|
||||
return nil, bootErr(pluginID, ctl.ID(), ctl.ConfigDir(), fmt.Errorf("config directory escapes the plugin"))
|
||||
}
|
||||
cfgPath := path.Join(dir, "config_form.yaml")
|
||||
if _, err := fs.Stat(fsys, cfgPath); err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, bootErr(pluginID, ctl.ID(), cfgPath, err)
|
||||
}
|
||||
return CompileForm(pluginID, ctl, fsys)
|
||||
}
|
||||
Reference in New Issue
Block a user