Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/petite-deer-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@getlang/parser": patch
"@getlang/ast": patch
"@getlang/get": patch
"@getlang/lib": patch
---

flexible urls with implied params
4 changes: 2 additions & 2 deletions packages/ast/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ type RequestStmt = {
export type RequestExpr = {
kind: 'RequestExpr'
method: Token
url: Expr
url: TemplateExpr
headers: RequestBlockExpr
blocks: RequestBlockExpr[]
body: Expr
Expand Down Expand Up @@ -214,7 +214,7 @@ const requestStmt = (request: RequestExpr): RequestStmt => ({

const requestExpr = (
method: Token,
url: Expr,
url: TemplateExpr,
headers: RequestBlockExpr,
blocks: RequestBlockExpr[],
body: Expr,
Expand Down
10 changes: 7 additions & 3 deletions packages/get/src/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { materialize } from './value.js'

type Info = {
ast: Program
inputs: Set<string>
imports: Set<string>
isMacro: boolean
}
Expand Down Expand Up @@ -86,15 +85,20 @@ export class Modules {
stack: string[],
contextType?: TypeInfo,
): Promise<Entry> {
const { ast, inputs, imports } = await this.getInfo(module)
const { ast, imports } = await this.getInfo(module)
const macros: string[] = []
for (const i of imports) {
const depInfo = await this.getInfo(i)
if (depInfo.isMacro) {
macros.push(i)
}
}
const { program: simplified, calls, modifiers } = desugar(ast, macros)
const {
program: simplified,
inputs,
calls,
modifiers,
} = desugar(ast, macros)

const returnTypes: Record<string, TypeInfo> = {}
for (const call of calls) {
Expand Down
28 changes: 20 additions & 8 deletions packages/lib/src/net/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ export const requestHook: RequestHook = async (url, opts) => {
}
}

function constructUrl(start: string, query: StringMap = {}) {
let url: URL
let stripProtocol = false

try {
url = new URL(start)
} catch (_) {
url = new URL(`http://${start}`)
stripProtocol = true
}

for (const entry of Object.entries(query)) {
url.searchParams.append(...entry)
}

const str = url.toString()
return stripProtocol ? str.slice(7) : str
}

export const request = async (
method: string,
url: string,
Expand All @@ -36,14 +55,7 @@ export const request = async (
bodyRaw: string,
hook: RequestHook,
) => {
// construct url
const finalUrl = new URL(url)
if (blocks.query) {
for (const entry of Object.entries(blocks.query)) {
finalUrl.searchParams.append(...entry)
}
}
const urlString = finalUrl.toString()
const urlString = constructUrl(url, blocks.query)

// construct headers
const headers = new Headers(_headers)
Expand Down
6 changes: 1 addition & 5 deletions packages/parser/src/passes/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@ import { ScopeTracker, transform } from '@getlang/walker'

export function analyze(ast: Program) {
const scope = new ScopeTracker()
const inputs = new Set<string>()
const imports = new Set<string>()
let isMacro = false

transform(ast, {
scope,
InputExpr(node) {
inputs.add(node.id.value)
},
ModuleExpr(node) {
imports.add(node.module.value)
},
Expand All @@ -23,5 +19,5 @@ export function analyze(ast: Program) {
},
})

return { inputs, imports, isMacro }
return { imports, isMacro }
}
23 changes: 18 additions & 5 deletions packages/parser/src/passes/desugar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { dropDrills } from './desugar/dropdrill.js'
import { settleLinks } from './desugar/links.js'
import { RequestParsers } from './desugar/reqparse.js'
import { insertSliceDeps } from './desugar/slicedeps.js'
import { addUrlInputs } from './desugar/urlinputs.js'
import { registerCalls } from './inference/calls.js'

export type DesugarPass = (
Expand All @@ -15,21 +16,33 @@ export type DesugarPass = (
},
) => Program

function listCalls(ast: Program) {
function analyze2(ast: Program) {
const inputs = new Set<string>()
const calls = new Set<string>()
const modifiers = new Set<string>()

transform(ast, {
InputExpr(node) {
inputs.add(node.id.value)
},
ModuleExpr(node) {
node.call && calls.add(node.module.value)
},
ModifierExpr(node) {
modifiers.add(node.modifier.value)
},
})
return { calls, modifiers }

return { inputs, calls, modifiers }
}

const visitors = [resolveContext, settleLinks, insertSliceDeps, dropDrills]
const visitors = [
addUrlInputs,
resolveContext,
settleLinks,
insertSliceDeps,
dropDrills,
]

export function desugar(ast: Program, macros: string[] = []) {
const parsers = new RequestParsers()
Expand All @@ -41,6 +54,6 @@ export function desugar(ast: Program, macros: string[] = []) {
// inference pass `registerCalls` is included in the desugar phase
// it produces the list of called modules required for type inference
program = registerCalls(program, macros)
const { calls, modifiers } = listCalls(program)
return { program, calls, modifiers }
const info = analyze2(program)
return { program, ...info }
}
48 changes: 48 additions & 0 deletions packages/parser/src/passes/desugar/urlinputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { TemplateExpr } from '@getlang/ast'
import { isToken, t } from '@getlang/ast'
import { ScopeTracker, transform } from '@getlang/walker'
import { tx } from '../../utils.js'
import type { DesugarPass } from '../desugar.js'

export const addUrlInputs: DesugarPass = ast => {
const scope = new ScopeTracker()
const implied = new Set<string>()

return transform(ast, {
scope,

RequestExpr: {
enter(node) {
function walkUrl(t: TemplateExpr) {
for (const el of t.elements) {
if (isToken(el)) {
// continue
} else if (el.kind === 'TemplateExpr') {
walkUrl(el)
} else if (el.kind === 'IdentifierExpr') {
const id = el.id.value
if (el.isUrlComponent && !scope.vars[id]) {
implied.add(el.id.value)
}
}
}
}

walkUrl(node.url)
},
},

Program(node) {
if (implied.size) {
let decl = node.body.find(s => s.kind === 'DeclInputsStmt')
if (!decl) {
decl = t.declInputsStmt([])
node.body.unshift(decl)
}
for (const i of implied) {
decl.inputs.push(t.InputExpr(tx.token(i), false))
}
}
},
})
}
27 changes: 6 additions & 21 deletions test/expect.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,16 @@
import { expect } from 'bun:test'
import { diff } from 'jest-diff'

async function toObject(req: Request) {
return {
url: req.url,
method: req.method,
headers: Object.fromEntries(req.headers),
body: await req.text(),
}
}

expect.extend({
async toHaveServed(received: unknown, expected: Request) {
const calls: [unknown][] = (received as any)?.mock?.calls
const expObj = await toObject(expected)
async toHaveServed(received: unknown, url: string, opts: RequestInit) {
const calls: [unknown, any][] = (received as any)?.mock?.calls
const { method, headers = {}, body } = opts
const expObj = { url, method, headers, body }

let receivedObj: any

for (const [req] of calls) {
if (!(req instanceof Request)) {
return {
pass: false,
message: () => `Received non-Request object: ${req}`,
}
}

const recObj = await toObject(req)
for (const [url, { method, headers, body }] of calls) {
const recObj = { url, method, headers, body }
receivedObj ??= recObj
const pass = this.equals(recObj, expObj)
if (pass) {
Expand Down
4 changes: 2 additions & 2 deletions test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type ExecuteOptions = Partial<{
willThrow: boolean
}>

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

export const SELSYN = true

Expand Down Expand Up @@ -47,7 +47,7 @@ export async function execute(
},
async request(url, opts) {
invariant(fetch, `Fetch required: ${url}`)
const res = await fetch(new Request(url, opts))
const res = await fetch(url, opts)
return {
status: res.status,
headers: res.headers,
Expand Down
Loading