Skip to content

Commit

Permalink
Merge pull request #637 from OpenFn/lazy-state-operator
Browse files Browse the repository at this point in the history
Lazy State Operator
  • Loading branch information
josephjclark authored Mar 22, 2024
2 parents 66fd7da + fec79c2 commit b44b7dd
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-kids-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openfn/compiler': minor
---

Basic support for lazy state ($) operator
7 changes: 5 additions & 2 deletions packages/compiler/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import createLogger, { Logger } from '@openfn/logger';

import addImports, { AddImportsOptions } from './transforms/add-imports';
import ensureExports from './transforms/ensure-exports';
import lazyState from './transforms/lazy-state';
import topLevelOps, {
TopLevelOpsOptions,
} from './transforms/top-level-operations';
Expand All @@ -13,7 +14,8 @@ export type TransformerName =
| 'add-imports'
| 'ensure-exports'
| 'top-level-operations'
| 'test';
| 'test'
| 'lazy-state';

type TransformFunction = (
path: NodePath<any, any>,
Expand All @@ -36,6 +38,7 @@ export type TransformOptions = {
['ensure-exports']?: boolean;
['top-level-operations']?: TopLevelOpsOptions | boolean;
['test']?: any;
['lazy-state']?: any;
};

const defaultLogger = createLogger();
Expand All @@ -46,7 +49,7 @@ export default function transform(
options: TransformOptions = {}
) {
if (!transformers) {
transformers = [ensureExports, topLevelOps, addImports] as Transformer[];
transformers = [lazyState, ensureExports, topLevelOps, addImports] as Transformer[];
}
const logger = options.logger || defaultLogger;
const transformerIndex = indexTransformers(transformers, options);
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/transforms/add-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type { Transformer } from '../transform';
import type { Logger } from '@openfn/logger';

const globals = [
'\\$', // TMP hack to fix a problem with lazy-state (needs double escaping to work)

'AggregateError',
'Array',
'ArrayBuffer',
Expand Down
46 changes: 46 additions & 0 deletions packages/compiler/src/transforms/lazy-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Convert $.a.b.c references into (state) => state.a.b.c
* Should this only run at top level?
* Ideally it would run on all arguments to operations - but we probably don't really know what an operation is
* So for now, first pass, it's only top level.
* (alternatively I guess it just dumbly converts everything and if it breaks, it breaks)
*
* TODO (maybe):
* - only convert $-expressions which are arguments to operations (needs type defs)
* - warn if converting a non-top-level $-expression
* - if not top level, convert to state.a.b.c (ie don't wrap the function)
*/
import { builders as b, namedTypes } from 'ast-types';
import type { NodePath } from 'ast-types/lib/node-path';
import type { Transformer } from '../transform';

function visitor(path: NodePath<namedTypes.MemberExpression>) {
let first = path.node.object;
while(first.hasOwnProperty('object')) {
first = (first as namedTypes.MemberExpression).object;
}

let firstIdentifer = first as namedTypes.Identifier;

if (first && firstIdentifer.name === "$") {
// rename $ to state
firstIdentifer.name = "state";

// Now nest the whole thing in an arrow
const params = b.identifier('state')
const arrow = b.arrowFunctionExpression(
[params],
path.node
)
path.replace(arrow)
}

// Stop parsing this member expression
return;
}

export default {
id: 'lazy-state',
types: ['MemberExpression'],
visitor,
} as Transformer;
26 changes: 26 additions & 0 deletions packages/compiler/test/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,30 @@ test('compile with nullish coalescence', (t) => {
const expected = 'export default [fn(a ?? b)];';
const result = compile(source);
t.assert(result === expected);
});

test('compile a lazy state ($) expression', (t) => {
const source = 'get($.data.endpoint);';
const expected = 'export default [get(state => state.data.endpoint)];';
const result = compile(source);
t.assert(result === expected);
});


test('compile a lazy state ($) expression with dumb imports', (t) => {
const options = {
'add-imports': {
adaptor: {
name: '@openfn/language-common',
exportAll: true,
},
},
};
const source = 'get($.data.endpoint);';
const expected = `import { get } from "@openfn/language-common";
export * from "@openfn/language-common";
export default [get(state => state.data.endpoint)];`

const result = compile(source, options);
t.assert(result === expected);
});
76 changes: 76 additions & 0 deletions packages/compiler/test/transforms/lazy-state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import test, { ExecutionContext } from 'ava';
import { print } from 'recast';
import { namedTypes, NodePath, builders as b } from 'ast-types';

import parse from '../../src/parse';

import transform from '../../src/transform';
import visitors from '../../src/transforms/lazy-state';

test('convert a simple dollar reference', (t) => {
const ast = parse('get($.data)');

const transformed = transform(ast, [visitors]);
const { code } = print(transformed)
t.log(code)

t.is(code, 'get(state => state.data)')
})

test('convert a chained dollar reference', (t) => {
const ast = parse('get($.a.b.c.d)');

const transformed = transform(ast, [visitors]);
const { code } = print(transformed)
t.log(code)

t.is(code, 'get(state => state.a.b.c.d)')
})

test('ignore a regular chain reference', (t) => {
const ast = parse('get(a.b.c.d)');

const transformed = transform(ast, [visitors]);
const { code } = print(transformed)
t.log(code)

t.is(code, 'get(a.b.c.d)')
})

test('ignore a string', (t) => {
const ast = parse('get("$.a.b")');

const transformed = transform(ast, [visitors]);
const { code } = print(transformed)
t.log(code)

t.is(code, 'get("$.a.b")')
})

// TODO do we want to support this?
test('convert a nested dollar reference', (t) => {
const ast = parse(`fn(() => {
get($.data)
})`);

const transformed = transform(ast, [visitors]);
const { code } = print(transformed)
t.log(code)

// syntax starts getting a but picky at this level,
// better to do ast tests
t.is(code, `fn(() => {
get(state => state.data)
})`)
})

// TODO does our compiler not support optional chaining??
test.skip('convert an optional chained simple dollar reference', (t) => {
const ast = parse('get($.a?.b.c.d)');

// const transformed = transform(ast, [visitors]);
// const { code } = print(transformed)
// t.log(code)

// t.is(code, 'get(state => state.a?.b.c.d)')
})

0 comments on commit b44b7dd

Please sign in to comment.