mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-11-09 00:22:38 -06:00
161 lines
3.8 KiB
Go
161 lines
3.8 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package mcp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"maps"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
"github.com/google/jsonschema-go/jsonschema"
|
|
)
|
|
|
|
const fieldsParam = "fields"
|
|
|
|
type projection int
|
|
|
|
const (
|
|
noProjection projection = iota
|
|
projectBody
|
|
// Keeps the Paginated envelope's paging metadata.
|
|
projectItems
|
|
)
|
|
|
|
func projectionFor(oapi *huma.OpenAPI, op *huma.Operation) projection {
|
|
if op.Method != http.MethodGet {
|
|
return noProjection
|
|
}
|
|
res := op.Responses["200"]
|
|
if res == nil || res.Content["application/json"] == nil || res.Content["application/json"].Schema == nil {
|
|
return noProjection
|
|
}
|
|
s := res.Content["application/json"].Schema
|
|
if s.Ref != "" {
|
|
s = oapi.Components.Schemas.SchemaFromRef(s.Ref)
|
|
}
|
|
if s != nil && s.Properties["items"] != nil && s.Properties["total_pages"] != nil {
|
|
return projectItems
|
|
}
|
|
return projectBody
|
|
}
|
|
|
|
func fieldsSchema() *jsonschema.Schema {
|
|
one := 1
|
|
return &jsonschema.Schema{
|
|
Type: "array",
|
|
Items: &jsonschema.Schema{Type: "string", MinLength: &one},
|
|
MinItems: &one,
|
|
Description: `Return only these response fields. Dot paths select nested fields, also inside arrays (e.g. ["id", "title", "labels.title"]). Omit for the full response.`,
|
|
}
|
|
}
|
|
|
|
// A nil subtree selects the whole value.
|
|
type fieldTree map[string]fieldTree
|
|
|
|
func parseFields(paths []string) fieldTree {
|
|
root := fieldTree{}
|
|
for _, path := range paths {
|
|
node := root
|
|
parts := strings.Split(path, ".")
|
|
for i, part := range parts {
|
|
sub, seen := node[part]
|
|
if seen && sub == nil {
|
|
break
|
|
}
|
|
if i == len(parts)-1 {
|
|
node[part] = nil
|
|
break
|
|
}
|
|
if sub == nil {
|
|
sub = fieldTree{}
|
|
node[part] = sub
|
|
}
|
|
node = sub
|
|
}
|
|
}
|
|
return root
|
|
}
|
|
|
|
func (t fieldTree) apply(v any) any {
|
|
switch x := v.(type) {
|
|
case []any:
|
|
out := make([]any, len(x))
|
|
for i, e := range x {
|
|
out[i] = t.apply(e)
|
|
}
|
|
return out
|
|
case map[string]any:
|
|
var out map[string]any
|
|
for k, sub := range t {
|
|
val, ok := x[k]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if out == nil {
|
|
out = make(map[string]any, len(t))
|
|
}
|
|
if sub == nil {
|
|
out[k] = val
|
|
} else {
|
|
out[k] = sub.apply(val)
|
|
}
|
|
}
|
|
if out == nil {
|
|
// Never fall back to the full object.
|
|
return map[string]any{}
|
|
}
|
|
return out
|
|
}
|
|
return v
|
|
}
|
|
|
|
// takeFields removes the fields argument so it is not forwarded to the v2 route.
|
|
func takeFields(args map[string]json.RawMessage) (fieldTree, error) {
|
|
raw, ok := args[fieldsParam]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
delete(args, fieldsParam)
|
|
var paths []string
|
|
if err := json.Unmarshal(raw, &paths); err != nil {
|
|
return nil, err
|
|
}
|
|
return parseFields(paths), nil
|
|
}
|
|
|
|
func (p projection) apply(fields fieldTree, result any) any {
|
|
if fields == nil {
|
|
return result
|
|
}
|
|
switch p {
|
|
case projectItems:
|
|
envelope, ok := result.(map[string]any)
|
|
if !ok {
|
|
return result
|
|
}
|
|
out := maps.Clone(envelope)
|
|
out["items"] = fields.apply(envelope["items"])
|
|
return out
|
|
case projectBody:
|
|
return fields.apply(result)
|
|
case noProjection:
|
|
}
|
|
return result
|
|
}
|