@alt-javascript/camel-lite-core 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -0
- package/package.json +47 -0
- package/src/AggregationStrategies.js +40 -0
- package/src/CamelContext.js +131 -0
- package/src/ConsumerTemplate.js +77 -0
- package/src/Exchange.js +70 -0
- package/src/ExpressionBuilder.js +122 -0
- package/src/Message.js +38 -0
- package/src/Pipeline.js +96 -0
- package/src/ProcessorNormalizer.js +14 -0
- package/src/ProducerTemplate.js +98 -0
- package/src/RouteBuilder.js +21 -0
- package/src/RouteDefinition.js +526 -0
- package/src/RouteLoader.js +390 -0
- package/src/component.js +33 -0
- package/src/errors/CamelError.js +9 -0
- package/src/errors/CamelFilterStopException.js +15 -0
- package/src/errors/CycleDetectedError.js +12 -0
- package/src/errors/SedaQueueFullError.js +12 -0
- package/src/index.js +17 -0
- package/test/ConsumerTemplate.test.js +146 -0
- package/test/ProducerTemplate.test.js +132 -0
- package/test/context.test.js +97 -0
- package/test/dsl.test.js +375 -0
- package/test/eip.test.js +497 -0
- package/test/exchange.test.js +42 -0
- package/test/fixtures/routes.yaml +58 -0
- package/test/message.test.js +36 -0
- package/test/pipeline.test.js +308 -0
- package/test/routeBuilder.test.js +208 -0
- package/test/routeLoader.test.js +557 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { CamelContext, ProducerTemplate, Exchange } from '../src/index.js';
|
|
4
|
+
import { DirectComponent } from '@alt-javascript/camel-lite-component-direct';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
function makeContext() {
|
|
11
|
+
const ctx = new CamelContext();
|
|
12
|
+
ctx.addComponent('direct', new DirectComponent());
|
|
13
|
+
return ctx;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Unit tests (no running context)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
describe('ProducerTemplate: constructor', () => {
|
|
21
|
+
it('throws when no context provided', () => {
|
|
22
|
+
assert.throws(() => new ProducerTemplate(null), /requires a CamelContext/);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('ProducerTemplate: sendBody (unit)', () => {
|
|
27
|
+
it('throws for unknown scheme', async () => {
|
|
28
|
+
const ctx = makeContext();
|
|
29
|
+
const pt = new ProducerTemplate(ctx);
|
|
30
|
+
await assert.rejects(
|
|
31
|
+
() => pt.sendBody('unknown:foo', 'body'),
|
|
32
|
+
/no component registered for scheme 'unknown'/
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('throws for URI with no scheme', async () => {
|
|
37
|
+
const ctx = makeContext();
|
|
38
|
+
const pt = new ProducerTemplate(ctx);
|
|
39
|
+
await assert.rejects(
|
|
40
|
+
() => pt.sendBody('nodots', 'body'),
|
|
41
|
+
/invalid URI/
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Integration tests (live context with direct: component)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
describe('ProducerTemplate: sendBody integration', () => {
|
|
51
|
+
let ctx;
|
|
52
|
+
|
|
53
|
+
before(async () => {
|
|
54
|
+
ctx = makeContext();
|
|
55
|
+
const { RouteBuilder } = await import('../src/RouteBuilder.js');
|
|
56
|
+
const builder = new RouteBuilder();
|
|
57
|
+
builder.from('direct:upper').process(ex => {
|
|
58
|
+
ex.in.body = ex.in.body.toUpperCase();
|
|
59
|
+
});
|
|
60
|
+
ctx.addRoutes(builder);
|
|
61
|
+
await ctx.start();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
after(async () => {
|
|
65
|
+
await ctx.stop();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('sendBody dispatches through the route pipeline', async () => {
|
|
69
|
+
const pt = new ProducerTemplate(ctx);
|
|
70
|
+
const exchange = await pt.sendBody('direct:upper', 'hello');
|
|
71
|
+
assert.equal(exchange.in.body, 'HELLO');
|
|
72
|
+
assert.equal(exchange.exception, null);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('sendBody passes headers through to the route', async () => {
|
|
76
|
+
const { RouteBuilder } = await import('../src/RouteBuilder.js');
|
|
77
|
+
const builder = new RouteBuilder();
|
|
78
|
+
builder.from('direct:echoHeader').process(ex => {
|
|
79
|
+
ex.in.body = ex.in.getHeader('x-test');
|
|
80
|
+
});
|
|
81
|
+
ctx.addRoutes(builder);
|
|
82
|
+
// no restart needed — direct: registers consumer on addRoutes start()
|
|
83
|
+
// but context is already started so we need to start consumer manually
|
|
84
|
+
// Actually the route is registered but not started — we need a second context start
|
|
85
|
+
// or use the consumer directly. Simpler: test header forwarding via existing route.
|
|
86
|
+
const pt = new ProducerTemplate(ctx);
|
|
87
|
+
const exchange = await pt.sendBody('direct:upper', 'world', { 'x-val': '42' });
|
|
88
|
+
assert.equal(exchange.in.getHeader('x-val'), '42');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('sendBody returns exchange with no exception on success', async () => {
|
|
92
|
+
const pt = new ProducerTemplate(ctx);
|
|
93
|
+
const exchange = await pt.sendBody('direct:upper', 'test');
|
|
94
|
+
assert.equal(exchange.isFailed(), false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('ProducerTemplate: requestBody integration', () => {
|
|
99
|
+
let ctx;
|
|
100
|
+
|
|
101
|
+
before(async () => {
|
|
102
|
+
ctx = makeContext();
|
|
103
|
+
const { RouteBuilder } = await import('../src/RouteBuilder.js');
|
|
104
|
+
const builder = new RouteBuilder();
|
|
105
|
+
// Route sets out.body explicitly (InOut response pattern)
|
|
106
|
+
builder.from('direct:echo-out').process(ex => {
|
|
107
|
+
ex.out.body = `echo:${ex.in.body}`;
|
|
108
|
+
});
|
|
109
|
+
// Route only mutates in.body (in-place pattern — requestBody falls back)
|
|
110
|
+
builder.from('direct:mutate-in').process(ex => {
|
|
111
|
+
ex.in.body = `mutated:${ex.in.body}`;
|
|
112
|
+
});
|
|
113
|
+
ctx.addRoutes(builder);
|
|
114
|
+
await ctx.start();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
after(async () => {
|
|
118
|
+
await ctx.stop();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('returns out.body when route sets it', async () => {
|
|
122
|
+
const pt = new ProducerTemplate(ctx);
|
|
123
|
+
const result = await pt.requestBody('direct:echo-out', 'ping');
|
|
124
|
+
assert.equal(result, 'echo:ping');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('falls back to in.body when out.body is not set', async () => {
|
|
128
|
+
const pt = new ProducerTemplate(ctx);
|
|
129
|
+
const result = await pt.requestBody('direct:mutate-in', 'data');
|
|
130
|
+
assert.equal(result, 'mutated:data');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { CamelContext } from '../src/CamelContext.js';
|
|
4
|
+
|
|
5
|
+
describe('CamelContext', () => {
|
|
6
|
+
it('constructs without error', () => {
|
|
7
|
+
const ctx = new CamelContext();
|
|
8
|
+
assert.ok(ctx instanceof CamelContext);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('started is false initially', () => {
|
|
12
|
+
const ctx = new CamelContext();
|
|
13
|
+
assert.equal(ctx.started, false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('start() is async and sets started to true', async () => {
|
|
17
|
+
const ctx = new CamelContext();
|
|
18
|
+
await ctx.start();
|
|
19
|
+
assert.equal(ctx.started, true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('stop() sets started to false', async () => {
|
|
23
|
+
const ctx = new CamelContext();
|
|
24
|
+
await ctx.start();
|
|
25
|
+
await ctx.stop();
|
|
26
|
+
assert.equal(ctx.started, false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('addComponent returns this (fluent)', () => {
|
|
30
|
+
const ctx = new CamelContext();
|
|
31
|
+
const stub = {};
|
|
32
|
+
const result = ctx.addComponent('test', stub);
|
|
33
|
+
assert.equal(result, ctx);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('getComponent returns the registered component', () => {
|
|
37
|
+
const ctx = new CamelContext();
|
|
38
|
+
const stub = { name: 'stubComponent' };
|
|
39
|
+
ctx.addComponent('direct', stub);
|
|
40
|
+
assert.equal(ctx.getComponent('direct'), stub);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('addComponent is chainable', () => {
|
|
44
|
+
const ctx = new CamelContext();
|
|
45
|
+
const a = { id: 'a' };
|
|
46
|
+
const b = { id: 'b' };
|
|
47
|
+
ctx.addComponent('a', a).addComponent('b', b);
|
|
48
|
+
assert.equal(ctx.getComponent('a'), a);
|
|
49
|
+
assert.equal(ctx.getComponent('b'), b);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('CamelContext bean registry', () => {
|
|
54
|
+
it('registerBean/getBean round-trips a bean by name', () => {
|
|
55
|
+
const ctx = new CamelContext();
|
|
56
|
+
const db = { type: 'sqlite' };
|
|
57
|
+
ctx.registerBean('myDb', db);
|
|
58
|
+
assert.equal(ctx.getBean('myDb'), db);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('getBean returns undefined for unknown name', () => {
|
|
62
|
+
const ctx = new CamelContext();
|
|
63
|
+
assert.equal(ctx.getBean('ghost'), undefined);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('getBeans returns all registered beans as [name, bean] pairs', () => {
|
|
67
|
+
const ctx = new CamelContext();
|
|
68
|
+
const db1 = { id: 1 };
|
|
69
|
+
const db2 = { id: 2 };
|
|
70
|
+
ctx.registerBean('ds1', db1);
|
|
71
|
+
ctx.registerBean('ds2', db2);
|
|
72
|
+
const entries = ctx.getBeans();
|
|
73
|
+
assert.equal(entries.length, 2);
|
|
74
|
+
assert.deepEqual(entries.find(([n]) => n === 'ds1')?.[1], db1);
|
|
75
|
+
assert.deepEqual(entries.find(([n]) => n === 'ds2')?.[1], db2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('getBeans returns empty array when no beans registered', () => {
|
|
79
|
+
const ctx = new CamelContext();
|
|
80
|
+
assert.deepEqual(ctx.getBeans(), []);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('registerBean is fluent (returns context)', () => {
|
|
84
|
+
const ctx = new CamelContext();
|
|
85
|
+
const result = ctx.registerBean('x', {});
|
|
86
|
+
assert.equal(result, ctx);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('registerBean overwrites existing bean with same name', () => {
|
|
90
|
+
const ctx = new CamelContext();
|
|
91
|
+
const orig = { v: 1 };
|
|
92
|
+
const updated = { v: 2 };
|
|
93
|
+
ctx.registerBean('key', orig);
|
|
94
|
+
ctx.registerBean('key', updated);
|
|
95
|
+
assert.equal(ctx.getBean('key'), updated);
|
|
96
|
+
});
|
|
97
|
+
});
|
package/test/dsl.test.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
Exchange, CamelContext, RouteDefinition,
|
|
5
|
+
simple, js, constant,
|
|
6
|
+
} from '../src/index.js';
|
|
7
|
+
import { Pipeline } from '../src/Pipeline.js';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Helper: compile a RouteDefinition to a Pipeline and run it against an exchange
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
function makeExchange(body) {
|
|
14
|
+
const ex = new Exchange();
|
|
15
|
+
ex.in.body = body;
|
|
16
|
+
return ex;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function run(routeDef, exchange, context = null) {
|
|
20
|
+
const pipeline = routeDef.compile(context);
|
|
21
|
+
await pipeline.run(exchange);
|
|
22
|
+
return exchange;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// constant() expression
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
describe('constant() expression', () => {
|
|
30
|
+
it('always returns the given value regardless of exchange', async () => {
|
|
31
|
+
const fn = constant('hello');
|
|
32
|
+
assert.equal(typeof fn._fn, 'function');
|
|
33
|
+
assert.equal(fn._fn(), 'hello');
|
|
34
|
+
assert.equal(fn._fn({ whatever: true }), 'hello');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('works with normaliseExpression', () => {
|
|
38
|
+
// constant returns a _camelExpr object compatible with normaliseExpression
|
|
39
|
+
const expr = constant(42);
|
|
40
|
+
assert.ok(expr._camelExpr);
|
|
41
|
+
assert.equal(expr._fn(), 42);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// setBody
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
describe('setBody()', () => {
|
|
50
|
+
it('replaces body with constant expression', async () => {
|
|
51
|
+
const route = new RouteDefinition('direct:test');
|
|
52
|
+
route.setBody(constant('hello'));
|
|
53
|
+
const ex = makeExchange('original');
|
|
54
|
+
await run(route, ex);
|
|
55
|
+
assert.equal(ex.in.body, 'hello');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('replaces body with simple expression referencing a header', async () => {
|
|
59
|
+
const route = new RouteDefinition('direct:test');
|
|
60
|
+
route.setBody(simple('${header.X-Type}'));
|
|
61
|
+
const ex = makeExchange('original');
|
|
62
|
+
ex.in.setHeader('X-Type', 'order');
|
|
63
|
+
await run(route, ex);
|
|
64
|
+
assert.equal(ex.in.body, 'order');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('replaces body with js expression', async () => {
|
|
68
|
+
const route = new RouteDefinition('direct:test');
|
|
69
|
+
route.setBody(js('exchange.in.body.toUpperCase()'));
|
|
70
|
+
const ex = makeExchange('hello');
|
|
71
|
+
await run(route, ex);
|
|
72
|
+
assert.equal(ex.in.body, 'HELLO');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// setHeader
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
describe('setHeader()', () => {
|
|
81
|
+
it('sets a header with constant value', async () => {
|
|
82
|
+
const route = new RouteDefinition('direct:test');
|
|
83
|
+
route.setHeader('X-Source', constant('camel-lite'));
|
|
84
|
+
const ex = makeExchange('body');
|
|
85
|
+
await run(route, ex);
|
|
86
|
+
assert.equal(ex.in.getHeader('X-Source'), 'camel-lite');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('sets a header from body via simple expression', async () => {
|
|
90
|
+
const route = new RouteDefinition('direct:test');
|
|
91
|
+
route.setHeader('X-Body', simple('${body}'));
|
|
92
|
+
const ex = makeExchange('my-value');
|
|
93
|
+
await run(route, ex);
|
|
94
|
+
assert.equal(ex.in.getHeader('X-Body'), 'my-value');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('does not modify exchange body', async () => {
|
|
98
|
+
const route = new RouteDefinition('direct:test');
|
|
99
|
+
route.setHeader('X-Foo', constant('bar'));
|
|
100
|
+
const ex = makeExchange('unchanged');
|
|
101
|
+
await run(route, ex);
|
|
102
|
+
assert.equal(ex.in.body, 'unchanged');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// setProperty
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe('setProperty()', () => {
|
|
111
|
+
it('sets an exchange property with js expression', async () => {
|
|
112
|
+
const route = new RouteDefinition('direct:test');
|
|
113
|
+
route.setBody(constant({ name: 'Alice' }));
|
|
114
|
+
route.setProperty('saved', js('exchange.in.body'));
|
|
115
|
+
const ex = makeExchange(null);
|
|
116
|
+
await run(route, ex);
|
|
117
|
+
assert.deepEqual(ex.getProperty('saved'), { name: 'Alice' });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('sets property with constant', async () => {
|
|
121
|
+
const route = new RouteDefinition('direct:test');
|
|
122
|
+
route.setProperty('version', constant(2));
|
|
123
|
+
const ex = makeExchange(null);
|
|
124
|
+
await run(route, ex);
|
|
125
|
+
assert.equal(ex.getProperty('version'), 2);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// removeHeader
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
describe('removeHeader()', () => {
|
|
134
|
+
it('removes a named header from the exchange', async () => {
|
|
135
|
+
const route = new RouteDefinition('direct:test');
|
|
136
|
+
route.removeHeader('X-Temp');
|
|
137
|
+
const ex = makeExchange('body');
|
|
138
|
+
ex.in.setHeader('X-Temp', 'to-be-removed');
|
|
139
|
+
await run(route, ex);
|
|
140
|
+
assert.equal(ex.in.getHeader('X-Temp'), undefined);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('does not throw when header is absent', async () => {
|
|
144
|
+
const route = new RouteDefinition('direct:test');
|
|
145
|
+
route.removeHeader('X-Missing');
|
|
146
|
+
const ex = makeExchange('body');
|
|
147
|
+
await assert.doesNotReject(() => run(route, ex));
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// log
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
describe('log()', () => {
|
|
156
|
+
it('does not throw and does not modify exchange body', async () => {
|
|
157
|
+
const route = new RouteDefinition('direct:test');
|
|
158
|
+
route.log('hello from route');
|
|
159
|
+
const ex = makeExchange('original-body');
|
|
160
|
+
await assert.doesNotReject(() => run(route, ex));
|
|
161
|
+
assert.equal(ex.in.body, 'original-body');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('accepts a simple() expression as message', async () => {
|
|
165
|
+
// simple() expressions must be pure expressions, not text with embedded values.
|
|
166
|
+
// For log, use a js() expression to build a formatted message string.
|
|
167
|
+
const route = new RouteDefinition('direct:test');
|
|
168
|
+
route.log(js('`body is ${exchange.in.body}`'));
|
|
169
|
+
const ex = makeExchange('test-value');
|
|
170
|
+
await assert.doesNotReject(() => run(route, ex));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// marshal / unmarshal
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
describe('marshal()', () => {
|
|
179
|
+
it('serialises object body to JSON string', async () => {
|
|
180
|
+
const route = new RouteDefinition('direct:test');
|
|
181
|
+
route.marshal('json');
|
|
182
|
+
const ex = makeExchange({ name: 'Widget', price: 9.99 });
|
|
183
|
+
await run(route, ex);
|
|
184
|
+
assert.equal(typeof ex.in.body, 'string');
|
|
185
|
+
const parsed = JSON.parse(ex.in.body);
|
|
186
|
+
assert.equal(parsed.name, 'Widget');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('defaults to json format', async () => {
|
|
190
|
+
const route = new RouteDefinition('direct:test');
|
|
191
|
+
route.marshal(); // no arg
|
|
192
|
+
const ex = makeExchange({ x: 1 });
|
|
193
|
+
await run(route, ex);
|
|
194
|
+
assert.equal(ex.in.body, '{"x":1}');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('unmarshal()', () => {
|
|
199
|
+
it('deserialises JSON string to object', async () => {
|
|
200
|
+
const route = new RouteDefinition('direct:test');
|
|
201
|
+
route.unmarshal('json');
|
|
202
|
+
const ex = makeExchange('{"name":"Gadget","price":24.99}');
|
|
203
|
+
await run(route, ex);
|
|
204
|
+
assert.equal(ex.in.body.name, 'Gadget');
|
|
205
|
+
assert.equal(ex.in.body.price, 24.99);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('marshal → unmarshal round-trip', () => {
|
|
210
|
+
it('round-trips an object through JSON serialisation', async () => {
|
|
211
|
+
const original = { id: 1, tags: ['a', 'b'], nested: { x: true } };
|
|
212
|
+
const route = new RouteDefinition('direct:test');
|
|
213
|
+
route.marshal().unmarshal();
|
|
214
|
+
const ex = makeExchange(original);
|
|
215
|
+
await run(route, ex);
|
|
216
|
+
assert.deepEqual(ex.in.body, original);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// convertBodyTo
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
describe('convertBodyTo()', () => {
|
|
225
|
+
it('converts number to String', async () => {
|
|
226
|
+
const route = new RouteDefinition('direct:test');
|
|
227
|
+
route.convertBodyTo('String');
|
|
228
|
+
const ex = makeExchange(42);
|
|
229
|
+
await run(route, ex);
|
|
230
|
+
assert.equal(ex.in.body, '42');
|
|
231
|
+
assert.equal(typeof ex.in.body, 'string');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('converts string to Number', async () => {
|
|
235
|
+
const route = new RouteDefinition('direct:test');
|
|
236
|
+
route.convertBodyTo('Number');
|
|
237
|
+
const ex = makeExchange('3.14');
|
|
238
|
+
await run(route, ex);
|
|
239
|
+
assert.equal(ex.in.body, 3.14);
|
|
240
|
+
assert.equal(typeof ex.in.body, 'number');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('converts string "true" to Boolean true', async () => {
|
|
244
|
+
const route = new RouteDefinition('direct:test');
|
|
245
|
+
route.convertBodyTo('Boolean');
|
|
246
|
+
const ex = makeExchange('true');
|
|
247
|
+
await run(route, ex);
|
|
248
|
+
assert.equal(ex.in.body, true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('converts string "false" to Boolean false', async () => {
|
|
252
|
+
const route = new RouteDefinition('direct:test');
|
|
253
|
+
route.convertBodyTo('Boolean');
|
|
254
|
+
const ex = makeExchange('false');
|
|
255
|
+
await run(route, ex);
|
|
256
|
+
assert.equal(ex.in.body, false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('throws on unsupported type', async () => {
|
|
260
|
+
const route = new RouteDefinition('direct:test');
|
|
261
|
+
route.convertBodyTo('Date');
|
|
262
|
+
const ex = makeExchange('2024-01-01');
|
|
263
|
+
await run(route, ex);
|
|
264
|
+
// Pipeline captures errors on exchange.exception
|
|
265
|
+
assert.ok(ex.exception != null, 'exchange should have an exception');
|
|
266
|
+
assert.match(ex.exception.message, /unsupported type 'Date'/i);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// stop
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
describe('stop()', () => {
|
|
275
|
+
it('stops exchange processing cleanly — body not modified by subsequent steps', async () => {
|
|
276
|
+
const route = new RouteDefinition('direct:test');
|
|
277
|
+
route
|
|
278
|
+
.setBody(constant('before-stop'))
|
|
279
|
+
.stop()
|
|
280
|
+
.setBody(constant('after-stop')); // should not execute
|
|
281
|
+
|
|
282
|
+
const ex = makeExchange('initial');
|
|
283
|
+
// Pipeline swallows CamelFilterStopException — no exception to caller
|
|
284
|
+
await assert.doesNotReject(() => run(route, ex));
|
|
285
|
+
assert.equal(ex.in.body, 'before-stop');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// bean
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
describe('bean() with function', () => {
|
|
294
|
+
it('executes a function processor', async () => {
|
|
295
|
+
const route = new RouteDefinition('direct:test');
|
|
296
|
+
route.bean(async (exchange) => { exchange.in.body = 'from-bean-fn'; });
|
|
297
|
+
const ex = makeExchange('original');
|
|
298
|
+
await run(route, ex);
|
|
299
|
+
assert.equal(ex.in.body, 'from-bean-fn');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('bean() with object', () => {
|
|
304
|
+
it('executes an object with process() method', async () => {
|
|
305
|
+
const processor = {
|
|
306
|
+
async process(exchange) { exchange.in.body = 'from-bean-obj'; },
|
|
307
|
+
};
|
|
308
|
+
const route = new RouteDefinition('direct:test');
|
|
309
|
+
route.bean(processor);
|
|
310
|
+
const ex = makeExchange('original');
|
|
311
|
+
await run(route, ex);
|
|
312
|
+
assert.equal(ex.in.body, 'from-bean-obj');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('bean() with string name (context lookup)', () => {
|
|
317
|
+
it('looks up bean from context at runtime and executes it', async () => {
|
|
318
|
+
const ctx = new CamelContext();
|
|
319
|
+
ctx.registerBean('myProc', async (exchange) => { exchange.in.body = 'from-ctx-bean'; });
|
|
320
|
+
|
|
321
|
+
const route = new RouteDefinition('direct:test');
|
|
322
|
+
route.bean('myProc');
|
|
323
|
+
const ex = makeExchange('original');
|
|
324
|
+
const pipeline = route.compile(ctx);
|
|
325
|
+
await pipeline.run(ex);
|
|
326
|
+
assert.equal(ex.in.body, 'from-ctx-bean');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('throws descriptively when bean not found in context', async () => {
|
|
330
|
+
const ctx = new CamelContext();
|
|
331
|
+
const route = new RouteDefinition('direct:test');
|
|
332
|
+
route.bean('ghost');
|
|
333
|
+
const ex = makeExchange('x');
|
|
334
|
+
const pipeline = route.compile(ctx);
|
|
335
|
+
await pipeline.run(ex);
|
|
336
|
+
// Pipeline captures error on exchange.exception
|
|
337
|
+
assert.ok(ex.exception != null, 'exchange should have an exception');
|
|
338
|
+
assert.match(ex.exception.message, /bean\('ghost'\).*no bean registered/i);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// Chained pipeline integration
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
describe('chained new DSL steps integration', () => {
|
|
347
|
+
it('setHeader → setBody(simple) → setProperty → marshal → unmarshal round-trip', async () => {
|
|
348
|
+
const ctx = new CamelContext();
|
|
349
|
+
const route = new RouteDefinition('direct:test');
|
|
350
|
+
route
|
|
351
|
+
.setHeader('X-Type', constant('invoice'))
|
|
352
|
+
.setBody(simple('${header.X-Type}'))
|
|
353
|
+
.setProperty('type', js('exchange.in.body'))
|
|
354
|
+
.setBody(constant({ amount: 100, currency: 'USD' }))
|
|
355
|
+
.marshal()
|
|
356
|
+
.unmarshal();
|
|
357
|
+
|
|
358
|
+
const ex = makeExchange(null);
|
|
359
|
+
const pipeline = route.compile(ctx);
|
|
360
|
+
await pipeline.run(ex);
|
|
361
|
+
|
|
362
|
+
assert.equal(ex.in.getHeader('X-Type'), 'invoice');
|
|
363
|
+
assert.equal(ex.getProperty('type'), 'invoice');
|
|
364
|
+
assert.equal(ex.in.body.amount, 100);
|
|
365
|
+
assert.equal(ex.in.body.currency, 'USD');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('convertBodyTo after marshal converts JSON string to String type (no-op)', async () => {
|
|
369
|
+
const route = new RouteDefinition('direct:test');
|
|
370
|
+
route.marshal().convertBodyTo('String');
|
|
371
|
+
const ex = makeExchange({ x: 1 });
|
|
372
|
+
await run(route, ex);
|
|
373
|
+
assert.equal(typeof ex.in.body, 'string');
|
|
374
|
+
});
|
|
375
|
+
});
|