Skip to content
Closed
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
2 changes: 1 addition & 1 deletion internal/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ const (

// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "7.0.1"
JSONSchemaVersion = "7.0.2"
)
153 changes: 153 additions & 0 deletions internal/logicalstrings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package internal

import (
"fmt"
"strings"

"github.com/invopop/jsonschema"
)

type Joiner string

const (
AND Joiner = "AND"
OR Joiner = "OR"
)

// LogicalStrings is a helper type for building logical expressions of strings, which can be combined
// in complex compound ways, with logical AND and OR. If no Joiner is provided, the default is AND.
type LogicalStrings struct {
Compound []LogicalStrings
Simple []string
Joiner
}

func (l LogicalStrings) Size() int {
return len(l.Compound) + len(l.Simple)
}

func (l LogicalStrings) String() string {
size := l.Size()
if size == 0 {
return ""
}
var parts []string
// first get the simple
parts = append(parts, l.Simple...)
// them get the complex
for _, e := range l.Compound {
s := e.String()
if e.Size() > 1 {
s = "(" + s + ")"
}
parts = append(parts, s)
}
joiner := l.Joiner
if joiner == "" {
joiner = AND
}
return strings.Join(parts, fmt.Sprintf(" %s ", joiner))
}

func (l LogicalStrings) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, l.String())), nil
}

func (l *LogicalStrings) UnmarshalJSON(data []byte) error {
raw := strings.Trim(string(data), `"`)
ls, err := ParseLogicalStrings(raw)
if err != nil {
return err
}
*l = ls
return nil
}

// Process processes each simple element inside the LogicalStrings through a provided function,
// returning a new LogicalStrings with the fields replaced.
func (l LogicalStrings) Process(f func(string) string) LogicalStrings {
var new LogicalStrings
for _, e := range l.Simple {
new.Simple = append(new.Simple, f(e))
}
for _, e := range l.Compound {
new.Compound = append(new.Compound, e.Process(f))
}
return new
}

// Elements returns all the elements of the LogicalStrings, the simple elements at the base of every compound.
func (l LogicalStrings) Elements() []string {
var elements []string
elements = append(elements, l.Simple...)
for _, e := range l.Compound {
elements = append(elements, e.Elements()...)
}
return elements
}

func (l LogicalStrings) JSONSchema() *jsonschema.Schema {
return &jsonschema.Schema{
Type: "string",
Title: "Logical Strings",
Description: "strings with simple or complex logical combinations",
}
}

// ParseLogicalStrings parse strings joined by AND or OR, as well as compounded by ( and ), into a LogicalStrings struct
func ParseLogicalStrings(s string) (LogicalStrings, error) {
var (
currentExpression string
expressionStack []string
currentLS LogicalStrings
lsStack []LogicalStrings
)

for _, c := range s {
switch c {
case '(':
expressionStack = append(expressionStack, currentExpression)
currentExpression = ""
lsStack = append(lsStack, currentLS)
currentLS = LogicalStrings{}
case ')':
simple, joiner := parseSimpleExpression(currentExpression)
currentLS.Simple = append(currentLS.Simple, simple...)
currentLS.Joiner = joiner
if len(expressionStack) == 0 {
return LogicalStrings{}, fmt.Errorf("unbalanced parentheses")
}
currentExpression = expressionStack[len(expressionStack)-1]
expressionStack = expressionStack[:len(expressionStack)-1]
lastLS := lsStack[len(lsStack)-1]
lastLS.Compound = append(lastLS.Compound, currentLS)
lsStack = lsStack[:len(lsStack)-1]
currentLS = lastLS
default:
currentExpression += string(c)
}
}
if currentExpression != "" {
simple, joiner := parseSimpleExpression(currentExpression)
if len(simple) > 0 {
currentLS.Simple = append(currentLS.Simple, simple...)
}
currentLS.Joiner = joiner
}
return currentLS, nil
}

