Skip to content

Commit

Permalink
Http module
Browse files Browse the repository at this point in the history
For the http module a parser is useful, but it is really difficult to use the same parser
and in @slangroom/core

Thus, for the moment the parsing is done in two different times
1. @slangroom/core extract the clauses using a parser (and build an AST)
2. During the interpretation of AST each clause is analyzed by another parser
   which is specific to each module.
  • Loading branch information
albertolerda committed Sep 27, 2023
1 parent 96375b9 commit 88f984f
Show file tree
Hide file tree
Showing 9 changed files with 1,008 additions and 655 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@
"cjs-fixup": "pnpm -F @slangroom/* exec sh -c \"echo '{\\\"type\\\":\\\"commonjs\\\"}' >build/cjs/package.json\""
},
"devDependencies": {
"@types/body-parser": "^1.19.3",
"@types/express": "^4.17.18",
"@types/node": "^20.3.1",
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@typescript-eslint/parser": "^5.59.11",
"ava": "^5.3.1",
"body-parser": "^1.20.2",
"c8": "^8.0.1",
"esbuild": "^0.18.4",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"express": "^4.18.2",
"prettier": "^2.8.8",
"ts-node": "^10.9.1",
"tslib": "^2.5.3",
Expand Down
5 changes: 3 additions & 2 deletions pkg/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
"name": "@slangroom/core",
"version": "1.0.0",
"dependencies": {
"@slangroom/shared": "workspace:*",
"@slangroom/deps": "workspace:*",
"@slangroom/ignored": "workspace:*"
"@slangroom/shared": "workspace:*",
"@slangroom/ignored": "workspace:*",
"@slangroom/core": "workspace:*"
},
"repository": "https://github.com/dyne/slangroom",
"license": "AGPL-3.0-only",
Expand Down
2 changes: 1 addition & 1 deletion pkg/core/src/slangroom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export const line2AST = (text: string) => {
};
}

type StmtContext = {
export type StmtContext = {
data: JsonableObject,
context: any,
}
Expand Down
1 change: 1 addition & 0 deletions pkg/http/.npmignore
37 changes: 37 additions & 0 deletions pkg/http/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@slangroom/http",
"version": "1.0.0",
"dependencies": {
"@slangroom/core": "workspace:*",
"@slangroom/deps": "workspace:*",
"@slangroom/shared": "workspace:*",
"axios": "^1.5.1"
},
"repository": "https://github.com/dyne/slangroom",
"license": "AGPL-3.0-only",
"type": "module",
"main": "./build/cjs/src/index.js",
"types": "./build/cjs/src/index.d.ts",
"exports": {
".": {
"import": {
"types": "./build/esm/src/index.d.ts",
"default": "./build/esm/src/index.js"
},
"require": {
"types": "./build/cjs/src/index.d.ts",
"default": "./build/cjs/src/index.js"
}
},
"./*": {
"import": {
"types": "./build/esm/src/*.d.ts",
"default": "./build/esm/src/*.js"
},
"require": {
"types": "./build/cjs/src/*.d.ts",
"default": "./build/cjs/src/*.js"
}
}
}
}
296 changes: 296 additions & 0 deletions pkg/http/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
import { createToken, Lexer, CstParser } from "@slangroom/deps/chevrotain";
//import { createSyntaxDiagramsCode } from "chevrotain";
import { JsonableObject, Jsonable, JsonableArray } from "@slangroom/shared";
import { StmtContext } from "@slangroom/core/slangroom";
import axios from "axios";

export enum Method {
GET,
POST
}

export enum DefaultBodyKind {
KEYS,
DATA,
ALL
}
type DefaultBody = {
kind: "Default",
body: DefaultBodyKind,
}

type CustomBody = {
kind: "Custom",
identifier: string
}
type Body = DefaultBody | CustomBody

type SingleReceiver = {
kind: "Endpoint",
name: string
}

type MultiReceiver = {
kind: "Endpoints",
name: string,
differentData: boolean,
}

type Receiver = SingleReceiver | MultiReceiver

type RequestAST = {
method: Method
receiver: Receiver
body?: Body
}


const Get = createToken({
name: "Get",
pattern: /get/i,
});
const Post = createToken({
name: "Post",
pattern: /post/i,
});
const Endpoints = createToken({
name: "Endpoints",
pattern: /endpoints/i,
});
const Endpoint = createToken({
name: "Endpoint",
pattern: /endpoint/i
});
const Passing = createToken({
name: "Passing",
pattern: /passing/i,
});
const Data = createToken({
name: "Data",
pattern: /data/i,
});
const Keys = createToken({
name: "Keys",
pattern: /keys/i,
});
const All = createToken({
name: "All",
pattern: /all/i,
});
const Different = createToken({
name: "Different",
pattern: /different/i,
});

const Identifier = createToken({
name: "Identifier",
pattern: /'[a-z]+'/i,
});


const WhiteSpace = createToken({
name: "WhiteSpace",
pattern: /\s+/,
group: Lexer.SKIPPED,
});

