docs: Remove live blocks (#7572)
These are no longer being used and are dependent on an old version of node to work. I have disabled the generation to allow the new docs site to be built. I thought it made sense to remove all things relating to live blocks since we have no plans to support them now anyway. Signed-off-by: Charlie Egan <charlie@styra.com>
@@ -466,7 +466,7 @@ endif
|
||||
netlify-prod: clean docs-clean build docs-production-build docs-new-site
|
||||
|
||||
.PHONY: netlify-preview
|
||||
netlify-preview: clean docs-clean build docs-live-blocks-install-deps docs-dev-generate docs-preview-build docs-new-site
|
||||
netlify-preview: clean docs-clean build docs-dev-generate docs-preview-build docs-new-site
|
||||
|
||||
# Kept for compatibility. Use `make fuzz` instead.
|
||||
.PHONY: check-fuzz
|
||||
|
||||
@@ -13,7 +13,6 @@ clean:
|
||||
rm -rf $(CURDIR)/website/generated
|
||||
rm -rf $(CURDIR)/website/public
|
||||
rm -rf $(CURDIR)/website/resources
|
||||
rm -rf $(CURDIR)/website/scripts/live-blocks/opa_versions/latest-*
|
||||
|
||||
.PHONY: generate-cli-docs
|
||||
generate-cli-docs:
|
||||
@@ -32,20 +31,6 @@ copy-hugo-static-content:
|
||||
dev-generate: generate-cli-docs copy-hugo-static-content
|
||||
DEV=true $(CURDIR)/website/scripts/load-docs.sh
|
||||
|
||||
# The website has some npm dependencies saved in ./website/node_modules
|
||||
# As well as dependencies required for the "live-blocks".
|
||||
# Use this target to update them as needed.
|
||||
.PHONY: install-deps
|
||||
install-deps: live-blocks-install-deps
|
||||
npm install
|
||||
|
||||
# The live-blocks-% pattern target will shim to the
|
||||
# npm scripts in ./website/scripts/live-blocks
|
||||
# NOTE(sr): we'll avoid an API call if we make use of releases.yaml to determine the latest release
|
||||
.PHONY: live-blocks-%
|
||||
live-blocks-%:
|
||||
cd $(CURDIR)/website/scripts/live-blocks && LATEST=$(shell sed -n '2s/- //p' $(CURDIR)/website/data/releases.yaml) npm run $*
|
||||
|
||||
.PHONY: serve-local
|
||||
serve-local: dev-build
|
||||
# must be run from root of repo for
|
||||
@@ -59,7 +44,7 @@ serve-remote: production-build
|
||||
cd $(CURDIR)/.. && netlify deploy
|
||||
|
||||
.PHONY: dev-build
|
||||
dev-build: clean dev-generate hugo-production-build live-blocks-inject
|
||||
dev-build: clean dev-generate hugo-production-build
|
||||
|
||||
######################################################
|
||||
#
|
||||
@@ -77,7 +62,6 @@ hugo-production-build:
|
||||
|
||||
.PHONY: production-build
|
||||
production-build: clean generate hugo-production-build
|
||||
make live-blocks-inject
|
||||
|
||||
.PHONY: preview-build
|
||||
preview-build:
|
||||
@@ -88,8 +72,6 @@ preview-build:
|
||||
--buildDrafts \
|
||||
--buildFuture \
|
||||
--ignoreCache
|
||||
# this is not compatible with node 22
|
||||
# make live-blocks-inject
|
||||
|
||||
.PHONY: new-site
|
||||
new-site:
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
dist/
|
||||
node_modules/
|
||||
@@ -1,180 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:import/errors"
|
||||
],
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"import",
|
||||
"sort-imports-es6-autofix",
|
||||
"sort-requires"
|
||||
],
|
||||
"rules": {
|
||||
"array-bracket-spacing": "error",
|
||||
"arrow-parens": "error",
|
||||
"arrow-spacing": [
|
||||
"error",
|
||||
{
|
||||
"before": true,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"brace-style": "error",
|
||||
"camelcase": [
|
||||
"error",
|
||||
{
|
||||
"ignoreDestructuring": true,
|
||||
"properties": "never"
|
||||
}
|
||||
],
|
||||
"comma-spacing": "error",
|
||||
"comma-style": "error",
|
||||
"computed-property-spacing": "error",
|
||||
"curly": "error",
|
||||
"dot-location": [
|
||||
"error",
|
||||
"property"
|
||||
],
|
||||
"dot-notation": "error",
|
||||
"func-call-spacing": "error",
|
||||
"generator-star-spacing": [
|
||||
"error",
|
||||
{
|
||||
"after": true,
|
||||
"before": false,
|
||||
"method": {
|
||||
"after": true,
|
||||
"before": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"import/namespace": [
|
||||
"error",
|
||||
{
|
||||
"allowComputed": true
|
||||
}
|
||||
],
|
||||
"import/no-unresolved": [
|
||||
"error",
|
||||
{
|
||||
"ignore": [
|
||||
"src/spritesheet\\.svg"
|
||||
]
|
||||
}
|
||||
],
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"groups": [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index"
|
||||
],
|
||||
"newlines-between": "always-and-inside-groups"
|
||||
}
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"MemberExpression": "off",
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"jsx-quotes": [
|
||||
"error",
|
||||
"prefer-double"
|
||||
],
|
||||
"key-spacing": "error",
|
||||
"keyword-spacing": "error",
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"new-parens": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-else-return": "error",
|
||||
"no-multi-spaces": [
|
||||
"error",
|
||||
{
|
||||
"ignoreEOLComments": true
|
||||
}
|
||||
],
|
||||
"no-multiple-empty-lines": [
|
||||
"error",
|
||||
{
|
||||
"max": 1,
|
||||
"maxBOF": 0
|
||||
}
|
||||
],
|
||||
"no-new-wrappers": "error",
|
||||
"no-template-curly-in-string": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"varsIgnorePattern": "^_+$"
|
||||
}
|
||||
],
|
||||
"no-whitespace-before-property": "error",
|
||||
"object-curly-spacing": "error",
|
||||
"prefer-arrow-callback": "error",
|
||||
"quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"rest-spread-spacing": "error",
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"sort-imports-es6-autofix/sort-imports-es6": [
|
||||
"error",
|
||||
{
|
||||
"ignoreCase": true,
|
||||
"ignoreMemberSort": false,
|
||||
"memberSyntaxSortOrder": [
|
||||
"none",
|
||||
"single",
|
||||
"all",
|
||||
"multiple"
|
||||
]
|
||||
}
|
||||
],
|
||||
"sort-requires/sort-requires": "error",
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
"anonymous": "always",
|
||||
"asyncArrow": "always",
|
||||
"named": "never"
|
||||
}
|
||||
],
|
||||
"space-in-parens": "error",
|
||||
"space-infix-ops": "error",
|
||||
"space-unary-ops": "error",
|
||||
"spaced-comment": "error",
|
||||
"switch-colon-spacing": "error",
|
||||
"template-curly-spacing": "error",
|
||||
"template-tag-spacing": "error",
|
||||
"yield-star-spacing": "error"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
opa_versions/
|
||||
@@ -1,144 +0,0 @@
|
||||
# Live Blocks
|
||||
|
||||
This is the high-level code documentation for the live blocks postprocessor.
|
||||
Whoever you are, you should probably read the "Live Code Blocks" section of
|
||||
`docs/README.md` first (it should be all you need if you just want to use
|
||||
live blocks when writing docs).
|
||||
|
||||
`scripts/live-blocks/` acts as a completely standalone postprocessor, editing and
|
||||
adding files in `docs/website/public` after the rest of a complete
|
||||
build has finished and then hydrating the code blocks
|
||||
on page load.
|
||||
|
||||
## Scripts
|
||||
`package.json` contains a bunch of scripts, in some cases it calls things in the `live-blocks/scripts/` folder.
|
||||
The most important of these is `inject` which calls a bunch of other scripts to add live docs functionality
|
||||
to an otherwise fully built site. All of these scripts can be run from the `docs/` folder using `make live-blocks-<script name>`
|
||||
or from the root of this repository using `make docs-live-blocks-<script name>`.
|
||||
|
||||
You probably want to run `install-deps` to automatically install JS dependencies. When testing, you might want to use something like
|
||||
`make docs-live-blocks-clear-bundle-cache && make docs-serve` so that the bundles get re-built. If a cache gets corrupted, you can
|
||||
run `clear-caches` or the script for a specific one to delete it. Once you're done, you can run `check`
|
||||
to test and sort package.json.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
- `dist/` (gitignored) - A local cache of bundled JS and CSS (automatically populated by `bundle-web`).
|
||||
- `node_modules/` (gitignored) - A local cache of JS dependencies (automatically downloaded by `install-deps`).
|
||||
- `opa_versions/` (gitignored) - A local cache of OPA binaries used for evaluating groups
|
||||
(automatically downloaded by `preprocess`; edge is redownloaded occasionally to keep it up-to-date).
|
||||
- `scripts/` - Internal Bash and Node scripts to automate tasks. These should generally be called via `make` or `npm` as described above.
|
||||
- `src/` - The live blocks source code.
|
||||
- `preprocess/` - Code that processes the documentation HTML before hydration.
|
||||
This includes evaluating the initial contents of output blocks.
|
||||
- `index.js` - Entry point.
|
||||
- `web/` - Code that hydrates and styles the live blocks once loaded by a reader.
|
||||
This includes reevaluating the contents of output blocks when their dependencies get edited.
|
||||
- `index.js` - Entry point.
|
||||
- `constants.js` - Various shared or platform-agnostic constant values (and some logic).
|
||||
- `helpers.js` - Various shared or platform-agnostic functions, particularly related to parsing block labels,
|
||||
working with groups, and async operations.
|
||||
- `errors.js` - A couple of shared error classes.
|
||||
- `static/` - Static assets including the "Open In Playground" "working" page and icons.
|
||||
- `test/` - Unit tests.
|
||||
- `package.json` - NodeJS package file, including a few constants and scripts. All the scripts can be run via `make live-docs-` from the `docs/` folder.
|
||||
- `rollup.config.js` - Configuration for the JS/CSS bundler.
|
||||
|
||||
## `groups` Object
|
||||
|
||||
On both the preprocessing and web sides, a `groups` object is constructed with mappings from full group names to group objects.
|
||||
These group objects in turn map from block types to block objects. Between the two contexts these block objects have some matching and some additional fields,
|
||||
their structure is documented by the respective `index.js`' `constructGroups()` functions.
|
||||
|
||||
## Preprocessing
|
||||
|
||||
The preprocessing stage tries to validate as much as it can, and fail noisily with as many of the problems
|
||||
that need to be fixed as possible. Most errors will occur around either constructing the `groups` object or
|
||||
evaluating the output contents, both steps will report all the errors for all the code blocks. One of these
|
||||
sets of errors (or some other unexpected one) will be reported (if applicable) for every file. Note that,
|
||||
in order to not store them while waiting for the rest to finish, files that are processed successfully are
|
||||
written to disk even if others fail.
|
||||
|
||||
Preprocessing should happen as quickly as possible and so multiple files and/or group evaluations can be in the middle of processing,
|
||||
waiting on file operations simultaneously.
|
||||
|
||||
## Hydration
|
||||
|
||||
Attaching the CodeMirror (editor) instances is easily the longest operation when hydrating the page
|
||||
(on the order of 5-10 seconds for >100 live blocks).
|
||||
As such, hydration happens in a couple of passes:
|
||||
|
||||
1. The `groups` object is constructed.
|
||||
2. Dependencies are determined and change handlers are registered.
|
||||
3. The blocks are visibly hydrated based on how close they are to the viewport and the `groups` object
|
||||
is incrementally updated accordingly.
|
||||
|
||||
This allows almost immediate editing of visible live blocks by a reader without breaking (or leaving unupdated)
|
||||
any dependent blocks whose editors haven't been hydrated yet.
|
||||
|
||||
## DOM Structure
|
||||
|
||||
Hugo generates code blocks like this:
|
||||
|
||||
```html
|
||||
<div class="highlight">
|
||||
<pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
|
||||
<code class="language-python" data-lang="python">
|
||||
<!--text wrapped in spans to style them-->
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
```
|
||||
|
||||
The preprocessor changes the div's class, adds an icon bar, and removes any highlighting from the code block:
|
||||
|
||||
```html
|
||||
<div class="live--block-container">
|
||||
<div class="live--icon-bar--outer">
|
||||
<div class="live--icon-bar--inner">
|
||||
<div class="live--icon--container live--icon--container--indicator" title="Type">
|
||||
<img class="live--icon--img" src="/live-blocks/icons/type.svg">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
|
||||
<code class="language-python" data-lang="python">
|
||||
<!--text-->
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
```
|
||||
|
||||
On hydration, the rest of the Hugo-generated HTML is replaced by CodeMirror wrapped in a div:
|
||||
|
||||
```html
|
||||
<div class="live--block-container">
|
||||
<div class="live--icon-bar--outer">
|
||||
<div class="live--icon-bar--inner">
|
||||
<div class="live--icon--container live--icon--container--indicator" title="Type">
|
||||
<img class="live--icon--img" src="/live-blocks/icons/type.svg">
|
||||
</div>
|
||||
<!--potentially more icons (buttons)-->
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="CodeMirror cm-s-default">
|
||||
<!--codemirror's internal structure-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
The preprocessor handles all doc versions the same, downloading the correct version of OPA
|
||||
(based on the file's path) from either GitHub or S3 (the latter for current edge builds).
|
||||
|
||||
The web script always hydrates the blocks but only allows them to be edited if the page's path conforms
|
||||
with `INTERACTIVE_PATH` in `constants.js`. This restricts the live functionality to the current version of the
|
||||
Rego Playground.
|
||||
|
||||
## TODO
|
||||
|
||||
The `INTERACTIVE_PATH` should eventually only allow the current docs but currently it only allows edge.
|
||||
Once doc updates are backported or there's a new OPA release, this will need to be updated in `src/constants.js`.
|
||||
@@ -1,59 +0,0 @@
|
||||
{
|
||||
"name": "live-blocks",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"author": "",
|
||||
"browser": "dist/live-blocks.js",
|
||||
"scripts": {
|
||||
"bundle-web": "rollup -c",
|
||||
"check": "sort-package-json && npm run test",
|
||||
"clear-bundle-cache": "rm -rf ./dist",
|
||||
"clear-caches": "npm run clear-bundle-cache && npm run clear-opa-cache && npm run clear-node-cache",
|
||||
"clear-node-cache": "rm -rf ./node_modules",
|
||||
"clear-opa-cache": "rm -rf ./opa_versions",
|
||||
"inject": "./scripts/inject",
|
||||
"install-deps": "./scripts/install-deps",
|
||||
"preprocess": "node -r esm src/preprocess/index.js",
|
||||
"test": "node -r esm ./node_modules/.bin/mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"codemirror": "^5.58.2",
|
||||
"codemirror-rego": "^1.3.0",
|
||||
"esm": "^3.2.25",
|
||||
"node-fetch": "^2.6.7",
|
||||
"tmp": "^0.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.5.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.4",
|
||||
"@babel/plugin-transform-react-jsx": "^7.3.0",
|
||||
"@babel/plugin-transform-regenerator": "^7.4.5",
|
||||
"@babel/preset-env": "^7.5.2",
|
||||
"@babel/register": "^7.10.5",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^19.0.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||
"acorn": ">=6.4.1",
|
||||
"browserslist": ">=4.16.5",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"core-js": "^3.1.4",
|
||||
"dot-prop": ">=5.1.1",
|
||||
"glob-parent": ">=6.0.1",
|
||||
"minimist": ">=1.2.6",
|
||||
"mocha": "^9.2.2",
|
||||
"normalize-url": ">=4.5.1",
|
||||
"nth-check": ">=2.0.1",
|
||||
"postcss": "^8.4.31",
|
||||
"regenerator-runtime": "^0.13.2",
|
||||
"rollup": "^2.79.2",
|
||||
"rollup-plugin-postcss": "^4.0.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"serialize-javascript": ">=6.0.2",
|
||||
"sinon": "^11.1.1",
|
||||
"sort-package-json": "^1.22.1"
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import babel from '@rollup/plugin-babel'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import json from '@rollup/plugin-json'
|
||||
import postcss from 'rollup-plugin-postcss'
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import {terser} from 'rollup-plugin-terser'
|
||||
|
||||
import pkg from './package.json'
|
||||
|
||||
export default [
|
||||
// browser-friendly UMD build
|
||||
{
|
||||
input: 'src/web/index.js',
|
||||
output: {
|
||||
file: pkg.browser,
|
||||
format: 'umd',
|
||||
},
|
||||
plugins: [
|
||||
json(),
|
||||
resolve(),
|
||||
commonjs({
|
||||
include: ['node_modules/**'],
|
||||
sourceMap: false
|
||||
}),
|
||||
babel({// Taken from package.js, wasn't using plugins correctly otherwise
|
||||
'plugins': [
|
||||
[
|
||||
'@babel/plugin-proposal-decorators',
|
||||
{
|
||||
'legacy': true
|
||||
}
|
||||
],
|
||||
'@babel/plugin-transform-react-jsx',
|
||||
'@babel/plugin-transform-regenerator'
|
||||
],
|
||||
'presets': [
|
||||
'@babel/preset-env'
|
||||
],
|
||||
exclude: ['node_modules/**'],
|
||||
}),
|
||||
postcss({
|
||||
extract: true
|
||||
}),
|
||||
terser(),
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
# To be called from live-blocks/
|
||||
|
||||
# Install dependencies if needed
|
||||
npm run install-deps
|
||||
|
||||
# Bundle web js and css if needed
|
||||
npm run --silent bundle-web
|
||||
|
||||
# Copy everything over
|
||||
# NOTE: Update package.json, src/constants.js, and static/opening-in-playground.html with any changes.
|
||||
cp dist/live-blocks.js ../../public/js/ # Bundles
|
||||
cp dist/live-blocks.css ../../public/css/
|
||||
mkdir -p ../../public/live-blocks # Other assets
|
||||
cp -r static/* ../../public/live-blocks/
|
||||
|
||||
# Preprocess the docs
|
||||
find ../../public/ -name '*.html' -exec node -r esm src/preprocess/index.js {} +
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# To be called from live-blocks/
|
||||
|
||||
# Only run install if node_modules does not exist
|
||||
if ! [ -d "node_modules/" ]; then
|
||||
npm i
|
||||
else
|
||||
echo "NOT running npm install as node_modules directory already is present"
|
||||
fi
|
||||
@@ -1,209 +0,0 @@
|
||||
// --- CSS CLASSES ---
|
||||
// NOTE: Update style.css with any changes.
|
||||
const join = (...fragments) => fragments.join('--')
|
||||
const CLASS_PREFIX = 'live'
|
||||
const BLOCK_CONTAINER = join(CLASS_PREFIX, 'block-container')
|
||||
const ICON_BAR = join(CLASS_PREFIX, 'icon-bar')
|
||||
const ICON = join(CLASS_PREFIX, 'icon')
|
||||
const ICON_CONTAINER = join(ICON, 'container')
|
||||
const ICON_IMG = join(ICON, 'img')
|
||||
export const CLASSES = {
|
||||
BLOCK_CONTAINER,
|
||||
BLOCK_CONTAINER_EDITABLE: join(BLOCK_CONTAINER, 'editable'),
|
||||
BLOCK_CONTAINER_MERGED_DOWN: join(BLOCK_CONTAINER, 'merged-down'),
|
||||
BLOCK_CONTAINER_ERROR: join(BLOCK_CONTAINER, 'error'),
|
||||
HUGO_BLOCK_CONTAINER: 'highlight',
|
||||
ICON_BAR_OUTER: join(ICON_BAR, 'outer'),
|
||||
ICON_BAR_INNER: join(ICON_BAR, 'inner'),
|
||||
ICON_CONTAINER,
|
||||
ICON_CONTAINER_INDICATOR: join(ICON_CONTAINER, 'indicator'),
|
||||
ICON_CONTAINER_BUTTON: join(ICON_CONTAINER, 'button'),
|
||||
ICON_IMG,
|
||||
ICON_IMG_SPINNING: join(ICON_IMG, 'spinning')
|
||||
}
|
||||
|
||||
// --- LABELS ---
|
||||
// Prefix for "lang" labels for live code blocks
|
||||
export const LABEL_PREFIX = 'live:'
|
||||
|
||||
// Regular expressions for recognizing group names.
|
||||
const GROUP_NAME_FRAGMENT_REG_EXP = '\\w+'
|
||||
const GROUP_NAME_REG_EXP = `${GROUP_NAME_FRAGMENT_REG_EXP}(?:\\/${GROUP_NAME_FRAGMENT_REG_EXP})*`
|
||||
|
||||
// The types of live code blocks with their names as found in labels
|
||||
export const BLOCK_TYPES = {
|
||||
MODULE: 'module',
|
||||
QUERY: 'query',
|
||||
INPUT: 'input',
|
||||
OUTPUT: 'output',
|
||||
}
|
||||
|
||||
// The types of expected error tags that output blocks can have with their names as found in labels
|
||||
export const EXPECTED_ERROR_TAG_TYPES = {
|
||||
EXPECT_ERROR: 'expect_error',
|
||||
EXPECT_UNDEFINED: 'expect_undefined',
|
||||
// from 'ast'
|
||||
EXPECT_REGO_ERROR: 'expect_rego_error',
|
||||
// EXPECT_PARSE_ERROR: 'expect_parse_error', Not properly supported by the CLI.
|
||||
EXPECT_COMPILE_ERROR: 'expect_compile_error',
|
||||
EXPECT_REGO_TYPE_ERROR: 'expect_rego_type_error',
|
||||
EXPECT_UNSAFE_VAR: 'expect_unsafe_var',
|
||||
EXPECT_RECURSION: 'expect_recursion',
|
||||
// from 'topdown'
|
||||
EXPECT_EVAL_ERROR: 'expect_eval_error',
|
||||
EXPECT_CONFLICT: 'expect_conflict',
|
||||
EXPECT_EVAL_TYPE_ERROR: 'expect_eval_type_error',
|
||||
EXPECT_BUILTIN_ERROR: 'expect_builtin_error',
|
||||
EXPECT_WITH_MERGE_ERROR: 'expect_with_merge_error',
|
||||
// more specific
|
||||
EXPECT_ASSIGNED_ABOVE: 'expect_assigned_above',
|
||||
EXPECT_REFERENCED_ABOVE: 'expect_referenced_above',
|
||||
}
|
||||
|
||||
// The types of eval configuration tags that output blocks can have with a `match` string used to build LABEL_REG_EXP and an `exec` function used to extract the value.
|
||||
const config = (name, contents) => ({match: `${name}\\(${contents}\\)`, exec: (tag) => {
|
||||
const res = (new RegExp(`^${name}\\((${contents})\\)$`)).exec(tag)
|
||||
return res ? res[1] : res // get the captured value if it matches
|
||||
}})
|
||||
export const EVAL_CONFIG_TAG_TYPES = {
|
||||
INCLUDE: config('include', GROUP_NAME_REG_EXP),
|
||||
// NOTE: This could potentially include options for setting the query package/ imports.
|
||||
}
|
||||
|
||||
// The non-variable types of tags that blocks can have with their names as found in labels.
|
||||
export const STATIC_TAG_TYPES = {
|
||||
HIDDEN: 'hidden',
|
||||
READ_ONLY: 'read_only',
|
||||
MERGE_DOWN: 'merge_down',
|
||||
OPENABLE: 'openable',
|
||||
LINE_NUMBERS: 'line_numbers',
|
||||
...EXPECTED_ERROR_TAG_TYPES,
|
||||
}
|
||||
|
||||
// Regular expressions for breaking apart labels, contructing them this way makes it easier to only accept valid ones while only specifying things in one place.
|
||||
const BLOCK_REG_EXP = Object.values(BLOCK_TYPES).join('|')
|
||||
const TAG_REG_EXP = Object.values(STATIC_TAG_TYPES).concat(Object.values(EVAL_CONFIG_TAG_TYPES).map((cfg) => cfg.match)).join('|')
|
||||
export const LABEL_REG_EXP = new RegExp(`^${LABEL_PREFIX}(${GROUP_NAME_REG_EXP}):(${BLOCK_REG_EXP})(:(?:(?:${TAG_REG_EXP}),)*(?:${TAG_REG_EXP})|)$`)
|
||||
|
||||
// CSS-style selector for finding the live code blocks in a document
|
||||
export const BLOCK_SELECTOR = `code[data-lang^="${LABEL_PREFIX}"]`
|
||||
|
||||
// The name for the module file that gets evaluated (specifically shows up in error messages.)
|
||||
export const EVAL_MODULE_NAME = 'module.rego'
|
||||
|
||||
// --- PREPROCESSING ---
|
||||
// The maximum number of files to be processing at any given time. 0 indicates unlimited.
|
||||
export const MAX_CONCUR_FILES = 1
|
||||
// The maximum number of evaluations to be performing for a single processing file at any given time (net maximum is MAX_CONCUR_FILES * MAX_CONCUR_FILE_EVALS). 0 indicates unlimited.
|
||||
export const MAX_CONCUR_FILE_EVALS = 50
|
||||
|
||||
// The version string for edge (e.g. not 'x.x.x[-x]')
|
||||
export const VERSION_EDGE = 'edge'
|
||||
export const VERSION_LATEST = 'latest'
|
||||
|
||||
// Platforms that there are OPA releases for.
|
||||
export const PLATFORMS = {
|
||||
WINDOWS: 'windows',
|
||||
DARWIN: 'darwin',
|
||||
LINUX: 'linux'
|
||||
}
|
||||
|
||||
// The folder where OPA binaries will be downloaded into.
|
||||
// NOTE: Update package.json with any changes
|
||||
export const OPA_CACHE_PATH = './opa_versions/'
|
||||
export const OPA_EDGE_PATH = '../../../../' // Repo root
|
||||
|
||||
// The amount of time between redownloads of the edge release (ms).
|
||||
export const OPA_EDGE_CACHE_PERIOD = 24 * 60 * 60 * 1000 // 1 day
|
||||
|
||||
// The paths that the bundles will be accessible from.
|
||||
// NOTE: Update package.json and inject.sh with any changes.
|
||||
import pkg from '../package.json'
|
||||
export const JS_BUNDLE_PATH = pkg.browser.replace(/dist/, '/js')
|
||||
export const CSS_BUNDLE_BATH = JS_BUNDLE_PATH.replace(/js/g, 'css')
|
||||
|
||||
// --- UI ---
|
||||
// Blocks on pages that match this regex will be non interactive (for limiting live functionality to the version that the playground supports).
|
||||
export const INTERACTIVE_PATH = /^\/docs\/(latest|edge)/
|
||||
|
||||
// The path to initially open a new tab to with when opening a group in the playground
|
||||
export const OPENING_IN_PLAYGROUND_PATH = '/live-blocks/opening-in-playground'
|
||||
|
||||
// How many blocks to hydrate before re-sorting the queue. Lower numbers ensure quick hydration if the page is scrolled while higher numbers make the entire process potentially faster.
|
||||
export const HYDRATION_QUEUE_SORT_PERIOD = 5
|
||||
|
||||
// CodeMirror options
|
||||
export const BASE_EDITOR_OPTS = {
|
||||
keyMap: 'styra',
|
||||
}
|
||||
|
||||
export const READ_ONLY_EDITOR_OPTS = {
|
||||
readOnly: true,
|
||||
cursorBlinkRate: -1 // Hides cursor w/o readOnly = 'nocursor' so that editor can be focused and copy/pasting still works
|
||||
}
|
||||
|
||||
export const LINE_NUMBERS_EDITOR_OPTS = {
|
||||
lineNumbers: true,
|
||||
}
|
||||
|
||||
const REGO_MODE = 'rego'
|
||||
const JSON_MODE = {name: 'javascript', json: true}
|
||||
export const EDITOR_MODES = {
|
||||
[BLOCK_TYPES.MODULE]: REGO_MODE,
|
||||
[BLOCK_TYPES.QUERY]: REGO_MODE,
|
||||
[BLOCK_TYPES.INPUT]: JSON_MODE,
|
||||
[BLOCK_TYPES.OUTPUT]: JSON_MODE // JSON mode also works pretty well for pretty-formatted output tables
|
||||
}
|
||||
|
||||
// Origin/ URL base for playground queries.
|
||||
export const PLAYGROUND = 'https://play.openpolicyagent.org'
|
||||
|
||||
// --- ICONS ---
|
||||
// NOTE: Update inject.sh and icons/ with any changes.
|
||||
const icon = (title, src, spinning) => ({title, src, spinning})
|
||||
const iconSRC = (name) => `/live-blocks/icons/${name}.svg`
|
||||
const pgIconSRC = (name) => iconSRC(`playground-${name}`)
|
||||
export const ICONS = {
|
||||
[BLOCK_TYPES.MODULE]: icon('Module', iconSRC('module')),
|
||||
[BLOCK_TYPES.QUERY]: icon('Query', iconSRC('query')),
|
||||
[BLOCK_TYPES.INPUT]: icon('Input', iconSRC('input')),
|
||||
[BLOCK_TYPES.OUTPUT]: icon('Output', iconSRC('output')),
|
||||
RESTORE: icon('Restore', iconSRC('restore')),
|
||||
PLAYGROUND: {
|
||||
READY: icon('Open in Playground', pgIconSRC('open')),
|
||||
WORKING: icon('Working...', pgIconSRC('working'), true),
|
||||
ERROR: icon('Failed!', pgIconSRC('error')),
|
||||
}
|
||||
}
|
||||
|
||||
// --- ERRORS ---
|
||||
// Predicates for each of the types of expected errors to determine whether the tag matches an OPA error object.
|
||||
// Error objects from OPA have a `code` and typically have some `message`. For undefined decisions, the error is the value undefined.
|
||||
const fIfErr = (pred) => (err) => { // Return false if an error occurs in the predicate (e.g. a field is not defined)
|
||||
try {
|
||||
return pred(err)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
const COMPILE_ERR_CODE = 'rego_compile_error'
|
||||
export const EXPECTED_ERROR_PREDICATES = {
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_ERROR]: () => true,
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_UNDEFINED]: (err) => err === undefined,
|
||||
// from 'ast'
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_REGO_ERROR]: fIfErr((err) => /rego_\w+_error/.test(err.code)),
|
||||
// [EXPECTED_ERROR_TAG_TYPES.EXPECT_PARSE_ERROR]: fIfErr((err) => err.code === 'rego_parse_error'), Not properly supported by the CLI.
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_COMPILE_ERROR]: fIfErr((err) => err.code === COMPILE_ERR_CODE),
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_REGO_TYPE_ERROR]: fIfErr((err) => err.code === 'rego_type_error'),
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_UNSAFE_VAR]: fIfErr((err) => err.code === 'rego_unsafe_var_error'),
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_RECURSION]: fIfErr((err) => err.code === 'rego_recursion_error'),
|
||||
// from 'topdown'
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_EVAL_ERROR]: fIfErr((err) => /eval_\w+_error/.test(err.code)),
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_CONFLICT]: fIfErr((err) => err.code === 'eval_conflict_error'),
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_EVAL_TYPE_ERROR]: fIfErr((err) => err.code === 'eval_type_error'),
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_BUILTIN_ERROR]: fIfErr((err) => err.code === 'eval_builtin_error'),
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_WITH_MERGE_ERROR]: fIfErr((err) => err.code === 'eval_with_merge_error'),
|
||||
// more specific
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_ASSIGNED_ABOVE]: fIfErr((err) => err.code === COMPILE_ERR_CODE && /assigned above/.test(err.message)),
|
||||
[EXPECTED_ERROR_TAG_TYPES.EXPECT_REFERENCED_ABOVE]: fIfErr((err) => err.code === COMPILE_ERR_CODE && /referenced above/.test(err.message)),
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import {EXPECTED_ERROR_PREDICATES} from './constants'
|
||||
import {expectedErrorTags, toArray} from './helpers'
|
||||
|
||||
// An error caused by another one. Allows clearer or user-friendly messages while preserving stack information.
|
||||
export class ChainedError extends Error {
|
||||
constructor(message, cause) {
|
||||
super(message)
|
||||
this.stack += `\n(caused by) ${cause ? ('stack' in cause ? cause.stack : cause) : 'no cause provided!'}`
|
||||
}
|
||||
}
|
||||
|
||||
// Errors returned from an OPA evaluation call.
|
||||
// Error objects from OPA have a `code` and typically have some `message`. For undefined decisions, the error is the value undefined.
|
||||
export class OPAErrors extends Error {
|
||||
// Consumes the pretty-formatted message and the one or more (the latter as an array) errors that OPA produced.
|
||||
// In the case of undefined decisions, call this with 'undefined decision', undefined.
|
||||
// The constructor may error with a non-user-friendly message.
|
||||
constructor(pretty, errors) {
|
||||
super(pretty)
|
||||
this.errors = toArray(errors)
|
||||
|
||||
// Validate there are more than one properly formatted errors
|
||||
if (!this.errors.length) {
|
||||
throw new Error('an OPAErrors object must contain at least one error')
|
||||
}
|
||||
for (let error of this.errors) {
|
||||
if (error != undefined && !('code' in error)) {
|
||||
throw new Error('an opa error should either be the value undefined (indicating an undefined decision) or have a code')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether the given tag array corresponds to this' errors.
|
||||
matchesExpected(tags) {
|
||||
const eTags = expectedErrorTags(tags)
|
||||
|
||||
return (
|
||||
// Check that all errors are matched by a tag
|
||||
this.errors.reduce((allErrsMatched, err) => allErrsMatched && (
|
||||
eTags.reduce((anyTagMatch, tag) => anyTagMatch || EXPECTED_ERROR_PREDICATES[tag](err), false)
|
||||
), true)
|
||||
&&
|
||||
// Check that all tags match an error
|
||||
eTags.reduce((allTagsMatch, tag) => allTagsMatch && (
|
||||
this.errors.reduce((anyErrMatched, err) => anyErrMatched || EXPECTED_ERROR_PREDICATES[tag](err), false)
|
||||
), true)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
import {BLOCK_TYPES, EVAL_CONFIG_TAG_TYPES, EXPECTED_ERROR_TAG_TYPES, LABEL_REG_EXP} from './constants'
|
||||
import {ChainedError} from './errors'
|
||||
|
||||
// --- LABELS ---
|
||||
// Takes a string that can be formatted like live:GROUP:BLOCK_TYPE[:TAG_TYPE[,TAG_TYPE[...]]], returns {group: /\w+/, type: BLOCK_TYPES[?], tags: [ string ]}.
|
||||
// Throws an error if it looks like a live code block but the label can't be parsed.
|
||||
export function infoFromLabel(label) {
|
||||
const info = LABEL_REG_EXP.exec(label)
|
||||
if (!info) {
|
||||
throw new Error(`improperly formated live code block label: ${label}`)
|
||||
}
|
||||
|
||||
let tags = []
|
||||
if (info[3].length > 1) {// Will include ':', minimum if there are tags is 2 chars
|
||||
tags = info[3].substring(1).split(',')
|
||||
}
|
||||
|
||||
// Validate parsing
|
||||
if (info[2] !== BLOCK_TYPES.OUTPUT && expectedErrorTags(tags).length) { // Only outputs can expect errors
|
||||
throw new Error(`${info[2]}s cannot expect errors (only outputs): ${label}`)
|
||||
}
|
||||
if (info[2] !== BLOCK_TYPES.OUTPUT && includedGroupNames(tags).length) { // Only outputs can include groups for evaluation
|
||||
throw new Error(`${info[2]}s cannot include modules for evaluation (only outputs): ${label}`)
|
||||
}
|
||||
|
||||
// Looks good
|
||||
return {
|
||||
group: info[1],
|
||||
type: info[2],
|
||||
tags
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an array of the expected error tag strings in the provided array of tags
|
||||
export function expectedErrorTags(tags) {
|
||||
const errors = Object.values(EXPECTED_ERROR_TAG_TYPES)
|
||||
return tags.filter((tag) => errors.includes(tag))
|
||||
}
|
||||
|
||||
// Returns an array of the included group names from `include(...)` tags in the provided array.
|
||||
export function includedGroupNames(tags) {
|
||||
return tags.reduce((iGNs, t) => {
|
||||
const res = EVAL_CONFIG_TAG_TYPES.INCLUDE.exec(t)
|
||||
if (res) {
|
||||
iGNs.push(res)
|
||||
}
|
||||
return iGNs
|
||||
}, [])
|
||||
}
|
||||
|
||||
// --- GROUPS AND BLOCKS ---
|
||||
// Gets the value of the deepest field with a given name in the group/ parent groups, otherwise undefined.
|
||||
export function getGroupField(groups, groupName, field) {
|
||||
const nameFragments = groupName.split('/') // Fragments of the group name
|
||||
|
||||
while (nameFragments.length) { // Pop off fragments until the field is set
|
||||
const group = groups[nameFragments.join('/')]
|
||||
if (group && field in group) {
|
||||
return group[field]
|
||||
}
|
||||
nameFragments.pop()
|
||||
}
|
||||
|
||||
return undefined // Explicit: it failed
|
||||
}
|
||||
|
||||
// Gets an array of module blocks from group and/or its parents from shallowest to deepest
|
||||
export function getAllGroupModules(groups, groupName) {
|
||||
const out = []
|
||||
|
||||
const nameFragments = groupName.split('/') // Fragments of the group name
|
||||
while (nameFragments.length) { // Append module blocks to out, pop off fragments
|
||||
const group = groups[nameFragments.join('/')]
|
||||
if (group && BLOCK_TYPES.MODULE in group) {
|
||||
out.push(group[BLOCK_TYPES.MODULE])
|
||||
}
|
||||
nameFragments.pop()
|
||||
}
|
||||
|
||||
return out.reverse() // Correct the order
|
||||
}
|
||||
|
||||
// Returns an object of the form
|
||||
//
|
||||
// {
|
||||
// module: string,
|
||||
// package: string
|
||||
// input: value
|
||||
// query: string,
|
||||
// included: map[filename]string
|
||||
// }
|
||||
//
|
||||
// Throws an error with a human-readable message if the specified or any
|
||||
// included groups don't have modules, the group's package cannot be found, or
|
||||
// if the input cannot be parsed.
|
||||
export function getGroupData(groups, groupName) {
|
||||
const queryBlock = getGroupField(groups, groupName, BLOCK_TYPES.QUERY)
|
||||
const inputBlock = getGroupField(groups, groupName, BLOCK_TYPES.INPUT)
|
||||
|
||||
const out = {module: getCompleteGroupModule(groups, groupName)}
|
||||
|
||||
let pkgRegexRes = /^package[ \t]+([\w.-]+)[ \t]*#*.*$/m.exec(out.module)
|
||||
if (!pkgRegexRes) {
|
||||
throw new Error('couldn\'t find package declaration in module')
|
||||
}
|
||||
out.package = pkgRegexRes[1]
|
||||
|
||||
if (queryBlock) {
|
||||
out.query = queryBlock.get()
|
||||
}
|
||||
if (inputBlock) {
|
||||
try {
|
||||
out.input = JSON.parse(inputBlock.get())
|
||||
} catch (e) {
|
||||
throw new Error('can\'t parse input', e)
|
||||
}
|
||||
}
|
||||
if (groupName in groups && BLOCK_TYPES.OUTPUT in groups[groupName]) {
|
||||
const included = includedGroupNames(groups[groupName][BLOCK_TYPES.OUTPUT].tags)
|
||||
if (included.length) {
|
||||
out.included = {}
|
||||
for (let name of included) {
|
||||
try {
|
||||
out.included[`${name}.rego`] = getCompleteGroupModule(groups, name)
|
||||
} catch (e) {
|
||||
throw new ChainedError(`unable to include ${name}: ${e.message}`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Returns the string of the full group's module or throws a user friendly error if there are no module blocks.
|
||||
function getCompleteGroupModule(groups, groupName) {
|
||||
const moduleBlocks = getAllGroupModules(groups, groupName)
|
||||
|
||||
if (!moduleBlocks.length) {
|
||||
throw new Error('no module')
|
||||
}
|
||||
|
||||
return moduleBlocks.map((block) => block.get()).join('\n\n')
|
||||
}
|
||||
|
||||
// --- ASYNC ---
|
||||
// Returns a promise that resolves to an array of the form [returned, thrown] (at least one will be undefined) where they are the result of awaiting a call to the passed function.
|
||||
// This is useful for awaiting an operation now but handling its results later and/or handling it multiple times.
|
||||
// WARNING: Make sure thrown values get handled, otherwise they'll get swallowed. This is effectively a work-around for unhandled rejection warnings/errors.
|
||||
export async function handleLater(func) {
|
||||
try {
|
||||
return [await func(), undefined]
|
||||
} catch (e) {
|
||||
return [undefined, e]
|
||||
}
|
||||
}
|
||||
|
||||
// Runs an array of async functions (`jobs`) maxConcurrent at a time, these can each reject with one or more errors (the latter as an array).
|
||||
// Once they've all finished, either resolves to an array of results ordered by when the jobs completed or rejects with a similarly ordered, potentially differently sized array of errors.
|
||||
// maxConcurrent is treated as unlimited if < 1
|
||||
export async function batchProcess(maxConcurrent, jobs) {
|
||||
if (maxConcurrent < 1 || maxConcurrent > jobs.length) {
|
||||
maxConcurrent = jobs.length // Run all of them simultaneously if maxConcurrent is beyond reasonable values
|
||||
}
|
||||
jobs = Array.from(jobs) // Shallow copy, the new array will get mutated
|
||||
|
||||
const out = []
|
||||
const errs = []
|
||||
async function process() {
|
||||
for (let job = jobs.pop(); job; job = jobs.pop()) { // Pop off jobs
|
||||
try {
|
||||
out.push(await job())
|
||||
} catch (e) {
|
||||
errs.push(...toArray(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({length: maxConcurrent}, () => process())) // Run maxConcurrent calls to process() asyncronously
|
||||
|
||||
if (errs.length) {
|
||||
throw errs
|
||||
} else {
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
// Runs an array of `inputs` through the async function `map`.
|
||||
// Each time it determines it's new output (returned when all inputs are processed) by calling fold with the current output (starting with the value outBase) and map result ({input: value, output: [returned, thrown]}).
|
||||
// maxConcurrent is passed to the underlying call to `batchProcess`.
|
||||
export async function batchMapFold(maxConcurrent, inputs, map, fold, outBase) {
|
||||
await batchProcess(maxConcurrent, inputs.map((input) => async () => { // Create an array of async jobs to map and fold each input
|
||||
const result = {
|
||||
input,
|
||||
output: await handleLater(() => map(input))
|
||||
}
|
||||
outBase = fold(outBase, result) // Folding happens syncronously (no awaits) so there's no concurrency concerns
|
||||
}))
|
||||
return outBase
|
||||
}
|
||||
|
||||
// Returns a promise that resolves in the given number of milliseconds.
|
||||
export function delay(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
|
||||
// --- MISC ---
|
||||
// Wraps a value in an array if it isn't one already
|
||||
export function toArray(val) {
|
||||
if (Array.isArray(val)) {
|
||||
return val
|
||||
}
|
||||
return [val]
|
||||
}
|
||||
|
||||
// Aliases for console functions so that messages can be intercepted in the future and stray debugging statements are easier to find.
|
||||
export const {log: println, error: report} = console
|
||||
@@ -1,108 +0,0 @@
|
||||
import cp from 'child_process'
|
||||
import path from 'path'
|
||||
import {constants as fsConsts, promises as promFS} from 'fs'
|
||||
|
||||
import fetch from 'node-fetch'
|
||||
|
||||
import {ChainedError} from '../errors'
|
||||
import {handleLater, println} from '../helpers'
|
||||
import {OPA_CACHE_PATH, OPA_EDGE_PATH, PLATFORMS, VERSION_EDGE, VERSION_LATEST} from '../constants'
|
||||
|
||||
const PLATFORM = (() => { // Current platform, massaged into the format that OPA releases use.
|
||||
let p = process.platform
|
||||
if (p === 'win32') {
|
||||
p = PLATFORMS.WINDOWS
|
||||
}
|
||||
return p
|
||||
})()
|
||||
const GOOS = (() => {
|
||||
return cp.execSync("go env GOOS").toString().trim()
|
||||
})()
|
||||
const GOARCH = (() => {
|
||||
return cp.execSync("go env GOARCH").toString().trim()
|
||||
})()
|
||||
const OPA_PATH = path.resolve(OPA_CACHE_PATH)
|
||||
|
||||
const acquireResults = {} // Cache: object mapping version strings to promises that will resolve to [path, error] where one is undefined.
|
||||
|
||||
// Returns a promise that either resolves to an absolute path to a opa binary of that version that is executable by the current process on the current platform or rejects with a user-friendly error.
|
||||
export async function acquireOPAVersion(version) {
|
||||
// Synchronously (no awaits, within this function) create and cache the acquirer if needed, prevents other calls to acquireOPAVersion that are running around the same time from creating duplicate acquirers.
|
||||
if (!acquireResults[version]) {
|
||||
acquireResults[version] = handleLater(createAcquirer(version))
|
||||
}
|
||||
|
||||
// Actually wait for the acquirer to do it's job, handle the result.
|
||||
const [path, err] = await acquireResults[version] // The same handleLater promise may be awaited multiple times before and/or after it resolves, this isn't a problem.
|
||||
if (err) {
|
||||
throw err
|
||||
} else {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an async function that either resolves to a path or rejects with a user-friendly error.
|
||||
// WARNING: This function and it's resulting acquirer should only get called once per version.
|
||||
function createAcquirer(version) {
|
||||
return async () => {
|
||||
|
||||
println(`Locating OPA version ${version}...`)
|
||||
|
||||
const path = pathToVersion(version)
|
||||
|
||||
if (await needsDownloading(path)) { // This does not protect against run conditions within a single preprocessing script run, simply allows (long-term) caching.
|
||||
|
||||
// Edge is required to have been built from the current source tree,
|
||||
// expect the binary to be available at the normal build output location.
|
||||
// We will _NOT_ download anything for it.
|
||||
if (version === VERSION_EDGE) {
|
||||
throw new Error(`binary for ${VERSION_EDGE} is not available at path ${path}, ensure it has been built for the current platform`)
|
||||
}
|
||||
|
||||
const assetURL = getReleaseAssetURL(version);
|
||||
|
||||
let file
|
||||
try {
|
||||
println(`Downloading OPA version ${version} from ${assetURL}...`)
|
||||
await promFS.mkdir(OPA_PATH, {recursive: true}) // recursive also prevents error if the folder already exists
|
||||
file = await promFS.open(path, 'w', 0o744) // create file with permissions rwx for user, r for group, other
|
||||
await promFS.writeFile(file, await (await fetch(assetURL)).buffer())
|
||||
await file.close()
|
||||
} catch (e) {
|
||||
if (file) { // Close the file if fetching/writing fails
|
||||
await file.close()
|
||||
}
|
||||
throw new ChainedError(`unable to download the OPA version ${version} for ${PLATFORM} from ${assetURL}`, e)
|
||||
}
|
||||
}
|
||||
|
||||
println(`Found OPA version ${version}, available at ${path}`)
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// The absolute path where an OPA copy should be located.
|
||||
function pathToVersion(version) {
|
||||
if (version === VERSION_EDGE) {
|
||||
return path.resolve(OPA_EDGE_PATH, `opa_${GOOS}_${GOARCH}`)
|
||||
}
|
||||
return path.resolve(OPA_CACHE_PATH, `${version}-${PLATFORM}${PLATFORM === PLATFORMS.WINDOWS ? '.exe' : ''}`)
|
||||
}
|
||||
|
||||
// Gets the URL that can be used to download a given version of OPA for the current platform.
|
||||
// Releases are on GitHub, URLs are predictable
|
||||
function getReleaseAssetURL(version) {
|
||||
if (version === VERSION_LATEST) {
|
||||
version = process.env.LATEST;
|
||||
}
|
||||
return `https://github.com/open-policy-agent/opa/releases/download/${version}/opa_${PLATFORM}_${GOARCH}${PLATFORM === PLATFORMS.WINDOWS ? '.exe' : ''}`;
|
||||
}
|
||||
|
||||
// Determines, based on the path to where it should be downloaded, whether it needs to be downloaded. Will not throw an error.
|
||||
async function needsDownloading(path) {
|
||||
try {
|
||||
await promFS.access(path, fsConsts.X_OK) // X_OK verifies that permissions-wise it's executable
|
||||
} catch (e) {
|
||||
return true // Can't be accessed, download it
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
import process from 'process'
|
||||
import {promises as promFS} from 'fs'
|
||||
|
||||
import * as filepath from 'path';
|
||||
|
||||
import cheerio from 'cheerio'
|
||||
|
||||
import {batchMapFold, batchProcess, expectedErrorTags, infoFromLabel, println, report, toArray} from '../helpers.js'
|
||||
import {BLOCK_SELECTOR, BLOCK_TYPES, CLASSES, CSS_BUNDLE_BATH, ICONS, JS_BUNDLE_PATH, MAX_CONCUR_FILE_EVALS, MAX_CONCUR_FILES, STATIC_TAG_TYPES, VERSION_EDGE, VERSION_LATEST} from '../constants.js'
|
||||
import {ChainedError, OPAErrors} from '../errors.js'
|
||||
|
||||
import localEval from './localEval.js'
|
||||
|
||||
// --- MAIN ---
|
||||
start() // Enter async.
|
||||
async function start() {
|
||||
// Process the files, generating a map from paths that resulted in errors to an array of those errors.
|
||||
// This allows several files to be processed at once, without processing ALL of them at the same time, while reporting as many errors as possible so that they can be fixed.
|
||||
const errors = await batchMapFold(MAX_CONCUR_FILES, process.argv.slice(2), processFile, (output, processingResult) => {
|
||||
const {input: path, output: [_, err]} = processingResult
|
||||
if (err) {
|
||||
output[path] = toArray(err)
|
||||
}
|
||||
return output
|
||||
}, {})
|
||||
|
||||
if (Object.keys(errors).length) { // Any of the files have errors
|
||||
for (let path in errors) { // Log details (stack trace)
|
||||
report(`\n--- Errors for ${path} ---`)
|
||||
for (let error of errors[path]) {
|
||||
report(error)
|
||||
}
|
||||
}
|
||||
report('\n\n')
|
||||
|
||||
report("It looks like there were some errors. Here's an overview:")
|
||||
for (let path in errors) { // Log just the messages (probably the only thing needed most of the time)
|
||||
report(`\n${path}:`)
|
||||
for (let error of errors[path]) {
|
||||
report(`\t${error.message}`)
|
||||
}
|
||||
}
|
||||
report('\n\n')
|
||||
|
||||
process.exit(1) // Exit with non-zero code to prevent the docs from being deployed.
|
||||
} else {
|
||||
// All good!
|
||||
println('Live docs preprocessing complete!')
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Processes a file at a path, may reject with one or more errors (the latter as an array) that occurred while trying to do so.
|
||||
async function processFile(path) {
|
||||
const version = getVersion(path)
|
||||
|
||||
if (!version) {
|
||||
return // Skip! No version.
|
||||
}
|
||||
|
||||
const $ = cheerio.load(await promFS.readFile(path))
|
||||
|
||||
const codeElts = $(BLOCK_SELECTOR)
|
||||
|
||||
if (!codeElts.length) {
|
||||
return // Skip! No live blocks.
|
||||
}
|
||||
|
||||
println(`Processing ${path}...`)
|
||||
const groups = constructGroups($, codeElts)
|
||||
styleBlocks(groups)
|
||||
await populateOutputs(groups, version)
|
||||
enliven($)
|
||||
await promFS.writeFile(path, $.html())
|
||||
}
|
||||
|
||||
// --- PROCESSING HELPERS ---
|
||||
// Based on a file's path containing /v[VERSION]/ or /edge/ returns the version string (e.g. '0.12.1' or 'edge'). Returns latest if it can't find a version.
|
||||
function getVersion(path) {
|
||||
const versionMatch = (new RegExp(`/(v[^/]*|${VERSION_EDGE}|${VERSION_LATEST})/`)).exec(filepath.resolve(path))
|
||||
if (!versionMatch) {
|
||||
if (process.env.LATEST) {
|
||||
return VERSION_LATEST
|
||||
}
|
||||
return VERSION_EDGE
|
||||
}
|
||||
|
||||
return versionMatch[1]
|
||||
}
|
||||
|
||||
// Constructs an object of the following form, may throw an array of errors.
|
||||
// {
|
||||
// groupName: {
|
||||
// module/query/input/output: {
|
||||
// tags: [string],
|
||||
// get: () => string,
|
||||
// set: (string value[, boolean error]) => undefined
|
||||
// codeElt: cheerio code block
|
||||
// container: cheerio element
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
function constructGroups($, codeElts) {
|
||||
const groups = {}
|
||||
const errors = []
|
||||
codeElts.each(function () { // Cheerio's `each` method (codeElts is not iterable).
|
||||
try {
|
||||
const codeElt = $(this)
|
||||
// remove trailing newline
|
||||
codeElt.text(codeElt.text().trim())
|
||||
|
||||
// wrap in div
|
||||
const container = $('<div></div>').addClass(CLASSES.BLOCK_CONTAINER)
|
||||
codeElt.parent().wrap(container)
|
||||
|
||||
const label = codeElt.data('lang')
|
||||
let info = infoFromLabel(label)
|
||||
|
||||
// Populate second level of the groups object structure
|
||||
if (!groups[info.group]) {
|
||||
groups[info.group] = {}
|
||||
}
|
||||
|
||||
// Prevent duplicates at preprocess-time
|
||||
if (info.type in groups[info.group]) {
|
||||
throw new Error(`${info.group} cannot have more than one ${info.type}`)
|
||||
}
|
||||
|
||||
// Prevent non-empty outputs
|
||||
if (info.type === BLOCK_TYPES.OUTPUT && !(/^\s*$/).test(codeElt.text())) {
|
||||
throw new Error(`${info.group}'s output must not contain anything; it will be populated automatically`)
|
||||
}
|
||||
|
||||
groups[info.group][info.type] = {
|
||||
tags: info.tags,
|
||||
get: () => codeElt.text(),
|
||||
set: (value, error) => {
|
||||
codeElt.text(value)
|
||||
if (typeof error === 'boolean') {
|
||||
if (error) {
|
||||
container.addClass(CLASSES.BLOCK_CONTAINER_ERROR)
|
||||
} else {
|
||||
container.removeClass(CLASSES.BLOCK_CONTAINER_ERROR)
|
||||
}
|
||||
}
|
||||
},
|
||||
codeElt,
|
||||
container,
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push(e)
|
||||
}
|
||||
})
|
||||
if (errors.length) {
|
||||
throw errors
|
||||
} else {
|
||||
return groups
|
||||
}
|
||||
}
|
||||
|
||||
// Add some classes, styles, and elements to the blocks/ their containers. Generally shouldn't but may throw an error.
|
||||
function styleBlocks(groups) {
|
||||
for (let group of Object.values(groups)) {
|
||||
for (let [type, block] of Object.entries(group)) {
|
||||
// Reset highlighting
|
||||
const content = block.get()
|
||||
block.codeElt.empty()
|
||||
block.set(content)
|
||||
|
||||
// Hide if tagged
|
||||
if (block.tags.includes(STATIC_TAG_TYPES.HIDDEN)) {
|
||||
block.container.css('display', 'none')
|
||||
}
|
||||
|
||||
// Merge if tagged
|
||||
if (block.tags.includes(STATIC_TAG_TYPES.MERGE_DOWN)) {
|
||||
block.container.addClass(CLASSES.BLOCK_CONTAINER_MERGED_DOWN)
|
||||
}
|
||||
|
||||
// Add icon bar/ type mark
|
||||
block.container.prepend(`
|
||||
<div class="${CLASSES.ICON_BAR_OUTER}">
|
||||
<div class="${CLASSES.ICON_BAR_INNER}">
|
||||
<div class="${CLASSES.ICON_CONTAINER} ${CLASSES.ICON_CONTAINER_INDICATOR}" title="${ICONS[type].title}">
|
||||
<img class="${CLASSES.ICON_IMG}" src="${ICONS[type].src}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate what the output blocks should contain and set it. May throw an array of errors if the evaluations can't be performed or they result in unexpected errors.
|
||||
async function populateOutputs(groups, opaVersion) {
|
||||
await batchProcess(MAX_CONCUR_FILE_EVALS, Object.entries(groups).filter(([, group]) => {
|
||||
return BLOCK_TYPES.OUTPUT in group // Only process groups that have an output (some parent groups, e.g. that others inherit module fragments from, may not have one)
|
||||
|
||||
}).map(([groupName, group]) => (async () => {
|
||||
const block = group[BLOCK_TYPES.OUTPUT]
|
||||
const expectedETags = expectedErrorTags(block.tags)
|
||||
const strETags = JSON.stringify(expectedETags)
|
||||
|
||||
let resultString
|
||||
try {
|
||||
resultString = await localEval(groups, groupName, opaVersion)
|
||||
} catch (e) { // If there's an error it might be expected (e.g. syntax errors like unsafe vars)
|
||||
if (e instanceof OPAErrors) { // Are the errors from OPA?
|
||||
if (e.matchesExpected(block.tags)) {
|
||||
block.set(e.message, true)
|
||||
return // Output populated, exit!
|
||||
}
|
||||
throw new ChainedError(`${groupName} evaluation failed unexpectedly (${expectedETags.length ? `only tagged with: ${strETags}` : 'untagged'}):\n${e.message}\n`, e)
|
||||
|
||||
}
|
||||
// Errors are not from OPA, something bad happened.
|
||||
throw new ChainedError(`${groupName} evaluation failed for an unexpected, possibly internal reason (check for additional logs): ${e.message}`, e)
|
||||
}
|
||||
|
||||
// If there's not an error, one might have been expected.
|
||||
if (expectedETags.length) {
|
||||
throw new Error(`${groupName} is tagged with ${strETags} but evaluation result was:\n${resultString}\n`)
|
||||
} else {
|
||||
block.set(resultString, false)
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
// Add the JS and CSS elements to the body/ head for hydrating the code blocks.
|
||||
function enliven($) {
|
||||
const head = $('head').first()
|
||||
const body = $('body').first()
|
||||
if (!head) {
|
||||
throw new Error('no head element')
|
||||
}
|
||||
if (!body) {
|
||||
throw new Error('no body element')
|
||||
}
|
||||
|
||||
// Add to beginning of head to allow the page to override style in other
|
||||
// css sources later on in <head>.
|
||||
head.prepend(`<link rel="stylesheet" href="${CSS_BUNDLE_BATH}">`)
|
||||
|
||||
body.append(`<script src="${JS_BUNDLE_PATH}"></script>`)
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import cp from 'child_process'
|
||||
import util from 'util'
|
||||
import {promises as promFS} from 'fs'
|
||||
|
||||
import tmp from 'tmp'
|
||||
import semver from 'semver'
|
||||
|
||||
import {ChainedError, OPAErrors} from '../errors'
|
||||
import {EVAL_MODULE_NAME, VERSION_EDGE, VERSION_LATEST} from '../constants'
|
||||
import {getGroupData} from '../helpers'
|
||||
|
||||
import {acquireOPAVersion} from './acquireOPAVersion'
|
||||
|
||||
// Clean up temporary files on exit
|
||||
tmp.setGracefulCleanup()
|
||||
|
||||
// Promisify non-fs async file operations
|
||||
const execFile = util.promisify(cp.execFile)
|
||||
const tmpFile = util.promisify(tmp.file)
|
||||
|
||||
// Evaluate the group using a local OPA binary. Either returns a string suitable for a code block or throws an error whose message is user-friendly. If the OPA evaluation itself failed, throws an OPAErrors instance.
|
||||
export default async function localEval(groups, groupName, opaVersion) {
|
||||
const opa = await acquireOPAVersion(opaVersion) // Get the path (and download if necessary) the required version of OPA.
|
||||
|
||||
const [args, moduleFilenameMap] = await prepEval(groups, groupName, opaVersion) // Save the group data to temporary files
|
||||
|
||||
try {
|
||||
return (await execFile(opa, args('pretty'))).stdout.replace(/\n$/, '') // Evaluate, retrieving the pretty-formatted output
|
||||
} catch (e) {
|
||||
if (e.stdout) { // OPA returned an error message
|
||||
let pretty = e.stdout.replace(/\n$/, '')
|
||||
// Replace the module file names with what the playground will use.
|
||||
for (let [tmp, replacement] of Object.entries(moduleFilenameMap)) {
|
||||
pretty = pretty.replace(new RegExp(tmp, 'g'), replacement)
|
||||
}
|
||||
|
||||
if (pretty === 'undefined') { // Special undefined case, use the same message as the playground.
|
||||
throw new OPAErrors('undefined decision', undefined)
|
||||
|
||||
} else { // Some other error occurred, reevaluate to get the JSON-formatted version of the errors so that they can be compared against what the block expects.
|
||||
try {
|
||||
await execFile(opa, args('json'))
|
||||
throw new Error('subsequent eval of failing evaluation did not fail')
|
||||
} catch (e2) {
|
||||
if (e2.stdout) { // Reevaluation seems to have worked
|
||||
let opaErrors
|
||||
try { // Parse the reevaluation result into an OPAErrors object to be thrown.
|
||||
let parsed = JSON.parse(e2.stdout)
|
||||
if (parsed.errors) {
|
||||
opaErrors = new OPAErrors(pretty, parsed.errors)
|
||||
} else {
|
||||
opaErrors = new OPAErrors(pretty, parsed.error)
|
||||
}
|
||||
} catch (e3) {
|
||||
throw new ChainedError('invalid response while trying to get details about an evaluation failure', e3)
|
||||
}
|
||||
throw opaErrors
|
||||
} else { // Reevaluation didn't produce output
|
||||
throw new ChainedError('an error occurred while trying to get details about an evaluation failure', e2)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // OPA didn't return an error message, probably some problem calling it.
|
||||
throw new ChainedError(e.stderr ? `OPA: ${e.stderr}` : 'unable to perform local evaluation', e) // Some OPA error. This particularly applies to parse errors, they occur at a different point internally and are not reported correctly; it's fine though, doc examples probably shouldn't cause them.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns 1st a function that consumes a string for the output format you want and produces an array of arguments and 2nd a map of module file names to strings that should be replaced in error messages. May throw a user-friendly error.
|
||||
async function prepEval(groups, groupName, opaVersion) {
|
||||
const {module, package: pkg, query, input, included} = getGroupData(groups, groupName)
|
||||
const base = ['eval', '--fail'] // Fail on undefined
|
||||
const rest = []
|
||||
const moduleFilenameMap = {}
|
||||
|
||||
try {
|
||||
let tmpModule = await tmpFile({postfix: '.rego'})
|
||||
await promFS.writeFile(tmpModule, module)
|
||||
rest.push('-d', tmpModule, '--package', pkg)
|
||||
moduleFilenameMap[tmpModule] = EVAL_MODULE_NAME
|
||||
|
||||
if (input) {
|
||||
let stringified = ''
|
||||
try {
|
||||
stringified = JSON.stringify(input)
|
||||
} catch (e) {
|
||||
throw new ChainedError('input cannot be stringified', e)
|
||||
}
|
||||
const tmpInput = await tmpFile({postfix: '.json'})
|
||||
await promFS.writeFile(tmpInput, stringified)
|
||||
rest.push('--input', tmpInput)
|
||||
}
|
||||
|
||||
if (query) {
|
||||
// NOTE(sr): Only do this if the version is recent enough to understand it,
|
||||
// and if we're rendering docs of a version where we actually started using
|
||||
// queries that need this import.
|
||||
if (opaVersion === VERSION_LATEST || opaVersion == VERSION_EDGE ||
|
||||
semver.satisfies(semver.coerce(opaVersion), '>=0.42.0')) {
|
||||
rest.push('--import', 'future.keywords') // queries may contain them
|
||||
}
|
||||
rest.push(query)
|
||||
} else { // Simulate playground default behavior
|
||||
if (included) {
|
||||
rest.push('data')
|
||||
} else {
|
||||
rest.push(`data.${pkg}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (included) {
|
||||
for (let [incName, incModule] of Object.entries(included)) {
|
||||
let tmpIncluded = await tmpFile({postfix: '.rego'})
|
||||
await promFS.writeFile(tmpIncluded, incModule)
|
||||
rest.push('-d', tmpIncluded)
|
||||
moduleFilenameMap[tmpIncluded] = incName
|
||||
}
|
||||
}
|
||||
|
||||
return [(format) => [...base, `--format=${format}`, ...rest], moduleFilenameMap]
|
||||
} catch (e) {
|
||||
throw new ChainedError('a problem occurred while preparing to evaluate', e)
|
||||
}
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
import 'core-js/stable'
|
||||
import 'regenerator-runtime/runtime'
|
||||
|
||||
import CodeMirror from 'codemirror'
|
||||
// eslint-disable-next-line
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
|
||||
import './style.css'
|
||||
import {BASE_EDITOR_OPTS, BLOCK_SELECTOR, BLOCK_TYPES, CLASSES, EDITOR_MODES, HYDRATION_QUEUE_SORT_PERIOD, ICONS, INTERACTIVE_PATH, LINE_NUMBERS_EDITOR_OPTS, OPENING_IN_PLAYGROUND_PATH, READ_ONLY_EDITOR_OPTS, STATIC_TAG_TYPES} from '../constants'
|
||||
import {batchProcess, delay, getAllGroupModules, getGroupField, handleLater, includedGroupNames, infoFromLabel, report} from '../helpers'
|
||||
import {OPAErrors} from '../errors'
|
||||
|
||||
import {playgroundEval, shareToPlayground} from './playground'
|
||||
|
||||
import 'codemirror-rego/key-map'
|
||||
import 'codemirror-rego/mode'
|
||||
import 'codemirror/addon/comment/comment'
|
||||
import 'codemirror/addon/edit/matchbrackets'
|
||||
|
||||
// --- MAIN ---
|
||||
start() // Enter async.
|
||||
async function start() {
|
||||
const codeElts = document.querySelectorAll(BLOCK_SELECTOR)
|
||||
if (!codeElts.length) {
|
||||
return // Exit! No live blocks.
|
||||
}
|
||||
|
||||
const groups = constructGroups(codeElts)
|
||||
registerChangeHandlers(groups)
|
||||
await hydrate(groups)
|
||||
}
|
||||
|
||||
// --- HYDRATION HELPERS ---
|
||||
// Constructs an object of the following form:
|
||||
// groupName: {
|
||||
// blockType (module/query/input/output): {
|
||||
// tags: [string],
|
||||
// get: () => string,
|
||||
// set: (string value[, boolean error]) => undefined,
|
||||
// restore: () => undefined // Restore the contents and error-state of the container.
|
||||
// changeHandlers: [0+ functions],
|
||||
// codeElt,
|
||||
// iconBar,
|
||||
// container,
|
||||
// codeMirror: initially, sometimes always, undefined
|
||||
// openButton: initially, sometimes always, undefined
|
||||
// },
|
||||
// output: {
|
||||
// ... same as above
|
||||
// current: initially undefined // Used to discard out of date responses from the playground
|
||||
// timeout: initially undefined // Used to debounce changes to dependencies
|
||||
// }
|
||||
// }
|
||||
function constructGroups(codeElts) {
|
||||
const groups = {}
|
||||
|
||||
for (let codeElt of codeElts) {
|
||||
const container = codeElt.parentNode.parentNode
|
||||
|
||||
const label = codeElt.dataset.lang
|
||||
let info = infoFromLabel(label)
|
||||
|
||||
if (!groups[info.group]) { // Populate second level of the groups object structure
|
||||
groups[info.group] = {}
|
||||
}
|
||||
|
||||
const initText = codeElt.textContent
|
||||
const initErr = container.classList.contains(CLASSES.BLOCK_CONTAINER_ERROR)
|
||||
groups[info.group][info.type] = {
|
||||
tags: info.tags,
|
||||
get: () => codeElt.textContent,
|
||||
set: (value, error) => {
|
||||
codeElt.textContent = value
|
||||
if (typeof error === 'boolean') {
|
||||
if (error) {
|
||||
container.classList.add(CLASSES.BLOCK_CONTAINER_ERROR)
|
||||
} else {
|
||||
container.classList.remove(CLASSES.BLOCK_CONTAINER_ERROR)
|
||||
}
|
||||
}
|
||||
runHandlers(groups[info.group][info.type].changeHandlers)
|
||||
},
|
||||
restore: () => {
|
||||
groups[info.group][info.type].set(initText, initErr)
|
||||
},
|
||||
changeHandlers: [],
|
||||
codeElt,
|
||||
iconBar: container.firstElementChild.firstElementChild,
|
||||
container,
|
||||
}
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
function registerChangeHandlers(groups) {
|
||||
for (let [groupName, group] of Object.entries(groups)) {
|
||||
const output = group.output
|
||||
if (!output) { // Not all groups have outputs
|
||||
continue
|
||||
}
|
||||
// Output contents depend on non-undefined query and input blocks in this group/a parent, this group's chain of inherited module blocks, as well as any included module blocks.
|
||||
const dependencies = [BLOCK_TYPES.QUERY, BLOCK_TYPES.INPUT].map((type) => getGroupField(groups, groupName, type)).filter((block) => !!block)
|
||||
dependencies.push(...getAllGroupModules(groups, groupName))
|
||||
dependencies.push(...(includedGroupNames(output.tags).reduce((iMs, iGN) => iMs.concat(getAllGroupModules(groups, iGN)), [])))
|
||||
|
||||
for (let dependency of dependencies) {
|
||||
dependency.changeHandlers.push(() => updateOutput(groups, groupName, output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hydrate the all the blocks as applicable
|
||||
async function hydrate(groups) {
|
||||
let queue = Object.entries(groups).reduce((arr, [groupName, group]) => arr.concat(Object.entries(group).map(([type, block]) => ({groupName, group, type, block}))), [])
|
||||
function sort() { // Sort based on distance to the top of the screen
|
||||
queue.sort((a, b) => Math.abs(b.block.container.getBoundingClientRect().top) - Math.abs(a.block.container.getBoundingClientRect().top))
|
||||
}
|
||||
sort()
|
||||
|
||||
for (let next = queue.pop(); next; next = queue.pop()) {
|
||||
const {groupName, type, block} = next
|
||||
|
||||
// Hydrate
|
||||
hydrateEditor(type, block)
|
||||
hydrateButtons(groups, groupName, type, block)
|
||||
|
||||
// Occasionally re-sort the queue in case the user scrolls.
|
||||
if (!(queue.length % HYDRATION_QUEUE_SORT_PERIOD)) {
|
||||
sort()
|
||||
}
|
||||
|
||||
// Don't freeze the browser :)
|
||||
await delay(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Hydrate visible editors
|
||||
function hydrateEditor(type, block) {
|
||||
if (!block.tags.includes(STATIC_TAG_TYPES.HIDDEN)) { // Only bother adding the editor to visible blocks
|
||||
// Replace intermediate container and hydrate it
|
||||
const cmDiv = document.createElement('div')
|
||||
block.codeElt.parentNode.replaceWith(cmDiv)
|
||||
const codeMirror = CodeMirror(cmDiv, Object.assign({
|
||||
value: block.get()
|
||||
}, constructEditorOptions(type, block.tags)))
|
||||
fixScroll(codeMirror)
|
||||
codeMirror.on('change', () => {
|
||||
runHandlers(block.changeHandlers)
|
||||
})
|
||||
|
||||
// Add class to trigger shadows
|
||||
if (isEditable(type, block.tags)) {
|
||||
block.container.classList.add(CLASSES.BLOCK_CONTAINER_EDITABLE)
|
||||
}
|
||||
|
||||
// Update block object
|
||||
Object.assign(block, {
|
||||
get: () => codeMirror.getValue(),
|
||||
set: (value, error) => {
|
||||
codeMirror.setValue(value)
|
||||
if (typeof error === 'boolean') {
|
||||
if (error) {
|
||||
block.container.classList.add(CLASSES.BLOCK_CONTAINER_ERROR)
|
||||
} else {
|
||||
block.container.classList.remove(CLASSES.BLOCK_CONTAINER_ERROR)
|
||||
}
|
||||
}
|
||||
fixScroll(codeMirror)
|
||||
},
|
||||
codeMirror
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Hydrate the restore and share buttons as applicable
|
||||
function hydrateButtons(groups, groupName, type, block) {
|
||||
if (isEditable(type, block.tags)) {
|
||||
const restoreButton = createButton(ICONS.RESTORE)
|
||||
restoreButton.addEventListener('click', () => block.restore())
|
||||
block.iconBar.appendChild(restoreButton)
|
||||
}
|
||||
|
||||
if (isInteractive(block.tags) && block.tags.includes(STATIC_TAG_TYPES.OPENABLE)) {
|
||||
// The three versions of the "Open in Playground" button.
|
||||
const readyButton = createButton(ICONS.PLAYGROUND.READY)
|
||||
const workingButton = createButton(ICONS.PLAYGROUND.WORKING)
|
||||
const errorButton = createButton(ICONS.PLAYGROUND.ERROR)
|
||||
|
||||
// Handler to open in the playground.
|
||||
const open = createOpenInPlaygroundHandler(groups, groupName, block, readyButton, workingButton, errorButton)
|
||||
|
||||
// Only ready and error trigger an open, if "working", forces user to wait until it completes.
|
||||
readyButton.addEventListener('click', open)
|
||||
errorButton.addEventListener('click', open)
|
||||
|
||||
// Hydrate
|
||||
putOpenInPlaygroundButton(block, readyButton)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new button in the DOM based on the given icon constant.
|
||||
function createButton(icon) {
|
||||
const image = document.createElement('img')
|
||||
image.src = icon.src
|
||||
image.classList.add(CLASSES.ICON_IMG)
|
||||
if (icon.spinning) {
|
||||
image.classList.add(CLASSES.ICON_IMG_SPINNING)
|
||||
}
|
||||
const container = document.createElement('div')
|
||||
container.title = icon.title
|
||||
container.classList.add(CLASSES.ICON_CONTAINER)
|
||||
container.classList.add(CLASSES.ICON_CONTAINER_BUTTON)
|
||||
container.appendChild(image)
|
||||
return container
|
||||
}
|
||||
|
||||
// Update the contents of an output block from a group.
|
||||
function updateOutput(groups, groupName, output) {
|
||||
output.set('...' + '\n'.repeat((output.get().match(/\n/g) || []).length), false) // Show that it's in progress without changing size
|
||||
|
||||
clearTimeout(output.timeout) // Debounce: If update is called rapidly, cancel last one.
|
||||
|
||||
output.timeout = setTimeout((async () => {
|
||||
const promise = handleLater(() => playgroundEval(groups, groupName)) // Wrap with handleLater so that immediate rejection isn't a problem
|
||||
output.current = promise // Store the promise to the playground's response globally (tied to this output)
|
||||
|
||||
const [text, error] = await promise // Time passes...
|
||||
|
||||
if (output.current === promise) { // Is the global current promise (for this output block) still the one created by this call?
|
||||
if (!error) {
|
||||
output.set(text, false)
|
||||
} else {
|
||||
if (!(error instanceof OPAErrors) || !error.matchesExpected(output.tags)) {
|
||||
report(error) // Some extra logging for unexpected errors.
|
||||
}
|
||||
output.set(error.message, true)
|
||||
}
|
||||
} else { // A new request was made while waiting for this response, discard it.
|
||||
return
|
||||
}
|
||||
}), 500) // Wait 0.5s before fetching in case they're still typing, will be canceled above if they do.
|
||||
}
|
||||
|
||||
// Padding was causing issues where text on the right side got cut off, this fixes it for a given CodeMirror instance.
|
||||
function fixScroll(editor) {
|
||||
// Below is copied from the CodeMirror window resize handler, codeMirror.refresh() didn't work...
|
||||
const d = editor.display
|
||||
d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null
|
||||
d.scrollbarsClipped = false
|
||||
editor.setSize()
|
||||
}
|
||||
|
||||
// Runs every potentially async handler of handlers, logging unexpected errors. Guaranteed to not reject so can be run without catching.
|
||||
async function runHandlers(handlers) {
|
||||
try {
|
||||
await batchProcess(0, handlers)
|
||||
} catch (errs) {
|
||||
report('one or more errors occurred in change handlers', handlers, errs)
|
||||
}
|
||||
}
|
||||
|
||||
// Determines whether a block can be interacted with (a lower bar than editability) based on its tags and the current page's path.
|
||||
function isInteractive(tags) {
|
||||
if (tags.includes(STATIC_TAG_TYPES.HIDDEN)) {
|
||||
return false
|
||||
}
|
||||
return INTERACTIVE_PATH.test(window.location.pathname);
|
||||
|
||||
}
|
||||
|
||||
// Determines whether a block should be editable based on its type, tags, and the current page's path.
|
||||
function isEditable(type, tags) {
|
||||
if (!isInteractive(tags)) {
|
||||
return false
|
||||
}
|
||||
if (type === BLOCK_TYPES.OUTPUT) {
|
||||
return false
|
||||
}
|
||||
if (tags.includes(STATIC_TAG_TYPES.READ_ONLY)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Returns the options to use with a given block type that has the given tags.
|
||||
function constructEditorOptions(type, tags) {
|
||||
const out = Object.assign({}, BASE_EDITOR_OPTS)
|
||||
|
||||
out.mode = EDITOR_MODES[type]
|
||||
|
||||
if (!isEditable(type, tags)) {
|
||||
Object.assign(out, READ_ONLY_EDITOR_OPTS)
|
||||
}
|
||||
|
||||
if (tags.includes(STATIC_TAG_TYPES.LINE_NUMBERS)) {
|
||||
Object.assign(out, LINE_NUMBERS_EDITOR_OPTS)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Add/ replace the "Open in Playground" button.
|
||||
function putOpenInPlaygroundButton(block, button) {
|
||||
if (!block.openButton) {
|
||||
block.iconBar.appendChild(button)
|
||||
} else {
|
||||
block.openButton.replaceWith(button)
|
||||
}
|
||||
block.openButton = button
|
||||
}
|
||||
|
||||
// Returns a function that when called attempts to open the given group in the playground. Puts the appropriate button for the given block as it works.
|
||||
// NOTE: The user should not be able to trigger the returned function when the button is set to `working`.
|
||||
function createOpenInPlaygroundHandler(groups, groupName, block, ready, working, error) {
|
||||
return function () {
|
||||
// Open window (really a tab) immediately in this bog-standard, non-async, non-arrow handler to minimize the chance that browsers will block it/ ask permission.
|
||||
const pg = window.open(OPENING_IN_PLAYGROUND_PATH, '_blank')
|
||||
|
||||
try {
|
||||
// Show that it's working
|
||||
putOpenInPlaygroundButton(block, working)
|
||||
|
||||
// Try to maintain focus, this generally won't work due to malicious popup mitigations in browsers.
|
||||
pg.blur()
|
||||
window.focus()
|
||||
} catch (e) {
|
||||
report(e)
|
||||
// If any of those not only didn't work but actually errored, it's still not a problem. Continue...
|
||||
}
|
||||
|
||||
// Asyncronously try to perform the share and update the new window accordingly.
|
||||
(async () => {
|
||||
try {
|
||||
const url = await shareToPlayground(groups, groupName)
|
||||
pg.location.replace(url)
|
||||
pg.focus()
|
||||
putOpenInPlaygroundButton(block, ready)
|
||||
} catch (e) {
|
||||
report('unable to open in playground', e)
|
||||
pg.close()
|
||||
putOpenInPlaygroundButton(block, error)
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import {ChainedError, OPAErrors} from '../errors'
|
||||
import {EVAL_MODULE_NAME, PLAYGROUND} from '../constants'
|
||||
import {getGroupData} from '../helpers'
|
||||
|
||||
// Evaluate the group using the playground. Either returns a string suitable for a code block or throws an error whose message is user-friendly. If the OPA evaluation itself failed, throws an OPAErrors instance.
|
||||
export async function playgroundEval(groups, groupName) {
|
||||
// Get fetch options
|
||||
const opts = createDataRequestOpts(groups, groupName)
|
||||
|
||||
// Make request
|
||||
let resp
|
||||
try {
|
||||
resp = await fetch(`${PLAYGROUND}/v1/data`, opts)
|
||||
} catch (e) {
|
||||
throw new ChainedError("can't contact server", e)
|
||||
}
|
||||
|
||||
// Parse response and return
|
||||
let parsed
|
||||
try {
|
||||
parsed = await resp.json()
|
||||
} catch (e) {
|
||||
throw new ChainedError('invalid response from server', e)
|
||||
}
|
||||
|
||||
if (resp.status === 200) {
|
||||
if (parsed.pretty) {
|
||||
return parsed.pretty.replace(/\n$/, '') // Some responses may have trailing newlines
|
||||
}
|
||||
if (parsed.result === null) {
|
||||
throw new OPAErrors("undefined decision", undefined)
|
||||
}
|
||||
// Else throw below
|
||||
} else if (parsed.message) { // The server returned a non-200 code and a message
|
||||
const message = parsed.message.replace(/\n$/, '') // Some messages may have trailing newlines
|
||||
if (parsed.error || message === 'undefined decision') { // The error was related to evaluation
|
||||
let opaErrors
|
||||
try { // Try validating the errors by creating an OPAErrors object to throw
|
||||
opaErrors = new OPAErrors(message, parsed.error)
|
||||
} catch (e) {
|
||||
throw new ChainedError(message, e) // Errors seem to be invalid but use the message it sent anyway, it's more informative than anything else.
|
||||
}
|
||||
throw opaErrors
|
||||
} else {
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
// None of the return or throw cases matched, that's unexpected
|
||||
throw new Error('invalid response from server')
|
||||
}
|
||||
|
||||
// Either resolves to a playground url with the group's content or rejects with a (non-user-friendly) error.
|
||||
export async function shareToPlayground(groups, groupName) {
|
||||
let url = (await (await fetch(`${PLAYGROUND}/v1/share`, createDataRequestOpts(groups, groupName))).json()).result
|
||||
if (!url) {
|
||||
throw new Error("Unable to get URL from the playground's response.")
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// Constructs a the fetch options for querying the playground with a DataRequest. Throws an error with a user-friendly message if it can't.
|
||||
function createDataRequestOpts(groups, groupName) {
|
||||
const {module, package: pkg, query, input, included} = getGroupData(groups, groupName) // Throws user-friendly messages.
|
||||
|
||||
try {
|
||||
const body = {} // In the format accepted by playground backend
|
||||
body.rego_modules = {[EVAL_MODULE_NAME]: module, ...included}
|
||||
body.rego_version = 1
|
||||
body.query_package = pkg
|
||||
if (query) {
|
||||
body.rego = query
|
||||
}
|
||||
if (input) {
|
||||
body.input = input
|
||||
}
|
||||
body.query_imports = ["rego.v1"]
|
||||
|
||||
return {
|
||||
body: JSON.stringify(body),
|
||||
// Both types of requests are POSTing JSON
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
} catch (e) { // Presumably stringifying the body failed, that shouldn't happen
|
||||
throw new ChainedError('unexpected error', e)
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/* NOTE: Update constants.js with any changes to class names. */
|
||||
.live--block-container {
|
||||
margin-bottom: 1.5rem;
|
||||
/* Ensure shadows appear over below elements correctly */
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.live--block-container--merged-down {
|
||||
margin-bottom: 0 !important;
|
||||
border-bottom: solid #cecece 1px;
|
||||
}
|
||||
|
||||
.live--block-container--editable.live--block-container--merged-down {
|
||||
/* Ensure shadows appear over below elements correctly */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.live--block-container--error span {
|
||||
color: #ff1b00 !important;
|
||||
}
|
||||
|
||||
.live--icon-bar--outer {
|
||||
height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.live--icon-bar--inner {
|
||||
height: 1.25em;
|
||||
padding: 0.0625em 0;
|
||||
flex-direction: row-reverse;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
font-size: 1.275em; /*TODO set for both this and codemirror, try setting it on container*/
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.live--icon--container {
|
||||
width: 1.0625em;
|
||||
height: 1.0625em;
|
||||
margin-right: 0.1875em;
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.live--icon--container--indicator {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.live--icon--container--button {
|
||||
border-radius: 0.25em;
|
||||
cursor: pointer;
|
||||
background-color: #363636;
|
||||
}
|
||||
|
||||
.live--icon--img {
|
||||
padding: 0.0625em;
|
||||
}
|
||||
|
||||
.live--icon--img--spinning {
|
||||
animation: spin 5000ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {transform:rotate(360deg);}
|
||||
to {transform:rotate(0deg);}
|
||||
}
|
||||
|
||||
/* --- SHADOWS --- */
|
||||
|
||||
.live--block-container .CodeMirror {
|
||||
transition: box-shadow 0.2s ease-out;
|
||||
}
|
||||
|
||||
.live--block-container--editable .CodeMirror {
|
||||
box-shadow: 0 0 3px #aaa;
|
||||
}
|
||||
|
||||
/* apply to CodeMirror instead of container using :focus-within because Edge doesn't support it */
|
||||
.live--block-container--editable .CodeMirror-focused {
|
||||
box-shadow: 0 0 6px #aaa !important;
|
||||
}
|
||||
|
||||
/* --- CODEMIRROR OVERRIDES --- */
|
||||
|
||||
.live--block-container .CodeMirror {
|
||||
height: auto;
|
||||
background-color: #f8f8f8;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.live--block-container .CodeMirror-scroll {
|
||||
overflow-y: hidden !important;
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
|
||||
.live--block-container .CodeMirror-hscrollbar {
|
||||
pointer-events: auto !important; /* Fixes weird bug where with no vertical scroll it was getting set to none... */
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.live--block-container .CodeMirror-sizer {
|
||||
padding: 1.25em 1.5em 1.25em 1.5em !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
.live--block-container .CodeMirror-lines {
|
||||
margin-left: -4px !important; /* Removing the 4px of left padding on .CodeMirror-line breaks highlighting and cursor, this corrects for it without removing it. Otherwise, the code jumps to the right when hydrated. */
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.live--block-container .CodeMirror-line {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.live--block-container .CodeMirror-gutter-wrapper {
|
||||
transform: translateX(-1.5em); /* Undo positioning from sizer padding */
|
||||
}
|
||||
|
||||
.live--block-container .cm-variable {
|
||||
color: #003c66; /* Just overriding the default theme for variables (otherwise black), we may want a custom theme later. */
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
The icons in this folder are provided by Google under the Apache 2.0 license (https://github.com/google/material-design-icons), a copy of which is included below:
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h4v-2H5V8h14v10h-4v2h4c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2zm-7 6l-4 4h3v6h2v-6h3l-4-4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 263 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M19.5 3.5L18 2l-1.5 1.5L15 2l-1.5 1.5L12 2l-1.5 1.5L9 2 7.5 3.5 6 2 4.5 3.5 3 2v20l1.5-1.5L6 22l1.5-1.5L9 22l1.5-1.5L12 22l1.5-1.5L15 22l1.5-1.5L18 22l1.5-1.5L21 22V2l-1.5 1.5zM19 19.09H5V4.91h14v14.18zM6 15h12v2H6zm0-4h12v2H6zm0-4h12v2H6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 379 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
||||
|
Before Width: | Height: | Size: 361 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12zM7 9h2v2H7zm8 0h2v2h-2zm-4 0h2v2h-2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 269 B |
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="live-docs-playground-error.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="-8.440678"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
id="path2" />
|
||||
<path
|
||||
d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"
|
||||
id="path4"
|
||||
style="fill:#f5f5f5;fill-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="live-docs-playground-open.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="-28.677966"
|
||||
inkscape:cy="11.59322"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
id="path2" />
|
||||
<path
|
||||
d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
id="path4"
|
||||
style="fill:#f5f5f5;fill-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="live-docs-playground-working.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="836"
|
||||
inkscape:window-height="480"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="7.6271186"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"
|
||||
id="path2"
|
||||
style="fill:#f5f5f5;fill-opacity:1" />
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
id="path4" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 381 B |
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="live-docs-restore.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="836"
|
||||
inkscape:window-height="480"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="11.694915"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
id="path2" />
|
||||
<path
|
||||
d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"
|
||||
id="path4"
|
||||
style="fill:#f5f5f5;fill-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,24 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Opening...</title>
|
||||
<style>
|
||||
.opening {
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
top: calc(50vh - 25px);
|
||||
left: calc(50vw - 25px);
|
||||
opacity: 0.25;
|
||||
animation: spin 5000ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {transform:rotate(360deg);}
|
||||
to {transform:rotate(0deg);}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img src="/live-blocks/icons/opening.svg" class="opening">
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,100 +0,0 @@
|
||||
import {describe, it} from 'mocha'
|
||||
import {expect} from 'chai'
|
||||
|
||||
import {ChainedError, OPAErrors} from '../src/errors'
|
||||
import {STATIC_TAG_TYPES} from '../src/constants'
|
||||
|
||||
describe('ChainedError', () => {
|
||||
it('uses a new message', () => {
|
||||
expect((new ChainedError('b', new Error('a'))).message).to.equal('b')
|
||||
})
|
||||
|
||||
it('preserves old stack information', () => {
|
||||
const e = new Error()
|
||||
e.stack = 'foobar'
|
||||
expect((new ChainedError('', e)).stack).to.contain('foobar')
|
||||
})
|
||||
|
||||
it('won\'t throw an unintelligible error if you forget the cause', () => {
|
||||
expect((new ChainedError('')).stack).to.contain('no cause')
|
||||
})
|
||||
})
|
||||
|
||||
describe('OPAErrors', () => {
|
||||
const oe = (code) => ({code})
|
||||
|
||||
it('message is the given pretty string', () => {
|
||||
expect((new OPAErrors('foobar', oe(''))).message).to.equal('foobar')
|
||||
})
|
||||
|
||||
it('can be constructed with a single OPA error', () => {
|
||||
expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([STATIC_TAG_TYPES.EXPECT_RECURSION])).to.be.true
|
||||
expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR])).to.be.false
|
||||
})
|
||||
|
||||
it('can be constructed with an array of OPA errors', () => {
|
||||
expect((new OPAErrors('', [oe('rego_recursion_error'), oe('rego_compile_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_RECURSION, STATIC_TAG_TYPES.EXPECT_COMPILE_ERROR])).to.be.true
|
||||
expect((new OPAErrors('', [oe('rego_recursion_error'), oe('rego_compile_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_RECURSION, STATIC_TAG_TYPES.EXPECT_UNSAFE_VAR])).to.be.false
|
||||
})
|
||||
|
||||
it('can be constructed with undefined as the error', () => {
|
||||
expect((new OPAErrors('', undefined)).matchesExpected([STATIC_TAG_TYPES.EXPECT_UNDEFINED])).to.be.true
|
||||
expect((new OPAErrors('', undefined)).matchesExpected([STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR])).to.be.false
|
||||
|
||||
expect((new OPAErrors('', [undefined])).matchesExpected([STATIC_TAG_TYPES.EXPECT_UNDEFINED])).to.be.true
|
||||
expect((new OPAErrors('', [undefined])).matchesExpected([STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR])).to.be.false
|
||||
})
|
||||
|
||||
it('can\'t be constructed with improperly formatted OPA errors', () => {
|
||||
expect(() => new OPAErrors('', 'foobar')).to.throw()
|
||||
expect(() => new OPAErrors('', {message: 'foobar'})).to.throw()
|
||||
expect(() => new OPAErrors('', [oe('rego_recursion_error'), {message: 'foobar'}])).to.throw()
|
||||
})
|
||||
|
||||
it('can\'t be constructed with empty array of OPA errors', () => {
|
||||
expect(() => new OPAErrors('', [])).to.throw()
|
||||
})
|
||||
|
||||
it('can match against no tags', () => {
|
||||
expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([])).to.be.false
|
||||
})
|
||||
|
||||
it('can match with duplicate errors', () => {
|
||||
expect((new OPAErrors('', [oe('rego_recursion_error'), oe('rego_recursion_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_RECURSION])).to.be.true
|
||||
})
|
||||
|
||||
it('can match with duplicate tags', () => {
|
||||
expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([STATIC_TAG_TYPES.EXPECT_RECURSION, STATIC_TAG_TYPES.EXPECT_RECURSION])).to.be.true
|
||||
})
|
||||
|
||||
it('can match with non-error tags', () => {
|
||||
expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.EXPECT_RECURSION])).to.be.true
|
||||
})
|
||||
|
||||
it('requires all errors to be matched', () => {
|
||||
expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([STATIC_TAG_TYPES.HIDDEN])).to.be.false
|
||||
expect((new OPAErrors('', [oe('rego_recursion_error'), oe('rego_compile_error')])).matchesExpected([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.EXPECT_RECURSION])).to.be.false
|
||||
})
|
||||
|
||||
it('requires all error tags to match an error', () => {
|
||||
expect((new OPAErrors('', oe('rego_recursion_error'))).matchesExpected([STATIC_TAG_TYPES.HIDDEN])).to.be.false
|
||||
expect((new OPAErrors('', [oe('rego_recursion_error'), oe('rego_compile_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_RECURSION, STATIC_TAG_TYPES.EXPECT_COMPILE_ERROR, STATIC_TAG_TYPES.EXPECT_ASSIGNED_ABOVE])).to.be.false
|
||||
})
|
||||
|
||||
it('child errors can be matched precisely', () => {
|
||||
expect((new OPAErrors('', [{code: 'rego_compile_error', message: 'assigned above'}])).matchesExpected([STATIC_TAG_TYPES.EXPECT_ASSIGNED_ABOVE])).to.be.true
|
||||
expect((new OPAErrors('', [{code: 'rego_compile_error', message: 'assigned above'}, {code: 'rego_compile_error', message: 'referenced above'}])).matchesExpected([STATIC_TAG_TYPES.EXPECT_ASSIGNED_ABOVE])).to.be.false
|
||||
})
|
||||
|
||||
it('parent tags match children', () => {
|
||||
expect((new OPAErrors('', [undefined, oe('rego_type_error'), oe('eval_type_error'), oe('fake_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_ERROR])).to.be.true
|
||||
expect((new OPAErrors('', [oe('rego_type_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_REGO_ERROR])).to.be.true
|
||||
expect((new OPAErrors('', [oe('eval_type_error')])).matchesExpected([STATIC_TAG_TYPES.EXPECT_EVAL_ERROR])).to.be.true
|
||||
expect((new OPAErrors('', [{code: 'rego_compile_error', message: 'assigned above'}])).matchesExpected([STATIC_TAG_TYPES.EXPECT_COMPILE_ERROR])).to.be.true
|
||||
})
|
||||
|
||||
it('undefined is only matched by its own tag and expect_error', () => {
|
||||
expect((new OPAErrors('', undefined).matchesExpected([STATIC_TAG_TYPES.EXPECT_ERROR, STATIC_TAG_TYPES.EXPECT_UNDEFINED]))).to.be.true
|
||||
expect((new OPAErrors('', undefined).matchesExpected([STATIC_TAG_TYPES.EXPECT_EVAL_ERROR, STATIC_TAG_TYPES.EXPECT_UNDEFINED]))).to.be.false
|
||||
})
|
||||
})
|
||||
@@ -1,422 +0,0 @@
|
||||
import chai, {expect} from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import sinon from 'sinon'
|
||||
import {describe, it} from 'mocha'
|
||||
|
||||
import {batchMapFold, batchProcess, delay, expectedErrorTags, getAllGroupModules, getGroupData, getGroupField, handleLater, includedGroupNames, infoFromLabel, toArray} from '../src/helpers'
|
||||
import {BLOCK_TYPES, STATIC_TAG_TYPES} from '../src/constants'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
describe('infoFromLabel', () => {
|
||||
it('parses a label', () => {
|
||||
const res = infoFromLabel('live:eg/pi:output:merge_down,expect_undefined,include(foobar/barfoo)')
|
||||
expect(res).has.keys(['group', 'type', 'tags'])
|
||||
expect(res.group).equals('eg/pi')
|
||||
expect(res.type).equals(BLOCK_TYPES.OUTPUT)
|
||||
expect(res.tags).lengthOf(3).and.contains('merge_down').and.contains('expect_undefined').and.contains('include(foobar/barfoo)')
|
||||
})
|
||||
|
||||
it('parses a label without tags', () => {
|
||||
const res = infoFromLabel('live:eg/pi:output')
|
||||
expect(res).has.keys(['group', 'type', 'tags'])
|
||||
expect(res.group).equals('eg/pi')
|
||||
expect(res.type).equals('output')
|
||||
expect(res.tags).lengthOf(0)
|
||||
})
|
||||
|
||||
it('errors on non-live block labels', () => {
|
||||
expect(() => infoFromLabel('json')).to.throw()
|
||||
})
|
||||
|
||||
it('errors on malformed block labels', () => {
|
||||
expect(() => infoFromLabel('live:eg:modle')).to.throw()
|
||||
expect(() => infoFromLabel('live::module')).to.throw()
|
||||
expect(() => infoFromLabel('live:eg:module:')).to.throw()
|
||||
expect(() => infoFromLabel('live:eg:module:foobar')).to.throw()
|
||||
expect(() => infoFromLabel('live:eg:module:expect_undefined')).to.throw()
|
||||
expect(() => infoFromLabel('live:eg:module:include(foobar/barfoo)')).to.throw()
|
||||
})
|
||||
})
|
||||
|
||||
describe('expectedErrorTags', () => {
|
||||
it('accepts an empty array', () => {
|
||||
expect(expectedErrorTags([])).deep.equals([])
|
||||
})
|
||||
|
||||
it('can consume only non-expectations', () => {
|
||||
expect(expectedErrorTags([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.MERGE_DOWN, 'include(foobar)'])).deep.equals([])
|
||||
})
|
||||
|
||||
it('can consume only expectations', () => {
|
||||
expect(expectedErrorTags([STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.EXPECT_ERROR]))
|
||||
.has.lengthOf(2)
|
||||
.and.contains(STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR)
|
||||
.and.contains(STATIC_TAG_TYPES.EXPECT_ERROR)
|
||||
})
|
||||
|
||||
it('can consume a mixture', () => {
|
||||
expect(expectedErrorTags([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.MERGE_DOWN, STATIC_TAG_TYPES.EXPECT_ERROR]))
|
||||
.has.lengthOf(2)
|
||||
.and.contains(STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR)
|
||||
.and.contains(STATIC_TAG_TYPES.EXPECT_ERROR)
|
||||
})
|
||||
|
||||
it('doesn\'t mutate argument', () => {
|
||||
const tags = [STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.MERGE_DOWN, STATIC_TAG_TYPES.EXPECT_ERROR]
|
||||
const args = tags.slice()
|
||||
|
||||
expectedErrorTags([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.MERGE_DOWN, STATIC_TAG_TYPES.EXPECT_ERROR])
|
||||
expect(args).to.deep.equal(tags)
|
||||
})
|
||||
})
|
||||
|
||||
describe('includedGroupNames', () => {
|
||||
it('accepts an empty array', () => {
|
||||
expect(includedGroupNames([])).deep.equals([])
|
||||
})
|
||||
|
||||
it('can consume only non-includes', () => {
|
||||
expect(includedGroupNames([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.MERGE_DOWN, STATIC_TAG_TYPES.EXPECT_ASSIGNED_ABOVE])).deep.equals([])
|
||||
})
|
||||
|
||||
it('can consume only includes', () => {
|
||||
expect(includedGroupNames(['include(foo/bar)', 'include(eg)']))
|
||||
.has.lengthOf(2)
|
||||
.and.contains('foo/bar')
|
||||
.and.contains('eg')
|
||||
})
|
||||
|
||||
it('can consume a mixture', () => {
|
||||
expect(includedGroupNames([STATIC_TAG_TYPES.HIDDEN, 'include(foo/bar)', STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.MERGE_DOWN, 'include(eg)', STATIC_TAG_TYPES.EXPECT_ERROR]))
|
||||
.has.lengthOf(2)
|
||||
.and.contains('foo/bar')
|
||||
.and.contains('eg')
|
||||
})
|
||||
|
||||
it('doesn\'t mutate argument', () => {
|
||||
const tags = [STATIC_TAG_TYPES.HIDDEN, 'include(foo/bar)', STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.MERGE_DOWN, 'include(eg)', STATIC_TAG_TYPES.EXPECT_ERROR]
|
||||
const args = tags.slice()
|
||||
|
||||
includedGroupNames([STATIC_TAG_TYPES.HIDDEN, STATIC_TAG_TYPES.EXPECT_BUILTIN_ERROR, STATIC_TAG_TYPES.MERGE_DOWN, STATIC_TAG_TYPES.EXPECT_ERROR])
|
||||
expect(args).to.deep.equal(tags)
|
||||
})
|
||||
|
||||
it('only accepts properly formated tags', () => {
|
||||
expect(includedGroupNames(['include(foo-bar)', 'include', 'include()', 'include(foo/bar/)']))
|
||||
.has.lengthOf(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getGroupField', () => {
|
||||
it('gets field in group if there', () => {
|
||||
expect(getGroupField({foo: {bar: 1}, 'foo/two': {bar: 2}}, 'foo/two', 'bar')).to.equal(2)
|
||||
})
|
||||
|
||||
it('gets parents\' fields when field is missing', () => {
|
||||
expect(getGroupField({foo: {bar: 7}, 'foo/two': {baz: 42}}, 'foo/two', 'bar')).to.equal(7)
|
||||
})
|
||||
|
||||
it('gets parents\' fields when group is missing', () => {
|
||||
expect(getGroupField({foo: {bar: 7}}, 'foo/two', 'bar')).to.equal(7)
|
||||
})
|
||||
|
||||
it('gets field from deepest parent', () => {
|
||||
const groups = {foo: {bar: 1}, 'foo/two': {bar: 2}, 'foo/two/child': {}}
|
||||
expect(getGroupField(groups, 'foo/two/child', 'bar')).to.equal(2)
|
||||
})
|
||||
|
||||
it('skips missing parents', () => {
|
||||
expect(getGroupField({foo: {bar: 7}, 'foo/two/three': {baz: 42}}, 'foo/two/three', 'bar')).to.equal(7)
|
||||
})
|
||||
|
||||
it('returns undefined when hierarchy does not exist', () => {
|
||||
expect(getGroupField({foo: {bar: 7}, 'foo/two/three': {baz: 42}}, 'a/b/c', 'bar')).to.equal(undefined)
|
||||
})
|
||||
|
||||
it('returns undefined when out of parents', () => {
|
||||
expect(getGroupField({foo: {bar: 7}, 'foo/two': {baz: 42}}, 'foo/two', 'unset')).to.equal(undefined)
|
||||
})
|
||||
|
||||
it('doesn\'t mutate groups', () => {
|
||||
const groups = {foo: {bar: 7}, 'foo/two': {baz: 42}}
|
||||
const arg = JSON.parse(JSON.stringify(groups))
|
||||
getGroupField(arg, 'foo/two', 'bar')
|
||||
expect(arg).to.deep.equal(groups)
|
||||
getGroupField(arg, 'foo/two', 'unset')
|
||||
expect(arg).to.deep.equal(groups)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAllGroupModules', () => {
|
||||
it('can get no modules', () => {
|
||||
expect(getAllGroupModules({}, 'foo')).to.deep.equal([])
|
||||
expect(getAllGroupModules({foo: {bar: 7}, 'foo/two': {baz: 42}}, 'foo/two')).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('gets all the present modules from shallowest to deepest', () => {
|
||||
expect(getAllGroupModules({
|
||||
'a': {module: 1},
|
||||
'a/b/c': {module: 2}
|
||||
}, 'a/b/c/d')).to.deep.equal([1, 2])
|
||||
})
|
||||
|
||||
it('doesn\'t mutate groups', () => {
|
||||
const groups = {
|
||||
'a': {module: 1},
|
||||
'a/b/c': {module: 2}
|
||||
}
|
||||
const arg = JSON.parse(JSON.stringify(groups))
|
||||
getAllGroupModules(arg, 'a/b/c/d')
|
||||
expect(arg).to.deep.equal(groups)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getGroupData', () => {
|
||||
it('concatenates module blocks', () => {
|
||||
expect(getGroupData({
|
||||
'a': {module: {get: () => 'package foo\na'}},
|
||||
'a/b/c': {module: {get: () => 'c'}}
|
||||
}, 'a/b/c/d').module).to.equal('package foo\na\n\nc')
|
||||
})
|
||||
|
||||
it('errors when there are no module blocks', () => {
|
||||
expect(() => getGroupData({
|
||||
'a': {query: {get: () => 'a'}},
|
||||
'a/b/c': {query: {get: () => 'c'}}
|
||||
}, 'a/b/c/d')).to.throw('no module')
|
||||
})
|
||||
|
||||
it('get\'s the module package', () => {
|
||||
expect(getGroupData({
|
||||
'a': {module: {get: () => 'package foo\na'}},
|
||||
'a/b/c': {module: {get: () => 'c'}}
|
||||
}, 'a/b/c/d').package).to.equal('foo')
|
||||
})
|
||||
|
||||
it('errors when there is no package declaration', () => {
|
||||
expect(() => getGroupData({
|
||||
'a': {module: {get: () => 'a'}},
|
||||
'a/b/c': {module: {get: () => 'c'}}
|
||||
}, 'a/b/c/d')).to.throw(/package/)
|
||||
})
|
||||
|
||||
const egInput = ['foo', {bar: 'baz'}]
|
||||
const strInput = JSON.stringify(egInput)
|
||||
|
||||
it('parses input from block', () => {
|
||||
expect(getGroupData({
|
||||
'a': {input: {get: () => 'foobar'}, module: {get: () => 'package foo\na'}}, // Will error if gets from this parent
|
||||
'a/b/c': {input: {get: () => strInput}, module: {get: () => 'c'}}
|
||||
}, 'a/b/c').input).to.deep.equal(egInput)
|
||||
})
|
||||
|
||||
it('can get input from parents', () => {
|
||||
expect(getGroupData({
|
||||
'a': {input: {get: () => 'foobar'}, module: {get: () => 'package foo\na'}}, // Will error if gets from this parent
|
||||
'a/b/c': {input: {get: () => strInput}, module: {get: () => 'c'}}
|
||||
}, 'a/b/c/d').input).to.deep.equal(egInput)
|
||||
})
|
||||
|
||||
it('errors when input cannot be parsed', () => {
|
||||
expect(() => getGroupData({
|
||||
'a': {input: {get: () => 'foobar'}, module: {get: () => 'package foo\na'}},
|
||||
}, 'a')).to.throw('can\'t parse input')
|
||||
})
|
||||
|
||||
it('gets the query', () => {
|
||||
expect(getGroupData({
|
||||
'a': {query: {get: () => {
|
||||
throw new Error()
|
||||
}}, module: {get: () => 'package foo\na'}}, // Will error if gets from this parent
|
||||
'a/b/c': {query: {get: () => 'foobar'}, module: {get: () => 'c'}}
|
||||
}, 'a/b/c').query).to.equal('foobar')
|
||||
})
|
||||
|
||||
it('can get the query from parents', () => {
|
||||
expect(getGroupData({
|
||||
'a': {query: {get: () => {
|
||||
throw new Error()
|
||||
}}, module: {get: () => 'package foo\na'}}, // Will error if gets from this parent
|
||||
'a/b/c': {query: {get: () => 'foobar'}, module: {get: () => 'c'}}
|
||||
}, 'a/b/c/d').query).to.equal('foobar')
|
||||
})
|
||||
|
||||
it('includes other modules', () => {
|
||||
expect(getGroupData({
|
||||
'a': {module: {get: () => 'package foo\na'}},
|
||||
'a/b/c': {module: {get: () => 'c'}, output: {tags: ['include(d)']}},
|
||||
'd': {module: {get: () => 'd'}}
|
||||
}, 'a/b/c').included["d.rego"]).to.equal('d')
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleLater', () => {
|
||||
it('resolves with the resolved value', async () => {
|
||||
expect(await handleLater(() => Promise.resolve('foo'))).to.deep.equal(['foo', undefined])
|
||||
})
|
||||
|
||||
it('resolves with the rejected value', async () => {
|
||||
expect(await handleLater(() => Promise.reject('foo'))).to.deep.equal([undefined, 'foo'])
|
||||
})
|
||||
|
||||
it('resolves with resolved undefined', async () => {
|
||||
expect(await handleLater(() => Promise.resolve(undefined))).to.deep.equal([undefined, undefined])
|
||||
})
|
||||
|
||||
it('resolves with rejected undefined', async () => {
|
||||
expect(await handleLater(() => Promise.reject(undefined))).to.deep.equal([undefined, undefined])
|
||||
})
|
||||
})
|
||||
|
||||
describe('batchProcess', () => {
|
||||
it('can run no jobs', async () => {
|
||||
expect(await batchProcess(0, [])).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('runs only maxC jobs at a time', async () => {
|
||||
expect(await time(() =>
|
||||
batchProcess(1, [
|
||||
async () => await delay(100),
|
||||
async () => await delay(100),
|
||||
])
|
||||
)).to.equal(200)
|
||||
|
||||
expect(await time(() =>
|
||||
batchProcess(2, [
|
||||
async () => await delay(100),
|
||||
async () => await delay(100),
|
||||
async () => await delay(100),
|
||||
async () => await delay(100),
|
||||
])
|
||||
)).to.equal(200)
|
||||
})
|
||||
|
||||
it('runs all of them simultaneously with maxC = 0', async () => {
|
||||
expect(await time(() =>
|
||||
batchProcess(0, [
|
||||
async () => await delay(100),
|
||||
async () => await delay(100),
|
||||
])
|
||||
)).to.equal(100)
|
||||
})
|
||||
|
||||
it('resolves to an array of the results', async () => {
|
||||
expect(await batchProcess(0, [() => 1, () => 2])).to.have.a.lengthOf(2).and.contain(1).and.contain(2)
|
||||
})
|
||||
|
||||
it('rejects once all are run with a flat array of errors', async () => {
|
||||
const [_, e] = await handleLater(() => batchProcess(0, [() => {
|
||||
throw [1, 2]
|
||||
}, () => {
|
||||
throw 3
|
||||
}]))
|
||||
expect(e).to.have.a.lengthOf(3).and.contain(1).and.contain(2).and.contain(3)
|
||||
})
|
||||
|
||||
it('doesn\'t mutate jobs', async () => {
|
||||
const jobs = [() => 1, () => 2]
|
||||
const arg = jobs.slice()
|
||||
await batchProcess(0, arg)
|
||||
expect(arg).to.deep.equal(jobs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('batchMapFold', () => {
|
||||
it('can handle no input', async () => {
|
||||
expect(await batchMapFold(0, [], () => {
|
||||
throw ''
|
||||
}, () => {
|
||||
throw ''
|
||||
}, 'foo')).to.equal('foo')
|
||||
})
|
||||
|
||||
it('handles only maxC inputs at a time', async () => {
|
||||
expect(await time(() =>
|
||||
batchMapFold(1, [0, 0], () => delay(100), () => {}, 0)
|
||||
)).to.equal(200)
|
||||
|
||||
expect(await time(() =>
|
||||
batchMapFold(2, [0, 0, 0, 0], () => delay(100), () => {}, 0)
|
||||
)).to.equal(200)
|
||||
})
|
||||
|
||||
it('handles all of them simultaneously with maxC = 0', async () => {
|
||||
expect(await time(() =>
|
||||
batchMapFold(0, [0, 0, 0, 0], () => delay(100), () => {}, 0)
|
||||
)).to.equal(100)
|
||||
})
|
||||
|
||||
it('resolves to the folded results', async () => {
|
||||
expect(await batchMapFold(0, [1, 2, 4, 8], (n) => n, (o, {output: [n]}) => o + n, 0)).to.equal(15)
|
||||
})
|
||||
|
||||
it('reports map errors to the folder', async () => {
|
||||
expect(await batchMapFold(0, [1, 2, 4, 8], (n) => {
|
||||
throw n
|
||||
}, (o, {output: [, n]}) => o + n, 0)).to.equal(15)
|
||||
})
|
||||
|
||||
it('folds synchronously', async () => {
|
||||
expect(await batchMapFold(0, [1, 2, 4, 8], (n) => n, async (o, {output: [n]}) => o + n, 0)).to.equal('[object Promise]1')
|
||||
})
|
||||
|
||||
it('doesn\'t mutate inputs', async () => {
|
||||
const inputs = [1, 2, 4, 8]
|
||||
const arg = inputs.slice()
|
||||
await batchMapFold(0, arg, (n) => n, (o, {output: [n]}) => o + n, 0)
|
||||
expect(arg).to.deep.equal(inputs)
|
||||
})
|
||||
|
||||
it('can mutate the out argument', async () => {
|
||||
const out = {val: 0}
|
||||
await batchMapFold(0, [1, 2, 4, 8], (n) => n, (o, {output: [n]}) => {
|
||||
o.val += n; return o
|
||||
}, out)
|
||||
expect(out.val).to.equal(15)
|
||||
})
|
||||
})
|
||||
|
||||
describe('delay', () => {
|
||||
it('delays the given amount', async () => {
|
||||
expect(await time(() => delay(100))).to.equal(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toArray', () => {
|
||||
it('returns arrays', () => {
|
||||
const empty = []
|
||||
expect(toArray(empty)).to.equal(empty) // Not just deep; actually equal
|
||||
const full = [1, 2, 3]
|
||||
expect(toArray(full)).to.equal(full)
|
||||
})
|
||||
|
||||
it('wraps non-arrays', () => {
|
||||
expect(toArray(undefined)).to.deep.equal([undefined])
|
||||
expect(toArray('a')).to.deep.equal(['a'])
|
||||
expect(toArray({a: 'a'})).to.deep.equal([{a: 'a'}])
|
||||
})
|
||||
})
|
||||
|
||||
// Time how long it takes an async function to complete without actually waiting that long. Returns a promise that resolves to the # of ms.
|
||||
async function time(f) {
|
||||
const clock = sinon.useFakeTimers() // Will only advance automatically if runAll() doesn't work.
|
||||
|
||||
// Run the function
|
||||
let done = false;
|
||||
(async () => {
|
||||
await handleLater(f) // await f to either resolve or reject
|
||||
done = true
|
||||
})()
|
||||
|
||||
// Run the clock, allowing the function's delays to complete
|
||||
while (!done) {
|
||||
clock.runAll() // Even though it's called 'runAll', calling it multiple times is necessary
|
||||
await Promise.resolve() // Yield to the function that's running
|
||||
}
|
||||
|
||||
// Finish up
|
||||
const end = new Date().getTime()
|
||||
clock.restore()
|
||||
return end
|
||||
}
|
||||