Skip to content

Commit

Permalink
Change API to satisfies(SPDX expression, array of licenses)
Browse files Browse the repository at this point in the history
The design decision to take the argument expressing acceptable license
policy as another SPDX expression has repeatedly confused people.
Meanwhile, the primary use case for this package is to check some
SPDX license expression for a package against a list of approved
licenses.  I believe we can better serve that use case and make this
package easier to maintain by taking a list of approved licenses instead
of a second SPDX expression.
  • Loading branch information
kemitchell committed Sep 21, 2023
1 parent bb284e1 commit 01fc32b
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 63 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
README.md
23 changes: 23 additions & 0 deletions README.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
`satisfies(SPDX license expression, array of approved licenses)`

Approved licenses may be simple license identifiers like `MIT`, plus-ranges like `EPL-2.0+`, or licenses with exceptions like `Apache-2.0 WITH LLVM`. They may _not_ be compound expressions using `AND` or `OR`.

```javascript
var assert = require('assert')
var satisfies = require('spdx-satisfies')

// True Examples
for (const [spdxExpression, arrayOfApproved] of [
<%- returnTrue.map(function (e) { return ' ' + JSON.stringify(e) }).join(',\n') %>
]) assert(satisfies(spdxExpression, arrayOfApproved))

// False Examples
for (const [spdxExpression, arrayOfApproved] of [
<%- returnFalse.map(function (e) { return ' ' + JSON.stringify(e) }).join(',\n') %>
]) assert(!satisfies(spdxExpression, arrayOfApproved))

// Exceptions
for (const [spdxExpression, arrayOfApproved] of [
<%- throwErrors.map(function (e) { return ' ' + JSON.stringify(e) }).join(',\n') %>
]) assert.throws(function () { satisfies(spdxExpression, arrayOfApproved) })
```
48 changes: 0 additions & 48 deletions README.md

This file was deleted.

34 changes: 34 additions & 0 deletions examples.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"returnTrue": [
["MIT", ["MIT"]],
["MIT", ["ISC", "MIT"]],
["Zlib", ["ISC", "MIT", "Zlib"]],
["GPL-2.0", ["GPL-2.0+"]],
["GPL-3.0", ["GPL-2.0+"]],
["GPL-1.0+", ["GPL-2.0+"]],
["GPL-2.0-only", ["GPL-2.0-only"]],
["GPL-3.0-only", ["GPL-2.0+"]],
["LGPL-3.0-only", ["LGPL-3.0-or-later"]],
["GPL-2.0", ["GPL-2.0+"]],
["GPL-2.0-only", ["GPL-2.0+"]],
["GPL-2.0", ["GPL-2.0-or-later"]],
["GPL-3.0 WITH Bison-exception-2.2", ["GPL-2.0+ WITH Bison-exception-2.2"]],
["(MIT OR GPL-2.0)", ["ISC", "MIT"]],
["(MIT OR Apache-2.0) AND (ISC OR GPL-2.0)", ["Apache-2.0", "ISC"]],
["(MIT AND GPL-2.0)", ["MIT", "GPL-2.0"]]
],

"returnFalse": [
["GPL-3.0", ["ISC", "MIT"]],
["GPL-1.0", ["GPL-2.0+"]],
["GPL-2.0", ["GPL-2.0+ WITH Bison-exception-2.2"]],
["(MIT AND GPL-2.0)", ["ISC", "GPL-2.0"]],
["MIT AND (GPL-2.0 OR ISC)", ["MIT"]],
["(MIT OR Apache-2.0) AND (ISC OR GPL-2.0)", ["MIT"]]
],

"throwErrors": [
["MIT AND ISC", ["(MIT OR GPL-2.0) AND ISC"]],
["MIT AND ISC", ["(MIT AND GPL-2.0)", "ISC"]]
]
}
22 changes: 9 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,6 @@ function expand (expression) {
return sort(expandInner(expression))
}

// Flatten the given expression into an array of all licenses mentioned in the expression.
function flatten (expression) {
var expanded = expandInner(expression)
var flattened = expanded.reduce(function (result, clause) {
return Object.assign(result, clause)
}, {})
return sort([flattened])[0]
}

function expandInner (expression) {
if (!expression.conjunction) return [{ [licenseString(expression)]: expression }]
if (expression.conjunction === 'or') return expandInner(expression.left).concat(expandInner(expression.right))
Expand Down Expand Up @@ -129,10 +120,15 @@ function isANDCompatible (one, two) {
})
}

function satisfies (first, second) {
var one = expand(normalizeGPLIdentifiers(parse(first)))
var two = flatten(normalizeGPLIdentifiers(parse(second)))
return one.some(function (o) { return isANDCompatible(o, two) })
function satisfies (spdxExpression, arrayOfLicenses) {
var parsedExpression = expand(normalizeGPLIdentifiers(parse(spdxExpression)))
var parsedLicenses = arrayOfLicenses.map(function (l) { return normalizeGPLIdentifiers(parse(l)) })
for (const parsed of parsedLicenses) {
if (parsed.hasOwnProperty('conjunction')) {
throw new Error('Approved licenses cannot be AND or OR expressions.')
}
}
return parsedExpression.some(function (o) { return isANDCompatible(o, parsedLicenses) })
}

module.exports = satisfies
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
"dependencies": {
"spdx-compare": "^1.0.0",
"spdx-expression-parse": "^3.0.0",
"spdx-ranges": "^2.0.0"
"spdx-ranges": "^2.0.0",
"tape": "^5.6.6"
},
"devDependencies": {
"defence-cli": "^2.0.1",
"ejs": "^3.1.9",
"replace-require-self": "^1.1.1",
"standard": "^11.0.0"
},
Expand All @@ -34,7 +36,12 @@
"index.js"
],
"scripts": {
"test": "defence -i javascript README.md | replace-require-self | node",
"build": "ejs -f examples.json README.ejs > README.md",
"pretest": "npm run build",
"prepublish": "npm run build",
"test": "npm run test:suite && npm run test:readme",
"test:suite": "node test.js",
"test:readme": "defence -i javascript README.md | replace-require-self | node",
"lint": "standard"
}
}
30 changes: 30 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
var examples = require('./examples.json')
var satisfies = require('./')
var tape = require('tape')

function label(expression, approved) {
return JSON.stringify(expression) + ', ' + JSON.stringify(approved)
}

for (const [expression, approved] of examples.returnTrue) {
tape.test(label(expression, approved), t => {
t.assert(satisfies(expression, approved), 'returns true')
t.end()
})
}

// False Examples
for (const [expression, approved] of examples.returnFalse) {
tape.test(label(expression, approved), t => {
t.assert(!satisfies(expression, approved))
t.end()
})
}

// Invalid License Arrays
for (const [expression, approved] of examples.throwErrors) {
tape.test(label(expression, approved), t => {
t.throws(() => satisfies(expression, approved))
t.end()
})
}

0 comments on commit 01fc32b

Please sign in to comment.