geneva

1.5.0 • Public • Published

Geneva

Geneva is a way to make ordinary data more dynamic. It allows you to include code within YAML or JSON documents for the purpose of keeping specifications lightweight yet extensible.

Beware: please do not run this code in production settings yet. This is an experimental library for testing out an idea. If you desire to use this kind of approach, consider testing this extensively and contributing back or writing something that works for you.

Install

This is currently in alpha for 1.0.0. There is an older version of Geneva that does not use Ramda and is not as easy to use.

npm install geneva

Why do this?

Since JSON parsers are everywhere, what if we turned JSON into its own little language? To make a usable language, we need a standard library, and Ramda is a great fit for that. Geneva mashes JSON/YAML and Ramda together to make a simple way to define Ramda code as JSON/YAML that can be passed around and evaluated as desired.

It's just for fun. But it's not too far off from tools like CloudFormation or Azure's ARM that put a bunch of code-like structure in YAML. Maybe this will spark some ideas of creating a little language that can be used for writing and processing configurations.

Making specifications dynamic

One main benefit of Geneva is that it can make static specifications dynamic. Instead of bloating a specification with ways to define variables, reference those variables, or do operations like join strings, use Geneva to provide all of them on top of a specification.

The example below defines parameters, computed values, and finally passes them all to the definition. This definition could be OpenAPI, AsyncAPI, or any other JSON/YAML-based specification.

parameters:
  - name: firstName
    check: ref:isString
  - name: lastName
    required: true
computed:
  - name: fullName
    compute:
      fn:template: "{{firstName}} {{lastName}}"
definition:
  greeting:
    fn:template: |-
      Hello, {{fullName}}

You can use the CLI to pass in parameters and run this file.

geneva definition ./definition.yml ./params.yml

You can evaluate this in JavaScript with code like:

const fs = require("fs");
const { ConfigBuilder } = require("geneva");
const config = ConfigBuilder.fromYAML(yamlAbove, { fs });
const run = config.build({ firstName: "Jane", lastName: "Doe" });
const results = run();
// results is equal to { "greeting": "Hello, Jane Doe" }

Language overview

Geneva allows you to pass in data and process that data as if it were code. Geneva looks for objects with one key that is the function name prefixed with "fn:" and an array of arguments to pass to the array.

For example, to call the sum function with 1 and 2 as the arguments, do something like this:

# YAML example
fn:sum: [1, 2]

This is using Ramda's sum function, so it is the same as:

R.sum([1, 2]);

Geneva also allows for defining variables and referencing those variables throughout your code. This example shows code that defines a variable and then uses that variable in the next call. The way Geneva knows that a string is a reference is by prepending "ref:" to the variable name.

fn:do:
  - fn:def: [x, 10]
  - fn:sum: [ref:x, 5]
# result is 15

This example used do, which is a special function that evaluates everything and returns the value of the last function.

The reason for appending these special characters is so that plain data can be passed in. This means that it can evaluate code in deeply-nested objects.

Supported functions

Geneva includes functions from the following libraries for use in your code. The following are added directly into the scope.

  1. Ramda function
  2. Ramda Adjunct

These are namespaced.

  1. Saunter as saunter
  2. JSON Path as jq

Calling and referencing

You can call a function by creating an object with one key with the function name and prefixed with fn:.

fn:add: [1, 2]

You can use dot notation to reference nested functions.

fn:mycode.foo: [bar, baz]

To reference values instead of calling it, prefix the variable with ref:. You can use dot notation with refs as well.

ref:add

Defining functions

Geneva has limited support of functions (also called lambdas in this project). Any function defined will essentially freeze the existing scope and then scope all variables within it during the call. This means that functions cannot see any changes after they are defined, and they cannot make changes to the outer scope when they are called.

fn:do:
  - fn:def:
      - square
      - fn:lambda:
          - n
          - fn:multiply: [ref:n, ref:n]
  - fn:square: [4]

This defines a function as a variable square and then calls that function. The result from this code will be 16.

There is a defn shortcut for defining functions more easily.

fn:do:
  - fn:defn:
      - square
      - [n]
      - fn:multiply: [ref:n, ref:n]
  - fn:square: [4]

Conditionals

