Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: provide methods shared with score-compose for score implementation #32

Merged
merged 1 commit into from
Apr 3, 2024
Merged
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
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# score-go

Reference library for the parsing and loading SCORE files in Go.
Reference library containing common types and functions for building Score implementations in Go.

This can be added to your project via:

Expand All @@ -11,6 +11,13 @@ go get -u github.com/score-spec/score-go@latest
**NOTE**: if you project is still using the hand-written types, you will need to stay on `github.com/score-spec/score-go@v0.0.1`
and any important fixes to the schema may be back-ported to that branch.

## Packages

- `github.com/score-spec/score-go/schema` - Go constant with the json schema, and methods for validating a json or yaml structure against the schema.
- `github.com/score-spec/score-go/types` - Go types for Score workloads, services, and resources generated from the json schema.
- `github.com/score-spec/score-go/loader` - Go functions for loading the validated json or yaml structure into a workload struct.
- `github.com/score-spec/score-go/framework` - Common types and functions for Score implementations.

## Parsing SCORE files

This library includes a few utility methods to parse source SCORE files.
Expand All @@ -19,9 +26,11 @@ This library includes a few utility methods to parse source SCORE files.
import (
"os"

"github.com/score-spec/score-go/loader"
"github.com/score-spec/score-go/schema"
score "github.com/score-spec/score-go/types"
"gopkg.in/yaml.v3"

scoreloader "github.com/score-spec/score-go/loader"
scoreschema "github.com/score-spec/score-go/schema"
scoretypes "github.com/score-spec/score-go/types"
)

func main() {
Expand All @@ -32,20 +41,20 @@ func main() {
defer src.Close()

var srcMap map[string]interface{}
if err := loader.ParseYAML(&srcMap, src); err != nil {
if err := yaml.NewDecoder(src).Decode(&srcMap); err != nil {
panic(err)
}

if err := schema.Validate(srcMap); err != nil {
if err := scoreschema.Validate(srcMap); err != nil {
panic(err)
}

var spec score.Workload
if err := loader.MapSpec(&spec, srcMap); err != nil {
var spec scoretypes.Workload
if err := scoreloader.MapSpec(&spec, srcMap); err != nil {
panic(err)
}

if err := loader.Normalize(&spec, "."); err != nil {
if err := scoreloader.Normalize(&spec, "."); err != nil {
panic(err)
}

Expand All @@ -54,6 +63,10 @@ func main() {
}
```

## Building a Score implementation

[score-compose](https://github.com/score-spec/score-compose) is the reference Score implementation written in Go and using this library. If you'd like to write a custom Score implementation, use the functions in this library and the `score-compose` implementation as a Guide.

## Upgrading the schema version

When the Score JSON schema is updated in <https://github.com/score-spec/schema>, this repo should be updated to match.
Expand Down
166 changes: 166 additions & 0 deletions framework/override_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright 2024 Humanitec
//
// 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 framework

import (
"fmt"
"maps"
"slices"
"strconv"
"strings"
)

// ParseDotPathParts will parse a common .-separated override path into path elements to traverse.
func ParseDotPathParts(input string) []string {
// support escaping dot's to insert elements with a . in them.
input = strings.ReplaceAll(input, "\\\\", "\x01")
input = strings.ReplaceAll(input, "\\.", "\x00")
parts := strings.Split(input, ".")
for i, part := range parts {
part = strings.ReplaceAll(part, "\x00", ".")
part = strings.ReplaceAll(part, "\x01", "\\")
parts[i] = part
}
return parts
}

// OverrideMapInMap will take in a decoded json or yaml struct and merge an override map into it. Any maps are merged
// together, other value types are replaced. Nil values will delete overridden keys or otherwise are ignored. This
// returns a shallow copy of the map in a copy-on-write way, so only modified elements are copied.
func OverrideMapInMap(input map[string]interface{}, overrides map[string]interface{}) (map[string]interface{}, error) {
output := maps.Clone(input)
for key, value := range overrides {
if value == nil {
delete(output, key)
continue
}

existing, hasExisting := output[key]
if !hasExisting {
output[key] = value
continue
}

eMap, isEMap := existing.(map[string]interface{})
vMap, isVMap := value.(map[string]interface{})
if isEMap && isVMap {
output[key], _ = OverrideMapInMap(eMap, vMap)
} else {
output[key] = value
}
}
return output, nil
}

// OverridePathInMap will take in a decoded json or yaml struct and override a particular path within it with either
// a new value or deletes it. This returns a shallow copy of the map in a copy-on-write way, so only modified elements
// are copied.
func OverridePathInMap(input map[string]interface{}, path []string, isDelete bool, value interface{}) (map[string]interface{}, error) {
return overridePathInMap(input, path, isDelete, value)
}

func overridePathInMap(input map[string]interface{}, path []string, isDelete bool, value interface{}) (map[string]interface{}, error) {
if len(path) == 0 {
return nil, fmt.Errorf("cannot change root node")
}

output := maps.Clone(input)
if len(path) == 1 {
if isDelete || value == nil {
delete(output, path[0])
} else {
output[path[0]] = value
}
return output, nil
}

if _, ok := output[path[0]]; !ok {
next := make(map[string]interface{})
subOutput, err := overridePathInMap(next, path[1:], isDelete, value)
if err != nil {
return nil, fmt.Errorf("%s: %w", path[0], err)
}
output[path[0]] = subOutput
return output, nil
}

switch typed := output[path[0]].(type) {
case map[string]interface{}:
subOutput, err := overridePathInMap(typed, path[1:], isDelete, value)
if err != nil {
return nil, fmt.Errorf("%s: %w", path[0], err)
}
output[path[0]] = subOutput
return output, nil
case []interface{}:
subOutput, err := overridePathInArray(typed, path[1:], isDelete, value)
if err != nil {
return nil, fmt.Errorf("%s: %w", path[0], err)
}
output[path[0]] = subOutput
return output, nil
default:
return nil, fmt.Errorf("%s: cannot set path in non-map/non-array", path[0])
}
}

func overridePathInArray(input []interface{}, path []string, isDelete bool, value interface{}) ([]interface{}, error) {
if len(path) == 0 {
return nil, fmt.Errorf("cannot change root node")
}

pathIndex, err := strconv.Atoi(path[0])
if err != nil {
return nil, fmt.Errorf("failed to parse '%s' as array index", path[0])
}

output := slices.Clone(input)
if len(path) == 1 {
if isDelete || value == nil {
if pathIndex < 0 || pathIndex >= len(input) {
return nil, fmt.Errorf("cannot remove '%d' in array: out of range", pathIndex)
}
return slices.Delete(output, pathIndex, pathIndex+1), nil
}
if pathIndex == -1 {
output = append(output, value)
return output, nil
}
if pathIndex < 0 || pathIndex >= len(input) {
return nil, fmt.Errorf("cannot set '%d' in array: out of range", pathIndex)
}
output[pathIndex] = value
return output, nil
}

switch typed := output[pathIndex].(type) {
case map[string]interface{}:
subOutput, err := overridePathInMap(typed, path[1:], isDelete, value)
if err != nil {
return nil, fmt.Errorf("%s: %w", path[0], err)
}
output[pathIndex] = subOutput
return output, nil
case []interface{}:
subOutput, err := overridePathInArray(typed, path[1:], isDelete, value)
if err != nil {
return nil, fmt.Errorf("%s: %w", path[0], err)
}
output[pathIndex] = subOutput
return output, nil
default:
return nil, fmt.Errorf("%s: cannot set path in non-map/non-array", path[0])
}
}
Loading
Loading