diff --git a/cmd/declarative/config/config.go b/cmd/declarative/config/config.go index 8f2b5a87..2d1cce9a 100644 --- a/cmd/declarative/config/config.go +++ b/cmd/declarative/config/config.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2024 The Falco Authors +// Copyright (C) 2025 The Falco Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -93,8 +93,8 @@ var containerImagePullPolicies = map[builder.ImagePullPolicy][]string{ builder.ImagePullPolicyIfNotPresent: {"ifnotpresent"}, } -// New creates a new config linked to the provided command. -func New(cmd *cobra.Command, declarativeEnvKey, envKeysPrefix string) *Config { +// New creates a new config. +func New(declarativeEnvKey, envKeysPrefix string) *Config { commonConf := &Config{ DeclarativeEnvKey: declarativeEnvKey, EnvKeysPrefix: envKeysPrefix, @@ -104,21 +104,22 @@ func New(cmd *cobra.Command, declarativeEnvKey, envKeysPrefix string) *Config { LabelsEnvKey: envKeyFromFlagName(envKeysPrefix, LabelsFlagName), TimeoutEnvKey: envKeyFromFlagName(envKeysPrefix, TimeoutFlagName), } - commonConf.initFlags(cmd) return commonConf } -// initFlags initializes the provided command's flags and uses the config instance to store the flag bound values. -func (c *Config) initFlags(cmd *cobra.Command) { - flags := cmd.PersistentFlags() +// InitCommandFlags initializes the provided command's flags and uses the config instance to store the flag bound +// values. +func (c *Config) InitCommandFlags(cmd *cobra.Command) { + flags := cmd.Flags() + // Miscellaneous flags. flags.StringVarP(&c.TestsDescriptionFile, DescriptionFileFlagName, "f", "", "The tests description YAML file specifying the tests to be run") flags.StringVarP(&c.TestsDescription, DescriptionFlagName, "d", "", "The YAML-formatted tests description string specifying the tests to be run") cmd.MarkFlagsMutuallyExclusive(DescriptionFileFlagName, DescriptionFlagName) flags.DurationVarP(&c.TestsTimeout, TimeoutFlagName, "t", time.Minute, - "The maximal duration of the tests. If running tests lasts more than testsTimeout, the execution of "+ + "The maximal duration of the tests. If running tests lasts more than the provided timeout, the execution of "+ "all pending tasks is canceled") // Container runtime flags. diff --git a/cmd/declarative/declarative.go b/cmd/declarative/declarative.go index d3708eab..178580bb 100644 --- a/cmd/declarative/declarative.go +++ b/cmd/declarative/declarative.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2024 The Falco Authors +// Copyright (C) 2025 The Falco Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import ( "github.com/spf13/cobra" "github.com/falcosecurity/event-generator/cmd/declarative/config" + "github.com/falcosecurity/event-generator/cmd/declarative/explain" "github.com/falcosecurity/event-generator/cmd/declarative/run" "github.com/falcosecurity/event-generator/cmd/declarative/test" ) @@ -32,11 +33,13 @@ func New(declarativeEnvKey, envKeysPrefix string) *cobra.Command { DisableAutoGenTag: true, } - commonConf := config.New(c, declarativeEnvKey, envKeysPrefix) + commonConf := config.New(declarativeEnvKey, envKeysPrefix) runCmd := run.New(commonConf) testCmd := test.New(commonConf, false).Command + explainCmd := explain.New().Command c.AddCommand(runCmd) c.AddCommand(testCmd) + c.AddCommand(explainCmd) return c } diff --git a/cmd/declarative/explain/doc.go b/cmd/declarative/explain/doc.go new file mode 100644 index 00000000..e7b54b8d --- /dev/null +++ b/cmd/declarative/explain/doc.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2025 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package explain provides the implementation of the "explain" command. This command provides the user with explanation +// of the tests description properties. +package explain diff --git a/cmd/declarative/explain/encoders.go b/cmd/declarative/explain/encoders.go new file mode 100644 index 00000000..c8c360f1 --- /dev/null +++ b/cmd/declarative/explain/encoders.go @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2025 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package explain + +import ( + "fmt" + "io" + "reflect" + "strings" + + "github.com/goccy/go-yaml" + + "github.com/falcosecurity/event-generator/pkg/test/loader/schema" +) + +// documentationEncoder allows to encode the documentation. +type documentationEncoder interface { + // encode encodes the node tree starting at the provided node with a specific format and writes it to the underlying + // destination. + encode(node *schema.Node) error +} + +// textEncoder encodes a node tree using a custom text-based format. +// Notice: the format slightly variates from YAML, as it uses capitalized snake-cased key, adaptive list printing and +// additional spacing. +type textEncoder struct { + w io.Writer +} + +// Verify that textEncoder implements documentationEncoder interface. +var _ documentationEncoder = (*textEncoder)(nil) + +// newTextEncoder creates a new textEncoder able to write the node tree to the provided destination. +func newTextEncoder(w io.Writer) *textEncoder { + return &textEncoder{w: w} +} + +// encode encodes the provided node tree and writes it to the underlying destination. +func (e *textEncoder) encode(node *schema.Node) error { + if node == nil { + return nil + } + + sb := &strings.Builder{} + writeNode(sb, "", node) + _, err := fmt.Fprint(e.w, sb.String()) + return err +} + +const defaultPadding = " " + +// writeNode writes the node hierarchy starting at the providing node to the provided string builder. +func writeNode(sb *strings.Builder, padding string, node *schema.Node) { + if node == nil { + return + } + + writeKeyValue(sb, padding, "NAME", node.Name) + writeKeySlice(sb, padding, "DESCRIPTIONS", node.Descriptions) + writeKeySliceInline(sb, padding, "TYPES", node.JSONTypes) + writeKeyValue(sb, padding, "REQUIRED", node.Required) + writeKeyValue(sb, padding, "MINIMUM", node.Minimum) + writeKeyValue(sb, padding, "MIN_LENGTH", node.MinLength) + writeKeyValue(sb, padding, "MIN_ITEMS", node.MinItems) + writeKeyValue(sb, padding, "MIN_PROPERTIES", node.MinProperties) + writeKeyValue(sb, padding, "PATTERN", node.Pattern) + writeKeySliceInline(sb, padding, "ENUM", node.Enum) + writeKeyValue(sb, padding, "DEFAULT", node.Default) + writeKeySlice(sb, padding, "EXAMPLES", node.Examples) + + if metadata := node.Metadata; metadata != nil { + writeKeyValue(sb, padding, "FIELD_TYPE", metadata.Type) + writeKeyValue(sb, padding, "IS_BIND_ONLY", metadata.IsBindOnly) + } + + length := len(node.Children) + if length > 0 { + writeFormatted(sb, "%sFIELDS:\n", padding) + } + for idx, child := range node.Children { + writeNode(sb, padding+defaultPadding, child) + if idx != length-1 { + sb.WriteByte('\n') + } + } + + length = len(node.PseudoChildren) + if length > 0 { + writeFormatted(sb, "%sEXPOSED FIELDS:\n", padding) + } + for idx, child := range node.PseudoChildren { + writeNode(sb, padding+defaultPadding, child) + if idx != length-1 { + sb.WriteByte('\n') + } + } +} + +// writeKeyValue writes the association between the provided key and the value to the provided string builder. If the +// provided value is a nil pointer, is the zero value of its kind, or it is a pointer pointing to a zero value, nothing +// is written. +func writeKeyValue(sb *strings.Builder, padding, key string, value any) { + valueOfValue := reflect.ValueOf(value) + + // Don't write anything if it is the zero value for its type. + if valueOfValue.IsZero() { + return + } + + // If it is a pointer, dereference it. + valueOfValue = reflect.Indirect(valueOfValue) + + // Check if the dereferenced value is the zero value for its type. + if valueOfValue.IsZero() { + return + } + + writeFormatted(sb, "%s%s: %v\n", padding, key, valueOfValue.Interface()) +} + +// writeFormatted is a wrapper around fmt.Fprintf ignoring the returned values. +func writeFormatted(w io.Writer, format string, a ...any) { + _, _ = fmt.Fprintf(w, format, a...) +} + +// writeKeySlice writes, if the provided slice is not empty, the association between the provided key and the slice to +// the provided string builder. The output format depends on the number of elements the slice contains. +func writeKeySlice[T any](sb *strings.Builder, padding, key string, slice []T) { + if len(slice) == 0 { + return + } + + writeFormatted(sb, "%s%s", padding, key) + switch len(slice) { + case 1: + writeFormatted(sb, ": %v\n", slice[0]) + default: + writeFormatted(sb, ":\n") + for _, e := range slice { + writeFormatted(sb, "%s- %v\n", padding+defaultPadding, e) + } + } +} + +// writeKeySliceInline is a variant of writeKeySlice writing multiple slice elements as a comma-separated list. +func writeKeySliceInline[T any](sb *strings.Builder, padding, key string, slice []T) { + if len(slice) == 0 { + return + } + + writeFormatted(sb, "%s%s", padding, key) + switch len(slice) { + case 1: + writeFormatted(sb, ": %v", slice[0]) + default: + writeFormatted(sb, ": %v", slice[0]) + for _, e := range slice[1:] { + writeFormatted(sb, ", %v", e) + } + } + sb.WriteByte('\n') +} + +// yamlEncoder encodes a node tree using a YAML format. +type yamlEncoder struct { + w io.Writer +} + +// Verify that yamlEncoder implements documentationEncoder interface. +var _ documentationEncoder = (*yamlEncoder)(nil) + +// newYAMLEncoder creates a new yamlEncoder able to write the node tree to the provided destination. +func newYAMLEncoder(w io.Writer) *yamlEncoder { + return &yamlEncoder{w: w} +} + +func (e *yamlEncoder) encode(node *schema.Node) error { + return yaml.NewEncoder(e.w).Encode(node) +} diff --git a/cmd/declarative/explain/explain.go b/cmd/declarative/explain/explain.go new file mode 100644 index 00000000..d51686de --- /dev/null +++ b/cmd/declarative/explain/explain.go @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2025 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package explain + +import ( + "fmt" + "os" + "regexp" + "strings" + + "github.com/spf13/cobra" + "github.com/thediveo/enumflag" + + "github.com/falcosecurity/event-generator/pkg/test/loader/schema" +) + +const ( + longDescriptionHeading = "Document test(s) YAML description properties" + longDescriptionTemplate = `%s. +Documentation is generated by following the property hierarchies in the YAML description. + +The can be used to specify to output the documentation for the desired properties. The expression is +composed by one or multiple dot-separated property path segments. Each property path segment follows the following +syntax: [{=}[{=}...]]. +The optional {=} parts are called "enum requirements" and can be used to set the value of an +enumerated property (under the property ) to a specific value: this enables the generation of the +documentation for properties enabled only by the presence of that value. +Enum requirements are evaluated in their appearing order, so order matters. +The following examples demonstrate how to specify property path expressions: + +event-generator declarative explain tests.context.processes + +event-generator declarative explain tests.steps{type=syscall}{syscall=openat2}.args.how + +Enum requirements can be provided also by using the --with flag (see --with flag documentation for the syntax). It is +not possible to specify enum requirements both with and the --with flag.` +) + +var ( + longDescription = fmt.Sprintf(longDescriptionTemplate, longDescriptionHeading) +) + +// documentationFormat defines the types of format used for outputting the documentation. +type documentationFormat int + +const ( + // documentationFormatText specifies to format the documentation using a formatted text encoding. + documentationFormatText documentationFormat = iota + // documentationFormatYAML specifies to format the documentation using a YAML encoding. + documentationFormatYAML +) + +var documentationFormats = map[documentationFormat][]string{ + documentationFormatText: {"text"}, + documentationFormatYAML: {"yaml"}, +} + +// CommandWrapper wraps the command and stores the associated flag values. +type CommandWrapper struct { + Command *cobra.Command + + // Flags + // + // With contains a list of key/value pairs in the form =. Each key is a dot-separated list of property + // path segments leading to a property accepting enumerated values. This flag is empty if + // contains enum requirements. + with []string + // docFormat defines the documentation output format. + docFormat documentationFormat +} + +// New creates a new explain command. +func New() *CommandWrapper { + cw := &CommandWrapper{} + c := &cobra.Command{ + Use: "explain []", + Short: longDescriptionHeading, + Long: longDescription, + DisableAutoGenTag: true, + RunE: cw.runE, + } + cw.Command = c + cw.initCommandFlags() + return cw +} + +// initCommandFlags initializes the command's flags. +func (cw *CommandWrapper) initCommandFlags() { + flags := cw.Command.Flags() + + flags.StringSliceVar(&cw.with, "with", nil, + "A list of comma-separated enum requirements. An enum requirement is expressed as a = expression. "+ + "Each key is a dot-separated list of property path segments leading to a property accepting enumerated "+ + "values. This flag cannot be used if contains enum requirements. "+ + "Example: '--with tests.steps.type=syscall,tests.steps.syscall=write'") + flags.VarP( + enumflag.New(&cw.docFormat, "format", documentationFormats, enumflag.EnumCaseInsensitive), "format", "f", + "The output format for the documentation; can be 'text' or 'yaml'") +} + +var errForbiddenWithClauses = fmt.Errorf("enum requirements cannot be specified both using and " + + "--with clauses") + +func (cw *CommandWrapper) runE(_ *cobra.Command, args []string) error { + var err error + var propPath []string + var enumRequirements []*schema.EnumRequirement + + switch argsNum := len(args); argsNum { + case 0: // keep propPath and enumRequirements set to their default values. + case 1: + if propPath, enumRequirements, err = parsePropertyPathExpression(args[0]); err != nil { + return fmt.Errorf("error parsing property path expression: %w", err) + } + default: + return fmt.Errorf("expected [], got %d arguments", argsNum) + } + + if len(enumRequirements) == 0 { + if enumRequirements, err = parseWithClauses(cw.with); err != nil { + return fmt.Errorf("error parsing --with clauses: %w", err) + } + } else if len(cw.with) > 0 { + // --with clauses are forbidden if the enum requirements are provided via property path expression. + return errForbiddenWithClauses + } + + // Retrieve the tree describing the entire path towards the requested property path. + rootProperty, err := schema.Describe(propPath, enumRequirements) + if err != nil { + return fmt.Errorf("error describing section: %w", err) + } + + // Print the documentation to stdout. + if err := printDoc(rootProperty, cw.docFormat); err != nil { + return fmt.Errorf("error outputting documentation: %w", err) + } + + return nil +} + +// printDoc prints the documentation for the node tree starting at the provided node, by using the provided format +// specifier, to stdout. +func printDoc(node *schema.Node, docFormat documentationFormat) error { + var encoder documentationEncoder + switch docFormat { + case documentationFormatText: + encoder = newTextEncoder(os.Stdout) + case documentationFormatYAML: + encoder = newYAMLEncoder(os.Stdout) + default: + panic(fmt.Sprintf("unsupported documentation output format %v", docFormat)) + } + + return encoder.encode(node) +} + +var ( + // propPathExprRegex matches against [{=}[{=}...]], + // capturing (1) and (2) the remaining part as wholes. + propPathExprRegex = regexp.MustCompile(`^(\w+)((?:{\w+=\w+})*)$`) + // enumRequirementsRegex matches against [{=}[{=}...]], capturing each + // couple / separately. + enumRequirementsRegex = regexp.MustCompile(`{(\w+)=(\w+)}+`) +) + +var errInvalidPropertyPathSegmentExpr = fmt.Errorf("must match %v", propPathExprRegex.String()) + +// parsePropertyPathExpression parses the provided property path expression and returns the encoded property path and +// enum requirements. +func parsePropertyPathExpression(propPathExpr string) (propPath []string, enumRequirements []*schema.EnumRequirement, + err error) { + segments := strings.Split(propPathExpr, ".") + for _, segment := range segments { + matches := propPathExprRegex.FindStringSubmatch(segment) + if len(matches) == 0 { + return nil, nil, fmt.Errorf("invalid path segment %q: %w", segment, errInvalidPropertyPathSegmentExpr) + } + + propName, enumRequirementsExpr := matches[1], matches[2] + propPath = append(propPath, propName) + enumRequirements = append(enumRequirements, parseEnumRequirementsExpression(propPath, enumRequirementsExpr)...) + } + return +} + +// parseEnumRequirementsExpression parses the provided enum requirements expression and returns the resulting enum +// requirements. +func parseEnumRequirementsExpression(basePropPath []string, enumReqsExpr string) []*schema.EnumRequirement { + allMatches := enumRequirementsRegex.FindAllStringSubmatch(enumReqsExpr, -1) + requirements := make([]*schema.EnumRequirement, 0, len(allMatches)) + for _, matches := range allMatches { + propName, propValue := matches[1], matches[2] + requirements = append(requirements, &schema.EnumRequirement{ + PathSegments: append(basePropPath, propName), + Value: propValue, + }) + } + return requirements +} + +// parseWithClauses parses the provided list of with clauses and returns the resulting enum requirements. +func parseWithClauses(withClauses []string) ([]*schema.EnumRequirement, error) { + requirements := make([]*schema.EnumRequirement, 0, len(withClauses)) + for _, withClause := range withClauses { + splitWithClause := strings.Split(withClause, "=") + if len(splitWithClause) != 2 { + return nil, fmt.Errorf("cannot recognize --with clause format in %q", withClause) + } + + enumPath, enumValue := splitWithClause[0], splitWithClause[1] + enumPathSegments := strings.Split(enumPath, ".") + requirement := &schema.EnumRequirement{PathSegments: enumPathSegments, Value: enumValue} + requirements = append(requirements, requirement) + } + return requirements, nil +} diff --git a/cmd/declarative/run/run.go b/cmd/declarative/run/run.go index 44e62f6c..d19d6e62 100644 --- a/cmd/declarative/run/run.go +++ b/cmd/declarative/run/run.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2024 The Falco Authors +// Copyright (C) 2025 The Falco Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -44,12 +44,19 @@ var ( // New creates a new run command. func New(commonConf *config.Config) *cobra.Command { + // Run command performs the same actions the test command performs, but skips outcomes verification. + testCmd := test.New(commonConf, true) + + // Initialize the run command using the test command Run function. c := &cobra.Command{ Use: "run", Short: longDescriptionHeading, Long: longDescription, DisableAutoGenTag: true, - Run: test.New(commonConf, true).Command.Run, + Run: testCmd.Command.Run, } + + // Set the run command's flag set to be equal to the set of flags the test command exports. + c.Flags().AddFlagSet(testCmd.Command.Flags()) return c } diff --git a/cmd/declarative/test/test.go b/cmd/declarative/test/test.go index 9937471c..1b0d7d39 100644 --- a/cmd/declarative/test/test.go +++ b/cmd/declarative/test/test.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2024 The Falco Authors +// Copyright (C) 2025 The Falco Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -82,7 +82,7 @@ const ( reportFormatText reportFormat = iota // reportFormatJSON specifies to format a tester report using a JSON encoding. reportFormatJSON - // reportFormatYAML specifies to format a tester report using a YAML text encoding. + // reportFormatYAML specifies to format a tester report using a YAML encoding. reportFormatYAML ) @@ -127,6 +127,11 @@ func New(commonConf *config.Config, skipOutcomesVerification bool) *CommandWrapp // initFlags initializes the provided command's flags. func (cw *CommandWrapper) initFlags(c *cobra.Command) { + // Initialize command's flags with the ones exported by the config. + cw.Config.InitCommandFlags(c) + + // The following flags are all associated with outcome verification, so early return if we are going to skip that + // step. if cw.skipOutcomeVerification { return } diff --git a/pkg/test/loader/schema/describe.go b/pkg/test/loader/schema/describe.go index 02b0169c..d6255ffa 100644 --- a/pkg/test/loader/schema/describe.go +++ b/pkg/test/loader/schema/describe.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2024 The Falco Authors +// Copyright (C) 2025 The Falco Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -34,54 +34,54 @@ type EnumRequirement struct { // NodeMetadata contains node metadata. type NodeMetadata struct { - Type string - IsBindOnly bool + Type string `yaml:"fieldType,omitempty"` + IsBindOnly bool `yaml:"isBindOnly,omitempty"` } // A Node represents a single schema element. type Node struct { // Name is the name of the node. - Name string + Name string `yaml:"name"` // Descriptions is the list of node's descriptions. A node can have multiple descriptions if it is the result of the // merging of multiple nodes. - Descriptions []string + Descriptions []string `yaml:"descriptions,omitempty"` // JSONTypes is the node's optional list of JSON types. - JSONTypes []string + JSONTypes []string `yaml:"types,omitempty"` // Required is true if the user is required to provide the current node in order to completely describe the // containing node. - Required bool + Required bool `yaml:"required,omitempty"` // Minimum is the minimum supported integer value (it can be different from nil only if JSONTypes contains the // integer type). - Minimum *float64 + Minimum *float64 `yaml:"minimum,omitempty"` // MinLength is the minimum string value length (it can be different from nil only if JSONTypes contains the string // type). - MinLength *int + MinLength *int `yaml:"minLength,omitempty"` // MinItems is the minimum number of node items (it can be different from nil only if JSONTypes contains the array // type). - MinItems *int + MinItems *int `yaml:"minItems,omitempty"` // MinProperties is the minimum number of properties the node can contain (it can be different from nil only if // JSONTypes contains the object type). - MinProperties *int + MinProperties *int `yaml:"minProperties,omitempty"` // Pattern is the regular expression the node is constrained to adhere (it can be different from nil only if // JSONTypes contains the string type). - Pattern *string + Pattern *string `yaml:"pattern,omitempty"` // Default is the node's optional default value. - Default *any + Default *any `yaml:"default,omitempty"` // Examples is the node's optional list of examples. - Examples []any + Examples []any `yaml:"examples,omitempty"` // Enum is the node's optional list of enumerated values that are allowed to be set as node's value. It is only // populated if the node represents an enumerated value. - Enum []any + Enum []any `yaml:"enum,omitempty"` // Children is the node's optional list of child nodes (a.k.a node's fields). - Children []*Node + Children []*Node `yaml:"fields,omitempty"` // PseudoChildren is the node's optional list of pseudo child nodes (a.k.a node's exposed fields). - PseudoChildren []*Node + PseudoChildren []*Node `yaml:"exposedFields,omitempty"` // Pseudo is true if the node is "pseudo" node. In the context of a node tree, a pseudo node is a node not present // in the original schema: it is added to augment the schema with additional information, such as node's additional // exposed fields. - Pseudo bool + Pseudo bool `yaml:"-"` // Metadata are the node's metadata. - Metadata *NodeMetadata + Metadata *NodeMetadata `yaml:",inline"` } // mergeMetadataSchema merges the provided metadata schema in the current node. @@ -301,27 +301,14 @@ func Describe(nodePath []string, requirements []*EnumRequirement) (*Node, error) if err != nil { return nil, fmt.Errorf("error extracting node tree: %w", err) } - printNode("", root) if err := root.pruneChildren(nodePath); err != nil { return nil, fmt.Errorf("error pruning node tree: %w", err) } - printNode("", root) return root, nil } -func printNode(space string, n *Node) { - if n == nil { - return - } - - fmt.Printf("%s%s\n", space, n.Name) - for _, child := range n.Children { - printNode(space+" ", child) - } -} - const metadataPropName = "x-metadata" // extractNode is the main node tree extraction function. It recursively calls itself to build the node tree