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>
This commit is contained in:
Charlie Egan
2025-05-12 12:41:57 +01:00
committed by GitHub
parent 23c06fa94d
commit f3cb38dc05
33 changed files with 2 additions and 13208 deletions
+1 -1
View File
@@ -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
+1 -19
View File
@@ -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/
-144
View File
@@ -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`.
File diff suppressed because it is too large Load Diff
@@ -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
}