const allTokens = [
WhiteSpace,
Get,
Post,
Endpoints,
Endpoint,
Passing,
Data,
Keys,
All,
Different,
Identifier
];
const StatementLexer = new Lexer(allTokens);
// ----------------- parser -----------------
class StatementParser extends CstParser {
constructor() {
super(allTokens);

this.performSelfAnalysis();
}

// TODO: remove code duplicatio using RULE ARGS and GATEs

public endpoint = this.RULE("endpoint", () => {
this.OR1([
{ ALT: () => this.CONSUME(Get) },
{ ALT: () => this.CONSUME(Post) },
]);
this.CONSUME(Endpoint);
this.CONSUME(Identifier, {LABEL: "name"});
this.OPTION(() => {
this.CONSUME(Passing)
this.OR2([
{ ALT: () => this.CONSUME(Data) },
{ ALT: () => this.CONSUME(Keys) },
{ ALT: () => this.CONSUME(All) },
{
ALT: () => {
this.CONSUME2(Identifier, {LABEL: "dataFrom"})
}
},
])
});
})

public endpoints = this.RULE("endpoints", () => {
this.OR1([
{ ALT: () => this.CONSUME(Get) },
{ ALT: () => this.CONSUME(Post) },
]);
this.CONSUME(Endpoints);
this.CONSUME(Identifier, {LABEL: "name"});
this.OPTION(() => {
this.CONSUME(Passing)
this.OR2([
{ ALT: () => this.CONSUME(Data) },
{ ALT: () => this.CONSUME(Keys) },
{ ALT: () => this.CONSUME(All) },
{
ALT: () => {
this.OPTION2(() => this.CONSUME(Different))
this.CONSUME2(Identifier, {LABEL: "dataFrom"})
}
},
])
});
})

public statement = this.RULE("statement", () => {
this.OR([
{ ALT: () => this.SUBRULE(this.endpoint) },
{ ALT: () => this.SUBRULE(this.endpoints) },
])

});
}

const parser = new StatementParser();
// ----------------- Interpreter -----------------
const BaseCstVisitor = parser.getBaseCstVisitorConstructor();

class StatementInterpreter extends BaseCstVisitor {
constructor() {
super();
this.validateVisitor();
}
statement(ctx: any) {
return this.visit(ctx.endpoint || ctx.endpoints)
}

endpoint(ctx: any): RequestAST {
const method = ctx.Get ? Method.GET : Method.POST
const receiver: SingleReceiver = {kind: "Endpoint", name: ctx.name[0].image.slice(1,-1)}
const body: Body | undefined =
ctx.Data ? {kind: "Default", body: DefaultBodyKind.DATA}
: ctx.Keys ? {kind: "Default", body: DefaultBodyKind.KEYS}
: ctx.All ? {kind: "Default", body: DefaultBodyKind.ALL}
: ctx.dataFrom ? {kind: "Custom", identifier: ctx.dataFrom[0].image.slice(1,-1)}
: undefined
const endpoint: RequestAST = {
method,
receiver
}
if(body) endpoint.body = body
return endpoint
}
endpoints(ctx: any) {
const method = ctx.Get ? Method.GET : Method.POST
const receiver: MultiReceiver = {kind: "Endpoints", name: ctx.name[0].image.slice(1,-1), differentData: !!ctx.Different}
const body: Body | undefined =
ctx.Data ? {kind: "Default", body: DefaultBodyKind.DATA}
: ctx.Keys ? {kind: "Default", body: DefaultBodyKind.KEYS}
: ctx.All ? {kind: "Default", body: DefaultBodyKind.ALL}
: ctx.dataFrom ? {kind: "Custom", identifier: ctx.dataFrom[0].image.slice(1,-1)}
: undefined
const endpoint: RequestAST = {
method,
receiver
}
if(body) endpoint.body = body
return endpoint
}
}

// We only need a single interpreter instance because our interpreter has no state.
const interpreter = new StatementInterpreter();

export const line2Ast = (text: string) => {
const lexResult = StatementLexer.tokenize(text);
parser.input = lexResult.tokens;
const cst = parser.statement();
const value = interpreter.visit(cst);
return {
value: value,
lexResult: lexResult,
parseErrors: parser.errors,
};
}

export const evaluate = async (ast: RequestAST,
keys: JsonableObject, stmtCtx: StmtContext): Promise<JsonableObject | JsonableArray> => {
let body: Jsonable | undefined
if(ast.body) {
if(ast.body.kind == "Default") {
switch(ast.body.body) {
case DefaultBodyKind.KEYS: body = keys; break;
case DefaultBodyKind.DATA: body = stmtCtx.data; break;
case DefaultBodyKind.ALL: body = {...stmtCtx.data, ...keys}; break;
}
} else {
body = stmtCtx.data[ast.body.identifier] || keys[ast.body.identifier]
}
}
if(ast.receiver.kind == "Endpoint") {
let error: any = null;
const r = await axios.request({
method: ast.method == Method.GET ? "get" : "post",
url: (stmtCtx.data[ast.receiver.name] || keys[ast.receiver.name] || ast.receiver.name).toString(),
data: body,
validateStatus: () => true
}).catch((e) => error = e);
const zenResult = error
? { status: error.code, error: "" }
: { status: r.status, result: r.data || "" }
return zenResult;
} else {
// TODO: check type of urls, body, ... all data from zenroom
let dataFz = (_i: number) => body;
if(ast.receiver.differentData) {
dataFz = (i: number) => (body as JsonableArray)[i]
}
const urls = (stmtCtx.data[ast.receiver.name] || keys[ast.receiver.name]) as string[]
let reqs_promises = []


for(let i = 0; i < urls.length; i++) {
reqs_promises.push(axios.request({
method: ast.method == Method.GET ? "get" : "post",
url: urls[i] || "",
data: dataFz(i),
validateStatus: () => true
}))
}
let results: JsonableArray = new Array(reqs_promises.length)
let errors: { [key: number]: any} = {};
const parallel_with_catch = reqs_promises.map((v, i) => v.catch(
(e) => errors[i] = e
))
const parallel_results = await axios.all(parallel_with_catch)
parallel_results.map((r, i) => {

const zenResult = errors[i]
? { "status": errors[i].code, "result": "" }
: { "status": r.status, "result": r.data || ""}
results[i] = zenResult;
});
return results;
}
}
Loading

0 comments on commit 88f984f

Please sign in to comment.