mirror of
https://github.com/gorules/zen.git
synced 2026-10-04 08:02:28 +00:00
* chore: add benchmarks for tier 1 languages * improve fixtures * fix cargo fmt
329 lines
13 KiB
JavaScript
329 lines
13 KiB
JavaScript
import { writeFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
const fixtures = path.join(dir, 'fixtures');
|
|
|
|
const ITEMS = 250;
|
|
const DAYS = 200;
|
|
const SHIPMENTS = 150;
|
|
const AMOUNTS = 600;
|
|
const SCORES = 300;
|
|
const ASSERTIONS = 30;
|
|
|
|
const range = (n) => Array.from({ length: n }, (_, i) => i);
|
|
|
|
let x = 100;
|
|
const pos = () => ({ x: (x += 160), y: 120 });
|
|
|
|
const inputNode = (id) => ({ type: 'inputNode', id, name: 'request', content: { schema: '' }, position: pos() });
|
|
|
|
const exprNode = (id, name, exprs, transform = {}) => ({
|
|
type: 'expressionNode',
|
|
id,
|
|
name,
|
|
content: {
|
|
expressions: exprs.map(([key, value], i) => ({ id: `${id}_e${i}`, key, value })),
|
|
passThrough: true,
|
|
inputField: null,
|
|
outputPath: null,
|
|
executionMode: 'single',
|
|
...transform,
|
|
},
|
|
position: pos(),
|
|
});
|
|
|
|
const tableNode = (id, name, inputs, outputs, rules, transform = {}) => ({
|
|
type: 'decisionTableNode',
|
|
id,
|
|
name,
|
|
content: {
|
|
hitPolicy: 'first',
|
|
inputs,
|
|
outputs,
|
|
rules,
|
|
passThrough: true,
|
|
inputField: null,
|
|
outputPath: null,
|
|
executionMode: 'single',
|
|
...transform,
|
|
},
|
|
position: pos(),
|
|
});
|
|
|
|
const graph = (nodes) => ({
|
|
contentType: 'application/vnd.gorules.decision',
|
|
nodes,
|
|
edges: nodes.slice(1).map((n, i) => ({ id: `edge${i}`, type: 'edge', sourceId: nodes[i].id, targetId: n.id })),
|
|
});
|
|
|
|
const dataModel = (name, props) => ({
|
|
id: 'dm',
|
|
type: 'dataModel',
|
|
props: { data: { name, properties: props.map((p, i) => ({ id: `p${i}`, name: p.name, type: p.type, array: !!p.array, optional: !!p.optional })) } },
|
|
children: [],
|
|
});
|
|
|
|
const expr = (id, key, value) => ({ id, type: 'expression', props: { data: { key, value } } });
|
|
|
|
const assertion = (id, output, expression) => ({
|
|
id,
|
|
type: 'assertion',
|
|
props: { data: { output, conditions: [{ id: `${id}c`, expression, operator: 'and', depth: 0 }] } },
|
|
children: [],
|
|
});
|
|
|
|
const expressionTableMap = graph([
|
|
inputNode('in'),
|
|
exprNode('enrich', 'enrich', [
|
|
['subtotal', 'amount * qty'],
|
|
['premium', "amount > 40 and group == 'accessories'"],
|
|
], { inputField: 'items', outputPath: 'enriched', executionMode: 'loop' }),
|
|
tableNode('classify', 'classify',
|
|
[{ id: 'ci', name: 'Subtotal', field: 'subtotal' }],
|
|
[{ id: 'co', name: 'Discount', field: 'discount' }],
|
|
[
|
|
{ _id: 'r1', ci: '> 1000', co: '15' },
|
|
{ _id: 'r2', ci: '> 500', co: '10' },
|
|
{ _id: 'r3', ci: '> 100', co: '5' },
|
|
{ _id: 'r4', ci: '', co: '0' },
|
|
],
|
|
{ inputField: 'enriched', outputPath: 'classified', executionMode: 'loop' }),
|
|
exprNode('replicate', 'replicate', [
|
|
['allItems', 'items'],
|
|
['enrichedCopy', 'enriched'],
|
|
['classifiedCopy', 'classified'],
|
|
]),
|
|
exprNode('totals', 'totals', [
|
|
['grandTotal', 'sum(map(enriched, #.subtotal))'],
|
|
['itemCount', 'len(items)'],
|
|
['premiumCount', 'len(filter(enriched, #.premium))'],
|
|
]),
|
|
]);
|
|
|
|
const dynamicShipping = graph([
|
|
inputNode('in'),
|
|
exprNode('dims', 'dims', [
|
|
['dimWeight', '(length * width * height) / 5000'],
|
|
['effWeight', 'max([weight, (length * width * height) / 5000])'],
|
|
['distFactor', 'distance / 100'],
|
|
], { inputField: 'shipments', outputPath: 'computed', executionMode: 'loop' }),
|
|
tableNode('baseRate', 'baseRate',
|
|
[
|
|
{ id: 'i1', name: 'Type', field: 'shippingType' },
|
|
{ id: 'i2', name: 'Weight', field: 'effWeight' },
|
|
],
|
|
[{ id: 'o1', name: 'Base', field: 'baseRate' }],
|
|
[
|
|
{ _id: 'r1', i1: "'standard'", i2: '< 20', o1: '10' },
|
|
{ _id: 'r2', i1: "'standard'", i2: '', o1: '20' },
|
|
{ _id: 'r3', i1: "'express'", i2: '< 20', o1: '20' },
|
|
{ _id: 'r4', i1: "'express'", i2: '', o1: '35' },
|
|
{ _id: 'r5', i1: "'priority'", i2: '', o1: '50' },
|
|
{ _id: 'r6', i1: '', i2: '', o1: '15' },
|
|
],
|
|
{ inputField: 'computed', outputPath: 'rated', executionMode: 'loop' }),
|
|
tableNode('finalPrice', 'finalPrice',
|
|
[{ id: 'i1', name: 'Tier', field: 'customerTier' }],
|
|
[{ id: 'o1', name: 'Final', field: 'finalPrice' }],
|
|
[
|
|
{ _id: 'r1', i1: "'gold'", o1: 'baseRate * (effWeight / weight) * (1 + distFactor * 0.1) * 0.9' },
|
|
{ _id: 'r2', i1: "'silver'", o1: 'baseRate * (effWeight / weight) * (1 + distFactor * 0.1) * 0.95' },
|
|
{ _id: 'r3', i1: '', o1: 'baseRate * (effWeight / weight) * (1 + distFactor * 0.1)' },
|
|
],
|
|
{ inputField: 'rated', outputPath: 'priced', executionMode: 'loop' }),
|
|
exprNode('replicate', 'replicate', [
|
|
['shipmentsCopy', 'shipments'],
|
|
['computedCopy', 'computed'],
|
|
['pricedCopy', 'priced'],
|
|
]),
|
|
exprNode('totals', 'totals', [
|
|
['totalPrice', 'sum(map(priced, #.finalPrice))'],
|
|
['count', 'len(shipments)'],
|
|
]),
|
|
]);
|
|
|
|
const customerEligibility = graph([
|
|
inputNode('in'),
|
|
exprNode('dailyStats', 'dailyStats', [
|
|
['cost', 'dataGB * 0.5 + minutes * 0.01 + texts * 0.001'],
|
|
['heavy', 'dataGB > 2'],
|
|
], { inputField: 'usageHistory', outputPath: 'daily', executionMode: 'loop' }),
|
|
tableNode('dailyTier', 'dailyTier',
|
|
[{ id: 'i', name: 'Cost', field: 'cost' }],
|
|
[{ id: 'o', name: 'Tier', field: 'usageTier' }],
|
|
[
|
|
{ _id: 'r1', i: '> 5', o: "'high'" },
|
|
{ _id: 'r2', i: '> 2', o: "'medium'" },
|
|
{ _id: 'r3', i: '', o: "'low'" },
|
|
],
|
|
{ inputField: 'daily', outputPath: 'dailyTiered', executionMode: 'loop' }),
|
|
exprNode('aggregate', 'aggregate', [
|
|
['totalCost', 'sum(map(daily, #.cost))'],
|
|
['heavyDays', 'len(filter(daily, #.heavy))'],
|
|
['avgData', 'sum(map(usageHistory, #.dataGB)) / len(usageHistory)'],
|
|
]),
|
|
tableNode('accountStatus', 'accountStatus',
|
|
[
|
|
{ id: 'i1', name: 'Age', field: 'accountInfo.accountAge' },
|
|
{ id: 'i2', name: 'Payment', field: 'accountInfo.paymentHistory' },
|
|
],
|
|
[
|
|
{ id: 'o1', name: 'Status', field: 'eligibility.accountStatus' },
|
|
{ id: 'o2', name: 'Good', field: 'eligibility.goodStanding' },
|
|
],
|
|
[
|
|
{ _id: 'r1', i1: '>= 24', i2: "'excellent'", o1: "'excellent'", o2: 'true' },
|
|
{ _id: 'r2', i1: '>= 12', i2: "'excellent', 'good'", o1: "'good'", o2: 'true' },
|
|
{ _id: 'r3', i1: '>= 6', i2: '', o1: "'fair'", o2: 'true' },
|
|
{ _id: 'r4', i1: '', i2: '', o1: "'review'", o2: 'false' },
|
|
]),
|
|
tableNode('loyaltyTier', 'loyaltyTier',
|
|
[
|
|
{ id: 'i1', name: 'Age', field: 'accountInfo.accountAge' },
|
|
{ id: 'i2', name: 'Spend', field: 'accountInfo.monthlySpend' },
|
|
],
|
|
[
|
|
{ id: 'o1', name: 'Tier', field: 'eligibility.loyaltyTier' },
|
|
{ id: 'o2', name: 'Discount', field: 'eligibility.loyaltyDiscountPercent' },
|
|
],
|
|
[
|
|
{ _id: 'r1', i1: '>= 36', i2: '>= 120', o1: "'platinum'", o2: '20' },
|
|
{ _id: 'r2', i1: '>= 24', i2: '>= 80', o1: "'gold'", o2: '15' },
|
|
{ _id: 'r3', i1: '>= 12', i2: '>= 50', o1: "'silver'", o2: '10' },
|
|
{ _id: 'r4', i1: '', i2: '', o1: "'standard'", o2: '0' },
|
|
]),
|
|
exprNode('specialOffers', 'specialOffers', [
|
|
['eligibility.specialOffers', "{ 'dataBooster': usageStats.dataUtilizationPercent > 80, 'familyPlan': len(customerProfile.devices) >= 3, 'loyaltyReward': eligibility.loyaltyTier in ['platinum', 'gold', 'silver'] }"],
|
|
['eligibility.offerCount', 'len(filter(values($.eligibility.specialOffers), # == true))'],
|
|
]),
|
|
exprNode('replicate', 'replicate', [
|
|
['historyCopy', 'usageHistory'],
|
|
['dailyCopy', 'daily'],
|
|
['tieredCopy', 'dailyTiered'],
|
|
]),
|
|
]);
|
|
|
|
const expressionChain = {
|
|
blocks: [
|
|
dataModel('customer', [
|
|
{ name: 'amounts', type: 'number', array: true },
|
|
{ name: 'label', type: 'string' },
|
|
]),
|
|
expr('e1', 'customer.positive', 'filter(customer.amounts, # > 0)'),
|
|
expr('e2', 'customer.negatives', 'filter(customer.amounts, # < 0)'),
|
|
expr('e3', 'customer.doubled', 'map(customer.amounts, # * 2)'),
|
|
expr('e4', 'customer.squared', 'map(customer.amounts, # * #)'),
|
|
expr('e5', 'customer.total', 'sum(customer.positive)'),
|
|
expr('e6', 'customer.negTotal', 'sum(customer.negatives)'),
|
|
expr('e7', 'customer.net', 'customer.total + customer.negTotal'),
|
|
expr('e8', 'customer.posCount', 'len(customer.positive)'),
|
|
expr('e9', 'customer.avg', 'customer.net / len(customer.amounts)'),
|
|
expr('e10', 'customer.maxVal', 'max(customer.amounts)'),
|
|
expr('e11', 'customer.minVal', 'min(customer.amounts)'),
|
|
assertion('a1', 'customer.bigSpender', 'customer.total > 1000'),
|
|
assertion('a2', 'customer.balanced', 'customer.net > 0'),
|
|
assertion('a3', 'customer.volatile', 'customer.maxVal - customer.minVal > 50'),
|
|
],
|
|
};
|
|
|
|
const multiAssertion = {
|
|
blocks: [
|
|
dataModel('customer', [
|
|
{ name: 'age', type: 'number' },
|
|
{ name: 'income', type: 'number' },
|
|
{ name: 'employmentYears', type: 'number' },
|
|
{ name: 'scores', type: 'number', array: true },
|
|
]),
|
|
expr('s1', 'customer.scoreSum', 'sum(customer.scores)'),
|
|
expr('s2', 'customer.scoreAvg', 'customer.scoreSum / len(customer.scores)'),
|
|
expr('s3', 'customer.highScores', 'filter(customer.scores, # > 50)'),
|
|
expr('s4', 'customer.scaled', 'map(customer.scores, # * 1.5)'),
|
|
...range(ASSERTIONS).map((i) => assertion(`a${i}`, `customer.flag${i}`, `customer.scoreAvg >= ${i}`)),
|
|
],
|
|
};
|
|
|
|
const manifest = [
|
|
{
|
|
name: 'decision-table-discounts',
|
|
kind: 'graph',
|
|
file: 'graphs/decision-table-discounts.json',
|
|
input: { code: 'WELCOME_2023', order: { createdAt: '2023-01-15' }, customer: { joinDate: '2023-02-01', pastPurchases: [] } },
|
|
},
|
|
{
|
|
name: 'expression-table-map',
|
|
kind: 'graph',
|
|
file: 'graphs/expression-table-map.json',
|
|
input: {
|
|
customer: { name: 'John Doe', country: 'US' },
|
|
items: range(ITEMS).map((i) => ({ name: `item${i}`, amount: 10 + (i % 90), group: i % 3 === 0 ? 'accessories' : 'electronics', qty: 1 + (i % 5) })),
|
|
},
|
|
},
|
|
{
|
|
name: 'credit-limit-adjustment',
|
|
kind: 'graph',
|
|
file: 'graphs/credit-limit-adjustment.json',
|
|
input: {
|
|
account: {
|
|
customer_id: 'CUST10025',
|
|
current_credit_limit: 5000,
|
|
payment_history: { on_time_payment_percentage: 98, late_payments_last_12_months: 0, total_payments: 24 },
|
|
utilization: { current_percentage: 25, average_last_6_months: 40, highest_balance: 2800 },
|
|
financial_behavior: { credit_inquiries_last_12_months: 0, years_with_institution: 4, income_verified: true, has_other_products: true },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'dynamic-shipping-cost-calculator',
|
|
kind: 'graph',
|
|
file: 'graphs/dynamic-shipping-cost-calculator.json',
|
|
input: {
|
|
shipments: range(SHIPMENTS).map((i) => ({
|
|
weight: 5 + (i % 40),
|
|
length: 20 + (i % 30),
|
|
width: 15 + (i % 20),
|
|
height: 10 + (i % 15),
|
|
distance: 50 + (i % 500),
|
|
shippingType: ['standard', 'express', 'priority'][i % 3],
|
|
customerTier: ['gold', 'silver', 'standard'][i % 3],
|
|
})),
|
|
},
|
|
},
|
|
{
|
|
name: 'customer-eligibility-engine',
|
|
kind: 'graph',
|
|
file: 'graphs/customer-eligibility-engine.json',
|
|
input: {
|
|
accountInfo: { accountAge: 28, paymentHistory: 'excellent', currentPlan: 'Family Premium 50GB', monthlySpend: 95, contractStatus: 'pending_renewal' },
|
|
usageStats: { dataUsageGB: 42.5, dataUtilizationPercent: 85, callMinutes: 350, textMessages: 1250 },
|
|
customerProfile: { segment: 'consumer', location: 'Seattle, WA', devices: ['iPhone 14', 'iPad Air', 'Apple Watch'], previousComplaints: 0 },
|
|
usageHistory: range(DAYS).map((d) => ({ day: d, dataGB: 1 + (d % 5), minutes: 100 + (d % 200), texts: 10 + (d % 50) })),
|
|
},
|
|
},
|
|
{ name: 'simple-assertion', kind: 'policy', file: 'policies/simple-assertion.json', input: { customer: { age: 30 } } },
|
|
{ name: 'expression', kind: 'policy', file: 'policies/expression.json', input: { customer: { age: 35, income: 60000 } } },
|
|
{
|
|
name: 'multi-assertion',
|
|
kind: 'policy',
|
|
file: 'policies/multi-assertion.json',
|
|
input: { customer: { age: 40, income: 60000, employmentYears: 5, scores: range(SCORES).map((i) => (i * 13) % 100) } },
|
|
},
|
|
{
|
|
name: 'expression-chain',
|
|
kind: 'policy',
|
|
file: 'policies/expression-chain.json',
|
|
input: { customer: { amounts: range(AMOUNTS).map((i) => ((i * 7) % 41) - 20), label: 'batch' } },
|
|
},
|
|
{ name: 'match', kind: 'policy', file: 'policies/match.json', input: { customer: { score: 85 } } },
|
|
];
|
|
|
|
writeFileSync(path.join(fixtures, 'graphs', 'expression-table-map.json'), JSON.stringify(expressionTableMap));
|
|
writeFileSync(path.join(fixtures, 'graphs', 'dynamic-shipping-cost-calculator.json'), JSON.stringify(dynamicShipping));
|
|
writeFileSync(path.join(fixtures, 'graphs', 'customer-eligibility-engine.json'), JSON.stringify(customerEligibility));
|
|
writeFileSync(path.join(fixtures, 'policies', 'expression-chain.json'), JSON.stringify(expressionChain));
|
|
writeFileSync(path.join(fixtures, 'policies', 'multi-assertion.json'), JSON.stringify(multiAssertion));
|
|
writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
|
|
console.log('generated 5 heavy fixtures + manifest.json');
|