From 534ecc333f6c79c26c3a3a1282662e6a44668fa7 Mon Sep 17 00:00:00 2001 From: niquola Date: Fri, 12 Jan 2024 15:29:25 +0000 Subject: [PATCH] clean implementation --- sof-js/src/index.js | 203 ++++++++++++++++++++++++----------- sof-js/src/index.old.js | 113 +++++++++++++++++++ sof-js/src/newsyntax.md | 44 ++++++++ sof-js/tests/1_basic.test.js | 3 +- sof-js/tests/test_helpers.js | 2 +- 5 files changed, 297 insertions(+), 68 deletions(-) create mode 100644 sof-js/src/index.old.js create mode 100644 sof-js/src/newsyntax.md diff --git a/sof-js/src/index.js b/sof-js/src/index.js index 5397aa5..5e5330e 100644 --- a/sof-js/src/index.js +++ b/sof-js/src/index.js @@ -1,10 +1,17 @@ import { fhirpath_evaluate } from './path.js' +function assert(condition, message) { + if (!condition) { + throw message || "Assertion failed"; + } +} + export function merge(a,b) { return Object.assign({}, a, b); } export function row_product(parts) { + if(parts.length == 1) {return parts[0]} let rows = [{}]; let new_rows = null; parts.forEach((partial_rows) => { @@ -19,9 +26,29 @@ export function row_product(parts) { return rows; } -function get_columns(column, node) { +function forEach(def, node) { + assert(def.forEach, 'forEach required') + let nodes = fhirpath_evaluate(node, def.forEach) + return nodes.flatMap((node)=>{ + return select({select: def.select}, node) + }) +} + +function forEachOrNull(def, node) { + assert(def.forEachOrNull, 'forEachOrNull required') + let nodes = fhirpath_evaluate(node, def.forEach) + if(nodes.length == 0) { + nodes = [{}]; + } + return nodes.flatMap((node)=>{ + return select({select: def.select}, node) + }) +} + +function column(def, node) { + assert(def.column, 'column required') let record = {}; - column.forEach((c) => { + def.column.forEach((c) => { let vs = fhirpath_evaluate( node, c.path); if(c.collection) { record[c.name] = vs; @@ -32,83 +59,129 @@ function get_columns(column, node) { throw new Error('Collection value for ' + c.path + ' => ' + JSON.stringify(vs)) } }); - return record; - + return [record]; } -function process_rows(column, nodes) { - return nodes.map((node)=> { - return get_columns(column, node); +function unionAll(def, node) { + assert(def.unionAll, 'unionAll') + return def.unionAll.flatMap((d)=>{ + return do_eval(d, node) }) } -function get_nodes(select, node) { - let nodes = null; - if(select.forEach) { - nodes = fhirpath_evaluate(node, select.forEach) - } else if (select.forEachOrNull) { - nodes = fhirpath_evaluate(node, select.forEach) - if(nodes.length == 0) { nodes = [{}] } - } else { - nodes = [node] +function select(def, node) { + assert(def.select, 'select') + if(def.where) { + let res = fhirpath_evaluate(node, def.where) + if(!res[0]) { return []} } - return nodes; -} - - -function process_union(select, nodes) { - return nodes.flatMap((node)=> { - let union_rows = select.unionAll.flatMap((s)=> { - let res = process_select_clause(s, node); - return res; + return row_product( + def.select.map((s)=> { + return do_eval(s, node); }) - if(select.column){ - union_rows = row_product([[get_columns(select.column, node)], union_rows]); - } - return union_rows; - }) + ) } -function process_select_clause(select, node) { - //there are two options - unroll collections with forEach[OrNull] or just process the node - let nodes = get_nodes(select, node); - - if( select.select ) { - return process_select(select, nodes); - } else if( select.unionAll ) { - return process_union(select, nodes); - } else if(select.column) { - return process_rows(select.column, nodes); - } - throw new Error('unexpected'); +function compile(def) { + throw new Error('not impl'); } -function filter_where(nodes, where) { - if(!where) { return nodes }; - return nodes.filter((node)=>{ - let res = fhirpath_evaluate(node, where); - return res[0]; - }) -} - -function process_select(definition, nodes) { - if(definition.column) { definition.select.unshift({column: definition.column})} - return filter_where(nodes, definition.where) - .flatMap((node)=>{ - let partial_rows = definition.select.map((s)=> {return process_select_clause(s, node)}); - return row_product(partial_rows) - }) +// * foreach / column / [select(..)] -> foreach select[column, ..] +// * foreach / union / [select(..)] -> foreach select[union, ..] +// * foreach / select(..) -> foreach select[..] +// * select[..] / union -> select [union, ..] +// * select[..] / column -> select [column, ..] +// * union / column -> select [column, union] +function normalize(def) { + if(def.forEach) { + def.select ||= [] + def.type = 'forEach' + if(def.unionAll) { + def.select.unshift({unionAll: def.union}) + delete def.unionAll + } + if(def.column) { + def.select.unshift({column: def.column}) + delete def.column + } + def.select = def.select.map((s)=> { return normalize(s)}) + return def; + } else if(def.forEachOrNull) { + def.select ||= [] + def.type = 'forEachOrNull' + if(def.unionAll) { + def.select.unshift({unionAll: def.union}) + delete def.unionAll + } + if(def.column) { + def.select.unshift({column: def.column}) + delete def.column + } + def.select = def.select.map((s)=> { return normalize(s)}) + return def; + } else if(def.unionAll && def.select) { + def.type = 'select' + def.select.unshift({unionAll: def.unionAll}) + delete def.unionAll + def.select = def.select.map((s)=> { return normalize(s)}) + return def; + } else if (def.select && def.column) { + def.select.unshift({column: def.column}) + delete def.column + def.type = 'select' + def.select = def.select.map((s)=> { return normalize(s)}) + return def; + } else if (def.unionAll && def.column) { + def.select ||= [] + def.select.unshift({unionAll: def.unionAll}) + def.select.unshift({column: def.column}) + delete def.unionAll + delete def.column + def.type = 'select' + def.select = def.select.map((s)=> { return normalize(s)}) + return def; + } else if (def.select){ + def.type = 'select' + def.select = def.select.map((s)=> { return normalize(s)}) + return def + } else { + if(def.unionAll) { + def.type = 'unionAll' + def.unionAll = def.unionAll.map((s)=> { return normalize(s)}) + } else if (def.column) { + def.type = 'column' + } else if (def.forEach) { + def.type = 'forEach' + } else if (def.forEachOrNull) { + def.type = 'forEachOrNull' + } else if (def.select) { + def.type = 'select' + } + return def + } } - - -export function validate(viewdef, opts) { - //TBD +let fns = { + 'forEach': forEach, + 'forEachOrNull': forEachOrNull, + 'unionAll': unionAll, + 'select': select, + 'column': column } -export function compile(viewdef) { - //TBD +function do_eval(def, node) { + let f = fns[def.type]; + if(!f){ throw Error('Not impl ' + def.type)} + return f(def, node); } -export function evaluate(viewdef, nodes) { - return process_select(viewdef, nodes); +export function evaluate(def, node) { + let normal_def = normalize(def); + // console.log(JSON.stringify(normal_def, null, " ")) + if(Array.isArray(node)) { + return node.flatMap((n)=>{ + return do_eval(normal_def, n); + }) + } else { + return do_eval(normal_def, node); + } } diff --git a/sof-js/src/index.old.js b/sof-js/src/index.old.js new file mode 100644 index 0000000..016e017 --- /dev/null +++ b/sof-js/src/index.old.js @@ -0,0 +1,113 @@ +import { fhirpath_evaluate } from './path.js' + +export function merge(a,b) { + return Object.assign({}, a, b); +} + +export function row_product(parts) { + let rows = [{}]; + let new_rows = null; + parts.forEach((partial_rows) => { + new_rows = []; + partial_rows.forEach((partial_row)=> { + rows.forEach((row)=> { + new_rows.push(merge(partial_row,row)) + }) + }) + rows = new_rows; + }); + return rows; +} + +function get_columns(column, node) { + let record = {}; + column.forEach((c) => { + let vs = fhirpath_evaluate( node, c.path); + if(c.collection) { + record[c.name] = vs; + } else if (vs.length <= 1) { + let v = vs[0]; + record[c.name] = (v === undefined) ? null : v; + } else { + throw new Error('Collection value for ' + c.path + ' => ' + JSON.stringify(vs)) + } + }); + return record; +} + +function process_rows(column, nodes) { + return nodes.map((node)=> { + return get_columns(column, node); + }) +} + +function get_nodes(select, node) { + let nodes = null; + if(select.forEach) { + nodes = fhirpath_evaluate(node, select.forEach) + } else if (select.forEachOrNull) { + nodes = fhirpath_evaluate(node, select.forEach) + if(nodes.length == 0) { nodes = [{}] } + } else { + nodes = [node] + } + return nodes; +} + + +function process_union(select, nodes) { + return nodes.flatMap((node)=> { + let union_rows = select.unionAll.flatMap((s)=> { + let res = process_select_clause(s, node); + return res; + }) + if(select.column){ + union_rows = row_product([[get_columns(select.column, node)], union_rows]); + } + return union_rows; + }) +} + +function process_select_clause(select, node) { + //there are two options - unroll collections with forEach[OrNull] or just process the node + let nodes = get_nodes(select, node); + + if( select.select ) { + return process_select(select, nodes); + } else if( select.unionAll ) { + return process_union(select, nodes); + } else if(select.column) { + return process_rows(select.column, nodes); + } + throw new Error('unexpected'); +} + +function filter_where(nodes, where) { + if(!where) { return nodes }; + return nodes.filter((node)=>{ + let res = fhirpath_evaluate(node, where); + return res[0]; + }) +} + +function process_select(definition, nodes) { + if(definition.column) { definition.select.unshift({column: definition.column})} + return filter_where(nodes, definition.where) + .flatMap((node)=>{ + let partial_rows = definition.select.map((s)=> {return process_select_clause(s, node)}); + return row_product(partial_rows) + }) +} + + +export function validate(viewdef, opts) { + //TBD +} + +export function compile(viewdef) { + //TBD +} + +export function evaluate(viewdef, nodes) { + return process_select(viewdef, nodes); +} diff --git a/sof-js/src/newsyntax.md b/sof-js/src/newsyntax.md new file mode 100644 index 0000000..8b07dcb --- /dev/null +++ b/sof-js/src/newsyntax.md @@ -0,0 +1,44 @@ +DSL consists of set of "functions" + +* select - Cartesian product of partial row sets +* column - columns extraction +* forEach - unroll nested expressions +* forEachOrNull - unroll nested expressions +* unionAll - concatenate row sets + + +```js +select( + column([{name: 'id', path: 'id'}, ]), + forEach('address', columns(...)), + unionAll( + forEach(...), + forEach(...)) + {where: '...'}); +``` + + +Here is JSON syntax for it. + +```js +{select: [ + {column: []}, + {forEach: 'address', + select: [{columns: []}]} + {unionAll: [ + {select: []}, + {select: []}]}] + where: '...'} +``` + +### Keywords combinations transform + +In case of keyword combinations here are rewrite rules to choose the main function and make params + +* foreach / columns / [select(..)] -> foreach select[columns, ..] +* foreach / union / [select(..)] -> foreach select[union, ..] +* foreach / select(..) -> foreach select[..] +* select[..] / union -> select [union, ..] +* select[..] / columns -> select [columns, ..] +* union / columns -> select [columns, union] + diff --git a/sof-js/tests/1_basic.test.js b/sof-js/tests/1_basic.test.js index 439259c..5dbdf1a 100644 --- a/sof-js/tests/1_basic.test.js +++ b/sof-js/tests/1_basic.test.js @@ -1,6 +1,6 @@ import { expect, test , describe} from "bun:test"; import { evaluate, row_product } from '../src/index.js' -import { start_case, end_case, add_test, run_test, should_fail } from './test_helpers.js' +import { start_case, end_case, add_test, debug, run_test, should_fail } from './test_helpers.js' let l = console.log @@ -87,7 +87,6 @@ describe("basics", () => { {column: [{name: 'last_name', path: 'name.family.first()'}]}]}, expected: expected}) - add_test({ title: 'where', view: diff --git a/sof-js/tests/test_helpers.js b/sof-js/tests/test_helpers.js index 5701f40..8912323 100644 --- a/sof-js/tests/test_helpers.js +++ b/sof-js/tests/test_helpers.js @@ -37,7 +37,7 @@ export function end_case(name, desc, resources) { export function debug(viewdef) { let res = evaluate( viewdef, test_case.resources) - console.log(res); + console.log('result:', res); return res; }