diff --git a/README.md b/README.md index a10b2b5..093b1a4 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,30 @@ assert.True(valid) assert.NotContains(invalidLicenses, "MIT AND APACHE-2.0") ``` +### ExtractLicenses + +```go +func ExtractLicenses(expression string) ([]string, error) +``` + +Function `ExtractLicenses` is used to extract licenses from the given expression without duplicates. + +**parameter: expression** + +`expression` is an SPDX expression string. + +**returns** + +Function `ExtractLicenses` has 2 return values. First is `[]string` which contains all of the SPDX licenses without duplicates. + +The second return value is an `error` which is not `nil` if the given expression is not a valid SPDX expression. + +#### Example + +```go +licenses, err := ExtractLicenses("(MIT AND APACHE-2.0) OR (APACHE-2.0)") +assert.Equal(licenses, []string{"MIT", "Apache-2.0"}) +``` ## Background diff --git a/spdxexp/extracts.go b/spdxexp/extracts.go new file mode 100644 index 0000000..55afff3 --- /dev/null +++ b/spdxexp/extracts.go @@ -0,0 +1,21 @@ +package spdxexp + +// ExtractLicenses extracts licenses from the given expression without duplicates. +// Returns an array of licenses or error if error occurs during processing. +func ExtractLicenses(expression string) ([]string, error) { + node, err := parse(expression) + if err != nil { + return nil, err + } + + expanded := node.expand(true) + licenses := make([]string, 0) + allLicenses := flatten(expanded) + for _, licenseNode := range allLicenses { + licenses = append(licenses, *licenseNode.reconstructedLicenseString()) + } + + licenses = removeDuplicateStrings(licenses) + + return licenses, nil +} diff --git a/spdxexp/extracts_test.go b/spdxexp/extracts_test.go new file mode 100644 index 0000000..d9e1b67 --- /dev/null +++ b/spdxexp/extracts_test.go @@ -0,0 +1,34 @@ +package spdxexp + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExtractLicenses(t *testing.T) { + tests := []struct { + name string + inputExpression string + extractedLicenses []string + }{ + {"Single license", "MIT", []string{"MIT"}}, + {"AND'ed licenses", "MIT AND Apache-2.0", []string{"MIT", "Apache-2.0"}}, + {"AND'ed & OR'ed licenses", "(MIT AND Apache-2.0) OR GPL-3.0", []string{"GPL-3.0", "MIT", "Apache-2.0"}}, + {"ONLY modifiers", "LGPL-2.1-only OR MIT OR BSD-3-Clause", []string{"MIT", "BSD-3-Clause", "LGPL-2.1-only"}}, + {"WITH modifiers", "GPL-2.0-or-later WITH Bison-exception-2.2", []string{"GPL-2.0-or-later+ WITH Bison-exception-2.2"}}, + {"Invalid SPDX expression", "MIT OR INVALID", nil}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + licenses, err := ExtractLicenses(test.inputExpression) + assert.ElementsMatch(t, test.extractedLicenses, licenses) + if test.extractedLicenses == nil { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/spdxexp/helpers.go b/spdxexp/helpers.go new file mode 100644 index 0000000..8ad73df --- /dev/null +++ b/spdxexp/helpers.go @@ -0,0 +1,24 @@ +package spdxexp + +// flatten will take an array of nested array and return +// all nested elements in an array. e.g. [[1,2,[3]],4] -> [1,2,3,4] +func flatten[T any](lists [][]T) []T { + var res []T + for _, list := range lists { + res = append(res, list...) + } + return res +} + +// removeDuplicateStrings will remove all duplicates from a slice +func removeDuplicateStrings(sliceList []string) []string { + allKeys := make(map[string]bool) + list := []string{} + for _, item := range sliceList { + if _, value := allKeys[item]; !value { + allKeys[item] = true + list = append(list, item) + } + } + return list +}