You can use an if statement in the code.

fn:if:
  - true
  - "Success!"
  - "Fail :("

If you leave out the else statement and the condition fails, you will get null (sorry).

Quote and Eval

Code can be "quoted" in the sense that it can be treated as a normal array rather than code. This allows for creating code on the fly (like a macro) if you so choose.

fn:quote:
  - fn:sum: [1, 2]
# returns fn:sum: [1, 2] unevaluated

You can evaluate quoted code as well.

fn:eval:
  - fn:quote:
      - fn:add: [1, 2]
# returns 3

Templates

Geneva includes a template function that uses Mustache and allows for rendering string that contain references. It will not allow for referencing anything in the provided in the supported libraries. It will however give you access to the local scope.

fn:do:
  - fn:def: [name, Jane Doe]
  - fn:template: Hello, {{name}}
# returns Hello, Jane Doe

You can also use templateFile to specify a file path and run that as a template instead. It will load file and run it just like calling the template function, which gives the template access to the scope.

fn:do:
  - fn:def: [name, Jane Doe]
  - fn:templateFile: ./say-hello.mustache

This is useful if you want to keep templates out of your main file.

Check out the Mustache manual for more information on using Mustache templates.

Including other files

You can use the include function to pull in a file and execute it in the current scope. It's expecting a YAML file. It will load it, parse it, then execute it.

fn:do:
  - fn:def: [name, Jane Doe]
  - fn:include: ./my-code.yml

In this example, the my-code.yml will have access to the name value.

Reading a file

Sometimes you just want to pull something out of a file. You can do this with readFile.

fn:readFile: ./my-file.txt

This does not execute the file or render as a template.

Usage

Using in JavaScript

You first need a code runner. Geneva tries not to know about the world it's in, so you'll need to pass in the fs module.

const fs = require("fs");
const { Geneva } = require("geneva");
const geneva = new Geneva({ fs });

You can use SimpleFS as a way to fake the file system. It only provides readFileSync because that's all Geneva uses.

const { Geneva, SimpleFS } = require("geneva");
const fs = new SimpleFS({
  "myfile.txt": "content here",
});
const geneva = new Geneva({ fs });

You can then run code as such:

// returns 3
geneva.run({ "fn:sum": [1, 2] });

If you are using JSON or YAML, you'll need to parse it first.

Initial data

You can pass initial data into Geneva in order to set up the scope before your code ever runs.

const geneva = new Geneva({
  initial: {
    foo: "bar",
  },
});
geneva.run("ref:foo"); // returns bar

Plain JavaScript functions may also be passed in and called directly in the code.

const geneva = new Geneva({
  initial: {
    hello: (name) => `Hello, ${name}`,
  },
});
geneva.run({ "fn:hello": ["World"] }); // return Hello, World

Forms

If you want to be able to evaluate code at runtime in your own function, you can pass in a special form to do so. This will pass in the raw code to your function along with the runtime for the given scope. Note that the runtime you get will be scoped to where the code is called, so the context will affect the scope.

This essentially allows you to modify the way the code itself executes. With great power comes great responsibility.

const geneva = new Geneva({
  forms: {
    hello: (runtime, args) => {
      // Evaluate the code passed to it
      const name = runtime.run(args[0]);
      return `Hello, ${name}`;
    },
  },
});
// return Hello, bar
geneva.run({
  "fn:hello": {
    "fn:join": ["b", "a", "r"],
  },
});

Custom runtime

Lastly, if you want to your own runtime to play with, you can call geneva.buildRuntime(), which takes the same options as geneva.run. This will give you access to the runtime to inspect and change the scope.

From the command line

If you install Geneva globally, you'll get the command line tool geneva.

Run code

geneva run ./code.yml

This will run a given file through Geneva.

Definition

geneva definition ./definition.yml ./params.yml

This will load a definition file and params and run them.

REPL

geneva repl

This will give you a prompt where you can directly type YAMP in Geneva code. Use .help to see other available commands.

Dependencies (8)

Dev Dependencies (2)

Package Sidebar

Install

npm i geneva

Weekly Downloads

0

Version

1.5.0

License

MIT

Unpacked Size

39.7 kB

Total Files

28

Last publish

Collaborators

  • smizell