pkg/log/structure/structuredata/structure.go (137 lines of code) (raw):

// Copyright 2024 Google LLC // // 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 structuredata import ( "fmt" "strconv" "gopkg.in/yaml.v3" ) type StructuredDataFieldType string const ( StructuredTypeMap StructuredDataFieldType = "map" StructuredTypeArray StructuredDataFieldType = "array" StructuredTypeScalar StructuredDataFieldType = "scalar" StructuredTypeInvalid StructuredDataFieldType = "invalid" ) // Represents a structured data. For example, the data represented as YAML string. // This provides low level API for reading the structure data in KHI. type StructureData interface { Type() (StructuredDataFieldType, error) Keys() ([]string, error) Value(fieldName string) (any, error) } type YamlNodeStructuredData struct { Node *yaml.Node } // DataFromYaml generates the StructureData from string YAML data. func DataFromYaml(yamlString string) (StructureData, error) { var rawNode yaml.Node err := yaml.Unmarshal([]byte(yamlString), &rawNode) if err != nil { return nil, fmt.Errorf("failed to parse yaml\nYAML:%s\nERROR:%s", yamlString, err) } return DataFromYamlNode(&rawNode) } // DataFromYamlNode generates the StructureData from the raw yaml.Node pointer. func DataFromYamlNode(node *yaml.Node) (StructureData, error) { if node.Kind == 0 { // Errornous YAML maybe parsed from empty string // Emptry string must be mapped to an empty map return &YamlNodeStructuredData{ Node: &yaml.Node{ Kind: yaml.MappingNode, Content: make([]*yaml.Node, 0), }}, nil } if node.Kind == yaml.DocumentNode { if len(node.Content) > 1 { return nil, fmt.Errorf("structured data is not supporting multiple document") } node = node.Content[0] } return &YamlNodeStructuredData{Node: node}, nil } // Keys implements StrucureData. func (y *YamlNodeStructuredData) Keys() ([]string, error) { t, err := y.Type() if err != nil { return nil, err } switch t { case StructuredTypeMap: if len(y.Node.Content)%2 == 1 { return nil, fmt.Errorf("map node content count must be even") } keys := []string{} for i := 0; i < len(y.Node.Content); i += 2 { keyValue := y.Node.Content[i] keys = append(keys, keyValue.Value) } return keys, nil case StructuredTypeArray: return stringSequence(len(y.Node.Content)), nil default: return []string{""}, nil } } // Type implements StrucureData. func (y *YamlNodeStructuredData) Type() (StructuredDataFieldType, error) { switch y.Node.Kind { case yaml.MappingNode: return StructuredTypeMap, nil case yaml.SequenceNode: return StructuredTypeArray, nil case yaml.ScalarNode: return StructuredTypeScalar, nil default: return StructuredTypeInvalid, fmt.Errorf("node kind %d is not a supported type", y.Node.Kind) } } // Value implements StrucureData. func (y *YamlNodeStructuredData) Value(fieldName string) (any, error) { t, err := y.Type() if err != nil { return nil, err } switch t { case StructuredTypeScalar: if fieldName == "" { var data any err := y.Node.Decode(&data) if err != nil { return nil, err } return data, nil } else { return nil, fmt.Errorf("attempted to read a field %s from a scalar", fieldName) } case StructuredTypeMap: if fieldName == "" { return y, nil } if len(y.Node.Content)%2 == 1 { return nil, fmt.Errorf("map node content count must be even") } for i := 0; i < len(y.Node.Content); i += 2 { keyValue := y.Node.Content[i] if keyValue.Value == fieldName { return &YamlNodeStructuredData{Node: y.Node.Content[i+1]}, nil } } return nil, fmt.Errorf("field not found:%s", fieldName) case StructuredTypeArray: if fieldName == "" { return y, nil } index, err := strconv.Atoi(fieldName) if err != nil { return nil, fmt.Errorf("index can't be parsed as an integer") } if index < 0 { return nil, fmt.Errorf("index must not be negative") } if index >= len(y.Node.Content) { return nil, fmt.Errorf("index out of bounds") } return &YamlNodeStructuredData{Node: y.Node.Content[index]}, nil default: return nil, fmt.Errorf("unsupported strcuture type %s", t) } } var _ StructureData = (*YamlNodeStructuredData)(nil) func stringSequence(length int) []string { result := []string{} for i := 0; i < length; i++ { result = append(result, strconv.Itoa(i)) } return result }