Skip to content

Commit 52f9bb3

Browse files
authored
Merge pull request #47 from getlang-dev/implicit-urls
flexible urls with implied params
2 parents e977596 + a2304c8 commit 52f9bb3

11 files changed

Lines changed: 206 additions & 151 deletions

File tree

.changeset/petite-deer-pay.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@getlang/parser": patch
3+
"@getlang/ast": patch
4+
"@getlang/get": patch
5+
"@getlang/lib": patch
6+
---
7+
8+
flexible urls with implied params

packages/ast/src/ast.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type RequestStmt = {
4848
export type RequestExpr = {
4949
kind: 'RequestExpr'
5050
method: Token
51-
url: Expr
51+
url: TemplateExpr
5252
headers: RequestBlockExpr
5353
blocks: RequestBlockExpr[]
5454
body: Expr
@@ -214,7 +214,7 @@ const requestStmt = (request: RequestExpr): RequestStmt => ({
214214

215215
const requestExpr = (
216216
method: Token,
217-
url: Expr,
217+
url: TemplateExpr,
218218
headers: RequestBlockExpr,
219219
blocks: RequestBlockExpr[],
220220
body: Expr,

packages/get/src/modules.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { materialize } from './value.js'
1313

1414
type Info = {
1515
ast: Program
16-
inputs: Set<string>
1716
imports: Set<string>
1817
isMacro: boolean
1918
}
@@ -86,15 +85,20 @@ export class Modules {
8685
stack: string[],
8786
contextType?: TypeInfo,
8887
): Promise<Entry> {
89-
const { ast, inputs, imports } = await this.getInfo(module)
88+
const { ast, imports } = await this.getInfo(module)
9089
const macros: string[] = []
9190
for (const i of imports) {
9291
const depInfo = await this.getInfo(i)
9392
if (depInfo.isMacro) {
9493
macros.push(i)
9594
}
9695
}
97-
const { program: simplified, calls, modifiers } = desugar(ast, macros)
96+
const {
97+
program: simplified,
98+
inputs,
99+
calls,
100+
modifiers,
101+
} = desugar(ast, macros)
98102

99103
const returnTypes: Record<string, TypeInfo> = {}
100104
for (const call of calls) {

packages/lib/src/net/http.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,25 @@ export const requestHook: RequestHook = async (url, opts) => {
2828
}
2929
}
3030

31+
function constructUrl(start: string, query: StringMap = {}) {
32+
let url: URL
33+
let stripProtocol = false
34+
35+
try {
36+
url = new URL(start)
37+
} catch (_) {
38+
url = new URL(`http://${start}`)
39+
stripProtocol = true
40+
}
41+
42+
for (const entry of Object.entries(query)) {
43+
url.searchParams.append(...entry)
44+
}
45+
46+
const str = url.toString()
47+
return stripProtocol ? str.slice(7) : str
48+
}
49+
3150
export const request = async (
3251
method: string,
3352
url: string,
@@ -36,14 +55,7 @@ export const request = async (
3655
bodyRaw: string,
3756
hook: RequestHook,
3857
) => {
39-
// construct url
40-
const finalUrl = new URL(url)
41-
if (blocks.query) {
42-
for (const entry of Object.entries(blocks.query)) {
43-
finalUrl.searchParams.append(...entry)
44-
}
45-
}
46-
const urlString = finalUrl.toString()
58+
const urlString = constructUrl(url, blocks.query)
4759

4860
// construct headers
4961
const headers = new Headers(_headers)

packages/parser/src/passes/analyze.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@ import { ScopeTracker, transform } from '@getlang/walker'
33

44
export function analyze(ast: Program) {
55
const scope = new ScopeTracker()
6-
const inputs = new Set<string>()
76
const imports = new Set<string>()
87
let isMacro = false
98

109
transform(ast, {
1110
scope,
12-
InputExpr(node) {
13-
inputs.add(node.id.value)
14-
},
1511
ModuleExpr(node) {
1612
imports.add(node.module.value)
1713
},
@@ -23,5 +19,5 @@ export function analyze(ast: Program) {
2319
},
2420
})
2521

26-
return { inputs, imports, isMacro }
22+
return { imports, isMacro }
2723
}

packages/parser/src/passes/desugar.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { dropDrills } from './desugar/dropdrill.js'
55
import { settleLinks } from './desugar/links.js'
66
import { RequestParsers } from './desugar/reqparse.js'
77
import { insertSliceDeps } from './desugar/slicedeps.js'
8+
import { addUrlInputs } from './desugar/urlinputs.js'
89
import { registerCalls } from './inference/calls.js'
910

1011
export type DesugarPass = (
@@ -15,21 +16,33 @@ export type DesugarPass = (
1516
},
1617
) => Program
1718

18-
function listCalls(ast: Program) {
19+
function analyze2(ast: Program) {
20+
const inputs = new Set<string>()
1921
const calls = new Set<string>()
2022
const modifiers = new Set<string>()
23+
2124
transform(ast, {
25+
InputExpr(node) {
26+
inputs.add(node.id.value)
27+
},
2228
ModuleExpr(node) {
2329
node.call && calls.add(node.module.value)
2430
},
2531
ModifierExpr(node) {
2632
modifiers.add(node.modifier.value)
2733
},
2834
})
29-
return { calls, modifiers }
35+
36+
return { inputs, calls, modifiers }
3037
}
3138

32-
const visitors = [resolveContext, settleLinks, insertSliceDeps, dropDrills]
39+
const visitors = [
40+
addUrlInputs,
41+
resolveContext,
42+
settleLinks,
43+
insertSliceDeps,
44+
dropDrills,
45+
]
3346

3447
export function desugar(ast: Program, macros: string[] = []) {
3548
const parsers = new RequestParsers()
@@ -41,6 +54,6 @@ export function desugar(ast: Program, macros: string[] = []) {
4154
// inference pass `registerCalls` is included in the desugar phase
4255
// it produces the list of called modules required for type inference
4356
program = registerCalls(program, macros)
44-
const { calls, modifiers } = listCalls(program)
45-
return { program, calls, modifiers }
57+
const info = analyze2(program)
58+
return { program, ...info }
4659
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { TemplateExpr } from '@getlang/ast'
2+
import { isToken, t } from '@getlang/ast'
3+
import { ScopeTracker, transform } from '@getlang/walker'
4+
import { tx } from '../../utils.js'
5+
import type { DesugarPass } from '../desugar.js'
6+
7+
export const addUrlInputs: DesugarPass = ast => {
8+
const scope = new ScopeTracker()
9+
const implied = new Set<string>()
10+
11+
return transform(ast, {
12+
scope,
13+
14+
RequestExpr: {
15+
enter(node) {
16+
function walkUrl(t: TemplateExpr) {
17+
for (const el of t.elements) {
18+
if (isToken(el)) {
19+
// continue
20+
} else if (el.kind === 'TemplateExpr') {
21+
walkUrl(el)
22+
} else if (el.kind === 'IdentifierExpr') {
23+
const id = el.id.value
24+
if (el.isUrlComponent && !scope.vars[id]) {
25+
implied.add(el.id.value)
26+
}
27+
}
28+
}
29+
}
30+
31+
walkUrl(node.url)
32+
},
33+
},
34+
35+
Program(node) {
36+
if (implied.size) {
37+
let decl = node.body.find(s => s.kind === 'DeclInputsStmt')
38+
if (!decl) {
39+
decl = t.declInputsStmt([])
40+
node.body.unshift(decl)
41+
}
42+
for (const i of implied) {
43+
decl.inputs.push(t.InputExpr(tx.token(i), false))
44+
}
45+
}
46+
},
47+
})
48+
}

test/expect.ts

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,16 @@
11
import { expect } from 'bun:test'
22
import { diff } from 'jest-diff'
33

4-
async function toObject(req: Request) {
5-
return {
6-
url: req.url,
7-
method: req.method,
8-
headers: Object.fromEntries(req.headers),
9-
body: await req.text(),
10-
}
11-
}
12-
134
expect.extend({
14-
async toHaveServed(received: unknown, expected: Request) {
15-
const calls: [unknown][] = (received as any)?.mock?.calls
16-
const expObj = await toObject(expected)
5+
async toHaveServed(received: unknown, url: string, opts: RequestInit) {
6+
const calls: [unknown, any][] = (received as any)?.mock?.calls
7+
const { method, headers = {}, body } = opts
8+
const expObj = { url, method, headers, body }
179

1810
let receivedObj: any
1911

20-
for (const [req] of calls) {
21-
if (!(req instanceof Request)) {
22-
return {
23-
pass: false,
24-
message: () => `Received non-Request object: ${req}`,
25-
}
26-
}
27-
28-
const recObj = await toObject(req)
12+
for (const [url, { method, headers, body }] of calls) {
13+
const recObj = { url, method, headers, body }
2914
receivedObj ??= recObj
3015
const pass = this.equals(recObj, expObj)
3116
if (pass) {

test/helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type ExecuteOptions = Partial<{
1313
willThrow: boolean
1414
}>
1515

16-
export type Fetch = (req: Request) => MaybePromise<Response>
16+
export type Fetch = (url: string, opts: RequestInit) => MaybePromise<Response>
1717

1818
export const SELSYN = true
1919

@@ -47,7 +47,7 @@ export async function execute(
4747
},
4848
async request(url, opts) {
4949
invariant(fetch, `Fetch required: ${url}`)
50-
const res = await fetch(new Request(url, opts))
50+
const res = await fetch(url, opts)
5151
return {
5252
status: res.status,
5353
headers: res.headers,

0 commit comments

Comments
 (0)