feat(10-02): filter choices and a fully typed admin API proven on the wire
- pact.FilterOptions on the model serves a scope filter's choices; a scope
filter whose model lacks it fails activation (D-27)
- GET /{vendor}/{plugin}/{controller}/filters/{scope}/options answers a
declared scope filter behind the controller permission with localized
{value, label} choices, 404 otherwise
- Every admin route documents a typed success schema, and protected routes
document 401, 403 and 404 (422 on writes); SuccessEnvelope is gone and
logout writes a typed AdminLogoutData
- jsonScalar and fieldContext decode their served shapes
- TestPhase10OpenAPIConformance calls every inventoried route through the
assembled router on PostgreSQL and decodes each body into its documented
type with unknown fields disallowed, checking admin.json's schema ref
- The SPA aliases every new schema type; Tailwind no longer scans the
generated API files, so API changes do not churn boardwalk/dist
This commit is contained in:
@@ -3,6 +3,7 @@ package cabana
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ListSort is the compiled default sort. Direction is exactly asc or desc.
|
||||
@@ -191,12 +192,45 @@ func (s jsonScalar) MarshalJSON() ([]byte, error) {
|
||||
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 := ""
|
||||
|
||||
Reference in New Issue
Block a user