Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 41 additions & 15 deletions cli/library_command.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package cli

import (
"encoding/json"
"fmt"

"github.com/choria-io/fisk"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/synadia-io/connect/model"
"github.com/synadia-io/connect/schema"

"os"
"strings"
Expand All @@ -22,6 +24,8 @@ type libraryCommand struct {
kind string
status string
component string

formatJsonSchema bool
}

func ConfigureLibraryCommand(parentCmd commandHost, opts *Options) {
Expand All @@ -48,6 +52,7 @@ func ConfigureLibraryCommand(parentCmd commandHost, opts *Options) {
infoCmd.Arg("runtime", "The runtime id").StringVar(&c.runtime)
infoCmd.Arg("kind", "The kind of component").EnumVar(&c.kind, kindOpts...)
infoCmd.Arg("name", "The name of the component").StringVar(&c.component)
infoCmd.Flag("jsonschema", "Output the component schema in JSON Schema format").UnNegatableBoolVar(&c.formatJsonSchema)
}

func (c *libraryCommand) listRuntimes(pc *fisk.ParseContext) error {
Expand Down Expand Up @@ -150,24 +155,45 @@ func (c *libraryCommand) info(pc *fisk.ParseContext) error {
os.Exit(1)
}

// Component info.
w := table.NewWriter()
w.SetStyle(table.StyleRounded)
w.SetTitle("Component Description")
w.AppendRow(table.Row{"Runtime", component.RuntimeId})
w.AppendRow(table.Row{"Name", component.Name})
w.AppendRow(table.Row{"Kind", component.Kind})
w.AppendRow(table.Row{"Status", component.Status})

if component.Description != nil {
w.AppendRow(table.Row{"Description", text.WrapSoft(*component.Description, 75)})
if component == nil {
color.Red("Component not found")
os.Exit(1)
}

result := w.Render()
fmt.Println(result)
if c.formatJsonSchema {
jsch, err := schema.ToJsonSchema(c.runtime, "", c.kind, *component)
if err != nil {
color.Red("Could not convert to JSON Schema: %s", err)
os.Exit(1)
}

for _, field := range component.Fields {
printField(field, "")
jout, err := json.MarshalIndent(jsch, "", " ")
if err != nil {
color.Red("Could not marshal JSON Schema: %s", err)
os.Exit(1)
}

fmt.Println(string(jout))
} else {
// Component info.
w := table.NewWriter()
w.SetStyle(table.StyleRounded)
w.SetTitle("Component Description")
w.AppendRow(table.Row{"Runtime", component.RuntimeId})
w.AppendRow(table.Row{"Name", component.Name})
w.AppendRow(table.Row{"Kind", component.Kind})
w.AppendRow(table.Row{"Status", component.Status})

if component.Description != nil {
w.AppendRow(table.Row{"Description", text.WrapSoft(*component.Description, 75)})
}

result := w.Render()
fmt.Println(result)

for _, field := range component.Fields {
printField(field, "")
}
}

return nil
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/choria-io/fisk v0.7.1
github.com/evanphx/json-patch/v5 v5.9.11
github.com/fatih/color v1.18.0
github.com/google/jsonschema-go v0.3.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/joho/godotenv v1.5.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18=
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
Expand Down
233 changes: 233 additions & 0 deletions schema/jsonschema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package schema

import (
"encoding/json"
"fmt"

"github.com/google/jsonschema-go/jsonschema"
"github.com/synadia-io/connect/model"
)

func ToJsonSchema(runtime string, version string, kind string, cmp model.Component) (*jsonschema.Schema, error) {
s := &jsonschema.Schema{
ID: fmt.Sprintf("%s.v%s.%s.%s", runtime, version, kind, cmp.Name),
Title: cmp.Label,
Type: "object",
Extra: map[string]any{
"status": string(cmp.Status),
},
Properties: map[string]*jsonschema.Schema{},
Required: []string{},
}

if cmp.Description != nil {
s.Description = *cmp.Description
}

if cmp.Icon != nil {
s.Extra["icon"] = *cmp.Icon
}

for _, field := range cmp.Fields {
fs, err := fieldSchema(field.Kind, field.Type, field)
if err != nil {
return nil, err
}

s.Properties[field.Name] = fs

if field.Optional != nil && !*field.Optional {
s.Required = append(s.Required, field.Name)
}
}

return s, nil
}

func fieldSchema(fk model.ComponentFieldKind, ft model.ComponentFieldType, fld model.ComponentField) (*jsonschema.Schema, error) {
switch fk {
case model.ComponentFieldKindScalar:
switch ft {
case model.ComponentFieldTypeString, model.ComponentFieldTypeExpression, model.ComponentFieldTypeCondition:
return stringSchema(fld)
case model.ComponentFieldTypeInt:
return integerSchema(fld)
case model.ComponentFieldTypeBool:
return booleanSchema(fld)
case model.ComponentFieldTypeObject:
return objectSchema(fld)
case model.ComponentFieldTypeScanner:
return scannerSchema(fld)
default:
return nil, fmt.Errorf("unsupported field type: %s", fld.Type)
}
case model.ComponentFieldKindList:
itemSchema, err := fieldSchema(model.ComponentFieldKindScalar, ft, model.ComponentField{
Type: ft,
Fields: fld.Fields,
})
if err != nil {
return nil, err
}

result, err := commonSchema(fld)
if err != nil {
return nil, err
}

result.Type = "array"
result.Items = itemSchema
return result, nil

case model.ComponentFieldKindMap:
itemSchema, err := fieldSchema(model.ComponentFieldKindScalar, ft, model.ComponentField{
Type: ft,
Fields: fld.Fields,
})
if err != nil {
return nil, err
}

result, err := commonSchema(fld)
if err != nil {
return nil, err
}

result.Type = "object"
result.AdditionalProperties = itemSchema
return result, nil

default:
return nil, fmt.Errorf("unsupported field kind: %s", fk)
}
}

func commonSchema(fld model.ComponentField) (*jsonschema.Schema, error) {
result := &jsonschema.Schema{
Title: fld.Label,
Extra: map[string]any{},
}

if fld.Description != nil {
result.Description = *fld.Description
}

if fld.Default != nil {
b, err := json.Marshal(fld.Default)
if err != nil {
return nil, fmt.Errorf("failed to marshal default value: %w", err)
}

result.Default = b
}

if fld.Secret != nil {
result.Extra["secret"] = *fld.Secret
}

if fld.RenderHint != nil {
result.Extra["preset"] = *fld.RenderHint
}

if fld.Examples != nil {
result.Examples = fld.Examples
}

return result, nil
}

func stringSchema(fld model.ComponentField) (*jsonschema.Schema, error) {
s, err := commonSchema(fld)
if err != nil {
return nil, err
}

s.Type = "string"

switch fld.Type {
case model.ComponentFieldTypeExpression:
s.Extra["render_hint"] = "expression"
case model.ComponentFieldTypeCondition:
s.Extra["render_hint"] = "condition"
}

for _, constraint := range fld.Constraints {
// -- add enum values
if constraint.Enum != nil {
for _, v := range constraint.Enum {
s.Enum = append(s.Enum, v)
}
}

if constraint.Preset != nil {
s.Extra["preset"] = constraint.Preset
}
// todo: add other constraints
}

return s, nil
}

func booleanSchema(fld model.ComponentField) (*jsonschema.Schema, error) {
s, err := commonSchema(fld)
if err != nil {
return nil, err
}

s.Type = "boolean"

return s, nil
}

func integerSchema(fld model.ComponentField) (*jsonschema.Schema, error) {
s, err := commonSchema(fld)
if err != nil {
return nil, err
}

s.Type = "integer"

// todo: add range constraints

return s, nil
}

func objectSchema(fld model.ComponentField) (*jsonschema.Schema, error) {
s, err := commonSchema(fld)
if err != nil {
return nil, err
}

s.Type = "object"
s.Properties = map[string]*jsonschema.Schema{}

// add the child fields
for _, childFld := range fld.Fields {
childSchema, err := fieldSchema(childFld.Kind, childFld.Type, *childFld)
if err != nil {
return nil, fmt.Errorf("failed to convert field %s to jsonschema: %w", childFld.Name, err)
}

s.Properties[childFld.Name] = childSchema

if childFld.Optional != nil && !*childFld.Optional {
s.Required = append(s.Required, childFld.Name)
}
}

return s, nil
}

func scannerSchema(fld model.ComponentField) (*jsonschema.Schema, error) {
s, err := commonSchema(fld)
if err != nil {
return nil, err
}

s.Type = "object"
s.AdditionalProperties = &jsonschema.Schema{
Type: "object",
}

return s, nil
}