func parseSimpleExpression(s string) ([]string, Joiner) {
var (
elements []string
joiner Joiner
)
for _, e := range strings.Fields(s) {
if e == "AND" || e == "OR" {
joiner = Joiner(e)
} else {
elements = append(elements, e)
}
}
return elements, joiner
}
62 changes: 62 additions & 0 deletions internal/logicalstrings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package internal

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestLogicalStrings(t *testing.T) {
tests := []struct {
elm LogicalStrings
expected string
}{
{LogicalStrings{Simple: []string{"a"}}, "a"},
{LogicalStrings{Simple: []string{"a", "b"}}, "a AND b"},
{LogicalStrings{Simple: []string{"a", "b"}, Joiner: AND}, "a AND b"},
{LogicalStrings{Simple: []string{"a", "b", "c"}, Joiner: OR}, "a OR b OR c"},
{LogicalStrings{
Compound: []LogicalStrings{
{Simple: []string{"a", "b"}, Joiner: OR},
{Simple: []string{"c", "d"}, Joiner: OR},
},
Joiner: AND,
}, "(a OR b) AND (c OR d)"},
}
for _, test := range tests {
t.Run(test.expected, func(t *testing.T) {
assert.Equal(t, test.expected, test.elm.String())
})
}
}

func TestParseLogicalStrings(t *testing.T) {
tests := []struct {
input string
expected LogicalStrings
}{
{"a", LogicalStrings{Simple: []string{"a"}}},
{"a AND b", LogicalStrings{Simple: []string{"a", "b"}, Joiner: AND}},
{"a OR b", LogicalStrings{Simple: []string{"a", "b"}, Joiner: OR}},
{"a AND (b OR c)", LogicalStrings{Simple: []string{"a"}, Joiner: AND, Compound: []LogicalStrings{
{Simple: []string{"b", "c"}, Joiner: OR},
}}},
{"(a AND b) OR (c AND d)", LogicalStrings{Joiner: OR, Compound: []LogicalStrings{
{Simple: []string{"a", "b"}, Joiner: AND},
{Simple: []string{"c", "d"}, Joiner: AND},
}}},
{"(a AND b) OR (c AND (d OR e))", LogicalStrings{Joiner: OR, Compound: []LogicalStrings{
{Simple: []string{"a", "b"}, Joiner: AND},
{Simple: []string{"c"}, Compound: []LogicalStrings{
{Simple: []string{"d", "e"}, Joiner: OR},
}, Joiner: AND},
}}},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
actual, err := ParseLogicalStrings(test.input)
assert.NoError(t, err)
assert.Equal(t, test.expected, actual)
})
}
}
2 changes: 1 addition & 1 deletion internal/spdxlicense/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const (
func ID(id string) (value, other string, exists bool) {
id = strings.TrimSpace(id)
// ignore blank strings or the joiner
if id == "" || id == "AND" {
if id == "" || id == "AND" || id == "OR" {
return "", "", false
}
// first look for a canonical license
Expand Down
6 changes: 6 additions & 0 deletions internal/spdxlicense/license_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ func TestIDParse(t *testing.T) {
"",
false,
},
{
"OR",
"",
"",
false,
},
}

for _, test := range tests {
Expand Down
57 changes: 16 additions & 41 deletions schema/json/schema-7.0.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,16 +202,21 @@
},
"BinaryMetadata": {
"properties": {
"matches": {
"items": {
"$ref": "#/$defs/ClassifierMatch"
},
"type": "array"
"classifier": {
"type": "string"
},
"realPath": {
"type": "string"
},
"virtualPath": {
"type": "string"
}
},
"type": "object",
"required": [
"matches"
"classifier",
"realPath",
"virtualPath"
]
},
"CargoPackageMetadata": {
Expand Down Expand Up @@ -244,21 +249,6 @@
"dependencies"
]
},
"ClassifierMatch": {
"properties": {
"classifier": {
"type": "string"
},
"location": {
"$ref": "#/$defs/Location"
}
},
"type": "object",
"required": [
"classifier",
"location"
]
},
"CocoapodsMetadata": {
"properties": {
"checksum": {
Expand Down Expand Up @@ -803,22 +793,10 @@
},
"type": "object"
},
"Location": {
"properties": {
"path": {
"type": "string"
},
"layerID": {
"type": "string"
},
"virtualPath": {
"type": "string"
}
},
"type": "object",
"required": [
"path"
]
"LogicalStrings": {
"type": "string",
"title": "Logical Strings",
"description": "strings with simple or complex logical combinations"
},
"MixLockMetadata": {
"properties": {
Expand Down Expand Up @@ -930,10 +908,7 @@
"type": "array"
},
"licenses": {
"items": {
"type": "string"
},
"type": "array"
"$ref": "#/$defs/LogicalStrings"
},
"language": {
"type": "string"
Expand Down
Loading