package cabana import ( "context" "encoding/json" "fmt" ) // ListSort is the compiled default sort. Direction is exactly asc or desc. type ListSort struct { Column string `json:"column"` Direction string `json:"direction"` } // ListColumn is one compiled columns.yaml entry, in file order. // Omitted sortable defaults to true, matching Winter; searchable defaults to false. type ListColumn struct { Key string `json:"key"` Label string `json:"label"` Searchable bool `json:"searchable"` Sortable bool `json:"sortable"` Type string `json:"type,omitempty"` Relation string `json:"relation,omitempty"` Select string `json:"select,omitempty"` } // ListFilter is one compiled config_filter scope. Values stay typed scalars. type ListFilter struct { Name string `json:"name"` Label string `json:"label,omitempty"` Type string `json:"type"` Column string `json:"column,omitempty"` Scope string `json:"scope,omitempty"` ModelClass string `json:"modelClass,omitempty"` NameFrom string `json:"nameFrom,omitempty"` TrueValue *jsonScalar `json:"trueValue,omitempty"` FalseValue *jsonScalar `json:"falseValue,omitempty"` Options []FormOption `json:"options,omitempty"` } // RowAction is a per-record link derived from recordUrl. type RowAction struct { Name string `json:"name"` Label string `json:"label,omitempty"` URL string `json:"url,omitempty"` } // BulkAction is a checkbox action. It carries no SQL. type BulkAction struct { Name string `json:"name"` Label string `json:"label,omitempty"` } // ListSchema is the boot-compiled list contract for one controller. // Labels stay as source keys until a request localizes a copy. type ListSchema struct { Title string `json:"title,omitempty"` ModelClass string `json:"modelClass,omitempty"` RecordURL string `json:"recordUrl,omitempty"` NoRecordsMessage string `json:"noRecordsMessage,omitempty"` RecordsPerPage int `json:"recordsPerPage"` PerPageOptions []int `json:"perPageOptions"` ShowSearch bool `json:"showSearch"` ShowSetup bool `json:"showSetup"` ShowCheckboxes bool `json:"showCheckboxes"` ShowSorting bool `json:"showSorting"` SearchTerm string `json:"searchTerm"` SearchPrompt string `json:"searchPrompt,omitempty"` DefaultSort *ListSort `json:"defaultSort,omitempty"` ToolbarButtons []string `json:"toolbarButtons"` Columns []ListColumn `json:"columns"` Filters []ListFilter `json:"filters"` RowActions []RowAction `json:"rowActions"` BulkActions []BulkAction `json:"bulkActions"` // Messages is the list's copy (D-13). The cached schema carries each // phrase key as its own form; a response resolves them in its locale. Messages *ListMessages `json:"messages"` Meta *FormMeta `json:"meta,omitempty"` messageKeys listMessageKeys } // MarshalJSON keeps omitted collections as arrays so a partial copy cannot emit null. func (s ListSchema) MarshalJSON() ([]byte, error) { if s.Messages == nil { keys := localizeMessages[listMessageKeys, ListMessages](context.Background(), nil, s.listMessageKeySet()) s.Messages = &keys } type alias ListSchema out := alias(s) if out.PerPageOptions == nil { out.PerPageOptions = []int{} } if out.ToolbarButtons == nil { out.ToolbarButtons = []string{} } if out.Columns == nil { out.Columns = []ListColumn{} } if out.Filters == nil { out.Filters = []ListFilter{} } if out.RowActions == nil { out.RowActions = []RowAction{} } if out.BulkActions == nil { out.BulkActions = []BulkAction{} } return json.Marshal(out) } // FormSchema is the locale-neutral form contract compiled once at boot. // Display strings stay as source keys until a request localizes a copy. type FormSchema struct { Name string `json:"name,omitempty"` ModelClass string `json:"modelClass,omitempty"` Fields []FormField `json:"fields"` messageKeys formMessageKeys redirects FormRedirects } // FormView is one request's localized form, including the locale actually used. type FormView struct { Name string `json:"name,omitempty"` ModelClass string `json:"modelClass,omitempty"` Fields []FormField `json:"fields"` // Messages is the form's copy resolved in the request locale (D-13). Messages FormMessages `json:"messages"` // Redirects are the raw Winter config_form.yaml targets; the SPA maps // them onto its routes. Redirects FormRedirects `json:"redirects"` Meta FormMeta `json:"meta"` } // FormRedirects are config_form.yaml defaultRedirect, create.* and update.*. type FormRedirects struct { Default string `json:"default"` Create FormRedirect `json:"create"` Update FormRedirect `json:"update"` } // FormRedirect is one Winter redirect pair. type FormRedirect struct { Redirect string `json:"redirect"` RedirectClose string `json:"redirectClose"` } // FormMeta reports the locale selected for a schema response. type FormMeta struct { Locale string `json:"locale"` } // FormField is one Winter field in source order. JSON keys keep Winter spelling. type FormField struct { Name string `json:"name"` Type string `json:"type"` Label string `json:"label,omitempty"` Comment string `json:"comment,omitempty"` Span string `json:"span,omitempty"` Tab string `json:"tab,omitempty"` Size string `json:"size,omitempty"` Context *fieldContext `json:"context,omitempty"` NameFrom string `json:"nameFrom,omitempty"` EmptyOption string `json:"emptyOption,omitempty"` Relation string `json:"relation,omitempty"` Multiple bool `json:"multiple,omitempty"` ReadOnly bool `json:"readOnly,omitempty"` Required bool `json:"required,omitempty"` Default *jsonScalar `json:"default,omitempty"` Attributes map[string]jsonScalar `json:"attributes,omitempty"` Options []FormOption `json:"options,omitempty"` optionsMethod string } // FormOption is one dropdown choice. Value keeps the YAML scalar's JSON type. type FormOption struct { Value jsonScalar `json:"value"` Label string `json:"label"` } // jsonScalar is a JSON scalar that still emits false, 0, and empty string. type jsonScalar struct { raw json.RawMessage } func (s jsonScalar) MarshalJSON() ([]byte, error) { if len(s.raw) == 0 { return []byte("null"), nil } return s.raw, nil } // UnmarshalJSON accepts exactly a JSON scalar (string, number, boolean or // null), so a served schema decodes back into its documented type. func (s *jsonScalar) UnmarshalJSON(raw []byte) error { var value any if err := json.Unmarshal(raw, &value); err != nil { return err } switch value.(type) { case nil: s.raw = nil case string, float64, bool: s.raw = append(json.RawMessage(nil), raw...) default: return fmt.Errorf("cabana: %s is not a JSON scalar", raw) } return nil } // fieldContext preserves a single Winter context string or a source-ordered list. type fieldContext struct { single bool values []string } // UnmarshalJSON accepts the two served shapes: a string or a string list. func (c *fieldContext) UnmarshalJSON(raw []byte) error { var single string if err := json.Unmarshal(raw, &single); err == nil { *c = fieldContext{single: true, values: []string{single}} return nil } var values []string if err := json.Unmarshal(raw, &values); err != nil { return fmt.Errorf("cabana: context must be a string or a list of strings") } *c = fieldContext{values: values} return nil } func (c fieldContext) MarshalJSON() ([]byte, error) { if c.single { text := "" if len(c.values) > 0 { text = c.values[0] } return json.Marshal(text) } values := c.values if values == nil { values = []string{} } return json.Marshal(values) }