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
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ Generate the README from the mz file.
```bash bash
markatzea ./README.mz | tee ./README.md

npx markdown-toc -i README.md
npx documentation readme references.mjs parse.mjs stringify.mjs -s Reference --github --a public --markdown-toc false

npx markdown-toc --maxdepth 3 -i README.md

git add *.md ./example
```
4 changes: 4 additions & 0 deletions README.mz
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ piqued your interest.
SendScript leaves it up to you to choose HTTP, web-sockets or any other method
of communication between servers and clients that best fits your needs.

## Reference

<!-- Reference -->

## Socket example

For this example we'll use [socket.io][socket.io].
Expand Down
80 changes: 51 additions & 29 deletions parse.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ function flattenSchema (schema) {

for (const item of schema) {
if (typeof item === 'string') {
// leaf function
obj[item] = true
} else if (Array.isArray(item)) {
const [name, children] = item

if (!Array.isArray(children)) {
throw new Error(`Expected children array for namespace "${name}"`)
}

obj[name] = flattenSchema(children)
} else {
throw new Error('Schema items must be strings or [name, children] arrays')
Expand All @@ -38,7 +39,6 @@ const spy = (type, fn) => (...args) => {
return value
}

// Recursively resolve awaited values in a parsed tree
const evaluate = spy('eval', (value, awaits = []) => {
if (value === undefinedSentinel) return undefined

Expand All @@ -47,49 +47,41 @@ const evaluate = spy('eval', (value, awaits = []) => {

if (operator === 'await') {
const [index] = rest
// No need to check index. It is closely tied to the program.
// if (typeof index !== 'number' || index < 0 || index >= awaitsResolved.length) {
// throw new Error(`Invalid await index: ${index}`);
// }
return awaits[index]
}

if (operator === 'then') {
const [v, onResolve, onReject] = rest
return evaluate(v, awaits).then(evaluate(onResolve, awaits), evaluate(onReject, awaits))
return evaluate(v, awaits).then(
evaluate(onResolve, awaits),
evaluate(onReject, awaits)
)
}

if (operator === 'call') {
// Step 1: evaluate the function itself
let [fn, args] = rest
fn = evaluate(fn, awaits)

// Step 2: evaluate each argument AFTER fn is ready
for (let i = 0; i < args.length; i++) {
args[i] = evaluate(args[i], awaits)
}

// Step 3: call the function
return fn(...args)
}

if (operator === 'quote') {
const [quoted] = rest
return quoted // return as-is without evaluating
return quoted
}

// fallback: evaluate each element
// re-uses the array again.

for (let index = 0; index < value.length; index++) {
const item = value[index]
value[index] = evaluate(item, awaits)
for (let i = 0; i < value.length; i++) {
value[i] = evaluate(value[i], awaits)
}

return value
}

if (isPlainObject(value)) {
// We muatate the object itself. No need to make a new one.
for (const key of Object.keys(value)) {
value[key] = evaluate(value[key], awaits)
}
Expand All @@ -99,23 +91,56 @@ const evaluate = spy('eval', (value, awaits = []) => {
return value
})

/**
* Default deserializer for leaf nodes.
*
* @param {string} text
* @returns {any}
* @public
*/
const defaultLeafDeserializer = (text) => JSON.parse(text)

export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
/**
* Creates a program parser for a given schema and environment.
*
* The parser:
* - resolves references into runtime functions
* - deserializes leaf nodes
* - collects and executes async awaits
* - evaluates AST-like JSON programs
*
* @param {Array<string | [string, Array]>} schemaArg
* @param {Object} env - runtime environment for refs
* @param {(text: string) => any} [deserialize=defaultLeafDeserializer]
* @returns {(program: string) => any|Promise<any>}
* @public
*/
export default function Parse (schemaArg, env, deserialize = defaultLeafDeserializer) {
const schema = flattenSchema(schemaArg)

/**
* Parses and executes a serialized program.
*
* @param {string} program - JSON encoded program
* @returns {any|Promise<any>}
* @public
*/
return function parse (program) {
debug('program', program)
const awaits = []

// Creates the list of awaits that will resolve in order
// and also deserializes the leaves.
/**
* JSON reviver used during parsing.
* Converts encoded operators into runtime structures.
*
* @param {string} key
* @param {any} value
* @returns {any}
*/
const reviver = spy('revive', (key, value) => {
if (value === null) return value

if (!Array.isArray(value)) {
return value
}
if (!Array.isArray(value)) return value

const [operator, ...rest] = value

Expand All @@ -126,7 +151,6 @@ export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {

if (operator === 'await') {
const [program] = rest

return ['await', awaits.push(program) - 1]
}

Expand All @@ -151,19 +175,17 @@ export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
})

const parsed = JSON.parse(program, reviver)

debug('parsed', parsed)

if (awaits.length) {
debug('awaits', awaits)

return (async function () {
for (let index = 0; index < awaits.length; index++) {
awaits[index] = await evaluate(awaits[index], awaits)
for (let i = 0; i < awaits.length; i++) {
awaits[i] = await evaluate(awaits[i], awaits)
}

debug('awaits(awaited)', awaits)

return evaluate(parsed, awaits)
})()
}
Expand Down
63 changes: 57 additions & 6 deletions references.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { awaitSymbol, call, ref, then, referenceSymbol } from './symbol.mjs'

/**
* Creates a callable reference node bound to a path.
* Used to build a lazy/instrumented execution structure.
*
* @param {Array<string>} path - Path representing the function location in schema.
* @returns {Function} Reference function with attached control methods (.then, .catch, toJSON).
*/
function instrument (path) {
/**
* Creates a callable reference invocation.
*
* @param {...any} args - Arguments passed to the call.
* @returns {Function} New instrumented reference node.
*/
function reference (...args) {
const called = instrument(path)

Expand All @@ -13,6 +26,13 @@ function instrument (path) {
return called
}

/**
* Internal helper for building promise-like continuation nodes.
*
* @param {Function|null} resolve
* @param {Function|null} reject
* @returns {Function} Instrumented continuation node.
*/
function dotThen (resolve, reject) {
const node = instrument(path)

Expand All @@ -26,18 +46,32 @@ function instrument (path) {
return node
}

/**
* Registers rejection handler (promise-style).
*
* @param {Function} reject
* @returns {Function}
*/
reference.catch = (reject) => {
return dotThen(null, reject)
}

/**
* Handles async chaining or awaiting logic.
*
* If resolve/reject contain a reference marker, it behaves like a .then chain.
* Otherwise it behaves like an await wrapper.
*
* @param {Function} resolve
* @param {Function} reject
* @returns {Function|any}
*/
reference.then = (resolve, reject) => {
// That is how we know if it is an await or a .then call.
if (resolve?.[referenceSymbol] || reject?.[referenceSymbol]) {
return dotThen(resolve, reject)
}

const awaited = instrument(path)
// Prevent infinite recur
delete awaited.then

awaited.toJSON = () => ({
Expand All @@ -48,6 +82,11 @@ function instrument (path) {
return resolve(awaited)
}

/**
* JSON representation of the reference path node.
*
* @returns {{ref: symbol, path: Array<string>}}
*/
reference.toJSON = () => ({
[ref]: ref,
path
Expand All @@ -58,17 +97,29 @@ function instrument (path) {
return reference
}

export default function module (schema, parentPath = []) {
/**
* Builds a nested API structure from a schema definition.
*
* Schema supports:
* - string => leaf function node
* - [name, children] => namespace with nested schema
*
* @param {Array<string | [string, Array]>} schema
* @param {Array<string>} [parentPath=[]]
* @returns {Object} Nested instrumented API object
*
* @throws {Error} If schema format is invalid
* @public
*/
export default function References (schema, parentPath = []) {
return schema.reduce((acc, item) => {
if (typeof item === 'string') {
// leaf function
acc[item] = instrument([...parentPath, item])
} else if (Array.isArray(item)) {
const [name, children] = item

if (Array.isArray(children)) {
// recurse: children can be strings or [name, children] arrays
acc[name] = module(children, [...parentPath, name])
acc[name] = References(children, [...parentPath, name])
} else {
throw new Error(`Expected children array for namespace "${name}"`)
}
Expand Down
Loading