diff --git a/.changeset/green-wings-help.md b/.changeset/green-wings-help.md new file mode 100644 index 0000000..866fdc0 --- /dev/null +++ b/.changeset/green-wings-help.md @@ -0,0 +1,6 @@ +--- +"@getlang/parser": patch +"@getlang/get": patch +--- + +fix modifier context (ii) diff --git a/packages/get/src/calls.ts b/packages/get/src/calls.ts index 096bdc8..bdf96d8 100644 --- a/packages/get/src/calls.ts +++ b/packages/get/src/calls.ts @@ -16,13 +16,9 @@ export async function callModifier( ) { const entry = await registry.importMod(mod) if (entry) { - let ctx: any - if (entry?.useContext) { - if (context && entry.materialize) { - ctx = materialize(context) - } else { - ctx = context?.data - } + let ctx: any = {} + if (entry.useContext && context) { + ctx = entry.materialize ? materialize(context) : context.data } return entry.mod(ctx, args) } diff --git a/packages/get/src/registry.ts b/packages/get/src/registry.ts index b317165..878e6d9 100644 --- a/packages/get/src/registry.ts +++ b/packages/get/src/registry.ts @@ -90,13 +90,15 @@ export class Registry { const info = analyze(ast) const imports = [...info.imports] const contextMods: string[] = [] - for (const mod of info.modifiers) { + for (const mod of info.modifiers.keys()) { const entry = await this.importMod(mod) if (entry?.useContext ?? true) { contextMods.push(mod) } } - const isMacro = info.hasUnboundSelector || contextMods.length > 0 + const isMacro = + info.hasUnboundSelector || + contextMods.some(mod => info.modifiers.get(mod)) return { ast, imports, contextMods, isMacro } }) return this.info[module] diff --git a/packages/parser/src/passes/analyze.ts b/packages/parser/src/passes/analyze.ts index 66f00df..3059056 100644 --- a/packages/parser/src/passes/analyze.ts +++ b/packages/parser/src/passes/analyze.ts @@ -5,7 +5,7 @@ export function analyze(ast: Program) { const scope = new ScopeTracker() const inputs = new Set() const calls = new Set() - const modifiers = new Set() + const modifiers = new Map() const imports = new Set() let hasUnboundSelector = false @@ -22,7 +22,10 @@ export function analyze(ast: Program) { hasUnboundSelector ||= !scope.context }, ModifierExpr(node) { - modifiers.add(node.modifier.value) + const mod = node.modifier.value + if (!modifiers.get(mod)) { + modifiers.set(mod, !scope.context) + } }, }) diff --git a/test/calls.spec.ts b/test/calls.spec.ts deleted file mode 100644 index 3b4f005..0000000 --- a/test/calls.spec.ts +++ /dev/null @@ -1,357 +0,0 @@ -import { describe, expect, test } from 'bun:test' -import { RecursiveCallError } from '@getlang/lib/errors' -import { execute } from './helpers.js' - -describe('calls', () => { - test('modules', async () => { - const result = await execute({ - Auth: 'extract { token: `"abc"` }', - Home: `extract { - auth: @Auth -> token - }`, - }) - expect(result).toEqual({ - auth: 'abc', - }) - }) - - describe('semantics', () => { - const Call = ` - inputs { called } - extract { called: |true| } - ` - - test('select', async () => { - const modules = { - Call, - Home: ` - set called = |false| - extract { - select: @Call({ $called }) -> called - } - `, - } - const result = await execute(modules) - expect(result).toEqual({ select: true }) - }) - - test('from var', async () => { - const modules = { - Call, - Home: ` - set called = |false| - set as_var = @Call({ $called }) - extract { - from_var: $as_var -> called - } - `, - } - const result = await execute(modules) - expect(result).toEqual({ from_var: true }) - }) - - test('subquery', async () => { - const modules = { - Call, - Home: ` - set called = |false| - extract { - subquery: ( - extract @Call({ $called }) - ) -> called - } - `, - } - const result = await execute(modules) - expect(result).toEqual({ subquery: true }) - }) - - test('from subquery', async () => { - const modules = { - Call, - Home: ` - set called = |false| - set as_subquery = ( extract @Call({ $called }) ) - - extract { - from_subquery: $as_subquery -> called - } - `, - } - const result = await execute(modules) - expect(result).toEqual({ from_subquery: true }) - }) - - test.skip('object key', async () => { - const modules = { - Call, - Home: ` - set called = |false| - extract { - object: as_entry = { key: @Call({ $called }) } -> key -> called - } - `, - } - const result = await execute(modules) - expect(result).toEqual({ object: true }) - }) - }) - - test('drill return value', async () => { - const modules = { - Req: ` - GET http://stub - - extract @html - `, - Home: ` - set req = @Req - extract $req -> { div, span } - `, - } - - const result = await execute( - modules, - {}, - { - fetch: () => new Response(`
x
y`), - }, - ) - expect(result).toEqual({ div: 'x', span: 'y' }) - }) - - test.skip('drill returned request', async () => { - const modules = { - Req: ` - GET http://stub - - extract $ - `, - Home: ` - set req = @Req - extract $req -> { div, span } - `, - } - - const result = await execute( - modules, - {}, - { - fetch: () => new Response(`
x
y`), - }, - ) - expect(result).toEqual({ div: 'x', span: 'y' }) - }) - - test('links', async () => { - const modules = { - Search: 'extract `1`', - Product: 'extract `2`', - Home: ` - inputs { query, page? } - - GET https://search.com/ - [query] - s: $query - page: $page - - set results = => li.result -> @Product({ - @link: a - name: a - desc: p.description - }) - - extract { - items: $results - pager: .pager -> { - next: @Search) a.next - prev: @Search) a.prev - } - } - `, - } - - const result = await execute( - modules, - { query: 'gifts' }, - { - fetch: () => - new Response(` - - -
- -
- `), - }, - ) - expect(result).toEqual({ - items: [ - { - '@link': 'https://search.com/products/1', - name: 'Deck o cards', - desc: 'Casino grade playing cards', - }, - ], - pager: { - next: { - '@link': 'https://search.com/?s=gifts&page=2', - }, - prev: {}, - }, - }) - }) - - test('links pre/post-amble', async () => { - const modules = { - Home: ` - GET http://stub/x/y/z - - extract #a - -> :scope > #b - -> @Link) :scope > #c - -> :scope > #d - `, - Link: ` - extract { - _module: |'Link'| - } - `, - } - const result = await execute( - modules, - {}, - { - fetch: () => - new Response(` - - -
-
-
- link -
-
- - `), - }, - ) - - expect(result).toEqual({ - '@link': 'http://stub/a/b/c/d', - }) - }) - - test('context propagation', async () => { - const modules = { - Home: ` - GET http://stub - - extract { - a: @Data({ text: |'first'| }) - b: @Data({ text: |'second'| }) - } - `, - Data: ` - inputs { text } - - extract //div[p[contains(text(), '$text')]] - -> ./@data-json - -> @json - `, - } - - const result = await execute( - modules, - {}, - { - fetch: () => - new Response(` - -

first

-

second

- `), - }, - ) - - expect(result).toEqual({ - a: { x: 1 }, - b: { y: 2 }, - }) - }) - - test('drill macro return types', async () => { - const modules = { - Home: ` - GET http://stub - - extract @Data({text: |'first'|}) - -> ./@data-json - -> @json - `, - Data: ` - inputs { text } - - extract //div[p[contains(text(), '$text')]] - `, - } - - const result = await execute( - modules, - {}, - { - fetch: () => - new Response(`

first

`), - }, - ) - - expect(result).toEqual({ x: 1 }) - }) - - test('recursive', async () => { - const modules = { - Home: ` - extract { - value: @Page -> value - } - `, - Page: ` - extract { - value: @Home -> value - } - `, - } - - const result = execute(modules) - return expect(result).rejects.toThrow( - new RecursiveCallError(['Home', 'Page', 'Home']), - ) - }) - - test('recursive link not called', async () => { - const modules = { - Home: ` - extract { - page: @Page -> value - } - `, - Page: ` - extract { - value: @Home({ - called: |false| - }) - } - `, - } - - const result = await execute(modules) - expect(result).toEqual({ - page: { called: false }, - }) - }) -}) diff --git a/test/modifiers.spec.ts b/test/modifiers.spec.ts index 013b5b5..c91ef55 100644 --- a/test/modifiers.spec.ts +++ b/test/modifiers.spec.ts @@ -50,7 +50,7 @@ describe('modifiers', () => { `, 'nocontext', ctx => { - expect(ctx).toBeUndefined() + expect(ctx).toEqual({}) return 123 }, false, diff --git a/test/modules.spec.ts b/test/modules.spec.ts index 3276db8..37455a9 100644 --- a/test/modules.spec.ts +++ b/test/modules.spec.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from 'bun:test' -import { NullInputError, UnknownInputsError } from '@getlang/lib/errors' +import { + NullInputError, + RecursiveCallError, + UnknownInputsError, +} from '@getlang/lib/errors' import { execute } from './helpers.js' describe('modules', () => { @@ -119,4 +123,374 @@ describe('modules', () => { `) expect(result).toEqual({ test: true }) }) + + describe('calls', () => { + test('modules', async () => { + const result = await execute({ + Auth: 'extract { token: `"abc"` }', + Home: `extract { + auth: @Auth -> token + }`, + }) + expect(result).toEqual({ + auth: 'abc', + }) + }) + + describe('semantics', () => { + const Call = ` + inputs { called } + extract { called: |true| } + ` + + test('select', async () => { + const modules = { + Call, + Home: ` + set called = |false| + extract { + select: @Call({ $called }) -> called + } + `, + } + const result = await execute(modules) + expect(result).toEqual({ select: true }) + }) + + test('from var', async () => { + const modules = { + Call, + Home: ` + set called = |false| + set as_var = @Call({ $called }) + extract { + from_var: $as_var -> called + } + `, + } + const result = await execute(modules) + expect(result).toEqual({ from_var: true }) + }) + + test('subquery', async () => { + const modules = { + Call, + Home: ` + set called = |false| + extract { + subquery: ( + extract @Call({ $called }) + ) -> called + } + `, + } + const result = await execute(modules) + expect(result).toEqual({ subquery: true }) + }) + + test('from subquery', async () => { + const modules = { + Call, + Home: ` + set called = |false| + set as_subquery = ( extract @Call({ $called }) ) + + extract { + from_subquery: $as_subquery -> called + } + `, + } + const result = await execute(modules) + expect(result).toEqual({ from_subquery: true }) + }) + + test.skip('object key', async () => { + const modules = { + Call, + Home: ` + set called = |false| + extract { + object: as_entry = { key: @Call({ $called }) } -> key -> called + } + `, + } + const result = await execute(modules) + expect(result).toEqual({ object: true }) + }) + }) + + test('drill return value', async () => { + const modules = { + Req: ` + GET http://stub + + extract @html + `, + Home: ` + set req = @Req + extract $req -> { div, span } + `, + } + + const result = await execute( + modules, + {}, + { + fetch: () => + new Response(`
x
y`), + }, + ) + expect(result).toEqual({ div: 'x', span: 'y' }) + }) + + test.skip('drill returned request', async () => { + const modules = { + Req: ` + GET http://stub + + extract $ + `, + Home: ` + set req = @Req + extract $req -> { div, span } + `, + } + + const result = await execute( + modules, + {}, + { + fetch: () => + new Response(`
x
y`), + }, + ) + expect(result).toEqual({ div: 'x', span: 'y' }) + }) + + test('links', async () => { + const modules = { + Search: 'extract `1`', + Product: 'extract `2`', + Home: ` + inputs { query, page? } + + GET https://search.com/ + [query] + s: $query + page: $page + + set results = => li.result -> @Product({ + @link: a + name: a + desc: p.description + }) + + extract { + items: $results + pager: .pager -> { + next: @Search) a.next + prev: @Search) a.prev + } + } + `, + } + + const result = await execute( + modules, + { query: 'gifts' }, + { + fetch: () => + new Response(` + + +
+ +
+ `), + }, + ) + expect(result).toEqual({ + items: [ + { + '@link': 'https://search.com/products/1', + name: 'Deck o cards', + desc: 'Casino grade playing cards', + }, + ], + pager: { + next: { + '@link': 'https://search.com/?s=gifts&page=2', + }, + prev: {}, + }, + }) + }) + + test('links pre/post-amble', async () => { + const modules = { + Home: ` + GET http://stub/x/y/z + + extract #a + -> :scope > #b + -> @Link) :scope > #c + -> :scope > #d + `, + Link: ` + extract { + _module: |'Link'| + } + `, + } + const result = await execute( + modules, + {}, + { + fetch: () => + new Response(` + + +
+
+
+ link +
+
+ + `), + }, + ) + + expect(result).toEqual({ + '@link': 'http://stub/a/b/c/d', + }) + }) + + test('context propagation', async () => { + const modules = { + Home: ` + GET http://stub + + extract { + a: @Data({ text: |'first'| }) + b: @Data({ text: |'second'| }) + } + `, + Data: ` + inputs { text } + + extract //div[p[contains(text(), '$text')]] + -> ./@data-json + -> @json + `, + } + + const result = await execute( + modules, + {}, + { + fetch: () => + new Response(` + +

first

+

second

+ `), + }, + ) + + expect(result).toEqual({ + a: { x: 1 }, + b: { y: 2 }, + }) + }) + + test('drill macro return types', async () => { + const modules = { + Home: ` + GET http://stub + + extract @Data({text: |'first'|}) + -> ./@data-json + -> @json + `, + Data: ` + inputs { text } + + extract //div[p[contains(text(), '$text')]] + `, + } + + const result = await execute( + modules, + {}, + { + fetch: () => + new Response(`

first

`), + }, + ) + + expect(result).toEqual({ x: 1 }) + }) + + test('recursive', async () => { + const modules = { + Home: ` + extract { + value: @Page -> value + } + `, + Page: ` + extract { + value: @Home -> value + } + `, + } + + const result = execute(modules) + return expect(result).rejects.toThrow( + new RecursiveCallError(['Home', 'Page', 'Home']), + ) + }) + + test('recursive link not called', async () => { + const modules = { + Home: ` + extract { + page: @Page -> value + } + `, + Page: ` + extract { + value: @Home({ + called: |false| + }) + } + `, + } + + const result = await execute(modules) + expect(result).toEqual({ + page: { called: false }, + }) + }) + + test('non-macro not called', async () => { + const modules = { + NotMacro: ` + extract "

100

" -> @html -> p + `, + Home: ` + extract @NotMacro({ num: 0 }) + `, + } + + const result = await execute(modules) + expect(result).toEqual({ num: 0 }) + }) + }) })