lex-transformer 0.2.1 → 0.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7f4f19cb531c6f7cb50302bb3f2aab973b55976bb05d1b870ac2ad4f7bdb9a0
4
- data.tar.gz: a445f592b23aae48a5510a7c6f17ee3cf0ea0a29dd3c9d67bc405b5b208a0c72
3
+ metadata.gz: 111582b1dfcbe44b2cf91d3b316bd6394eb09e4d750a7b19be8844e91257e2c5
4
+ data.tar.gz: 6efebcb5ea769c9426e17e6be93aa1cc0ca1bf5093e02d9f5ea0fe5254809760
5
5
  SHA512:
6
- metadata.gz: 21096cee7c407f8df2aa5307e3fe71e35008103a798bc7f9c9cd8dfd1724728af7d65fbeeada8b5df657c412bdcfd44c60fd100efb2a3f63815c6615e63f9917
7
- data.tar.gz: 4e6348dc31ae9217b34040010ecf8b613b8b0e97ae1995854a3f30c821bce8a5b6d190bb283ad9a06e305fe128b68bd8a3533772451d608c55912659c1970ec9
6
+ metadata.gz: 8af3daa3467d9e37e571319c876a17ff772e3558c0510fcca75a633e970ed32b0497c1e31f63faa047e0eb2f6e8488d8a55820d0d5e46394450957228f4ec3af
7
+ data.tar.gz: a8dc1caf0ce16261c8eecf6d6ed37b2af4ff1cf4736465047a67cfd96eeb431a1454d4a31f2c34861219fea481b8d79b42d13b860a3935d240f81b89669c870d
data/.gitignore CHANGED
@@ -11,3 +11,4 @@
11
11
  .rspec_status
12
12
 
13
13
  Gemfile.lock
14
+ Gemfile.lock
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] - 2026-03-19
4
+
5
+ ### Added
6
+ - LLM engine error handling with categorized retry (timeout/network retry, auth errors raise, provider errors return failure)
7
+ - LLM engine model/provider/temperature/system_prompt kwargs via `engine_options`
8
+ - LLM engine structured output support (`structured: true` + `schema:`)
9
+ - LLM engine JSON response validation with correction prompt on retry
10
+ - Settings-based LLM defaults (`lex-transformer.llm.*`)
11
+ - Named transform definitions via Settings (`lex-transformer.definitions.*`)
12
+ - `Definitions` class for Settings-based definition lookup
13
+ - `name:` parameter on `Client#transform` for named definition execution
14
+ - `engine_options:` parameter on `Client#transform` and `Client#transform_chain`
15
+ - Conditioner integration for named definitions with conditions
16
+ - LLM failure hash passthrough (no dispatch on engine failure)
17
+ - Integration specs for LLM engine with client, chain, and named definitions
18
+
19
+ ### Changed
20
+ - All engine `render` signatures accept `**opts` (backward-compatible, non-LLM engines ignore)
21
+
3
22
  ## [0.2.1] - 2026-03-17
4
23
 
5
24
  ### Added
data/CLAUDE.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## Purpose
8
8
 
9
- Legion Extension that transforms task payloads between services in a relationship chain. Uses ERB template-based transformation (via the `tilt` gem) to map data from one task's output into the format expected by the next task's input. Supports single-hash output (1:1 dispatch) and array output (fan-out/multiply). Requires `legion-data` (`data_required? true`).
9
+ Legion Extension that transforms task payloads between services in a relationship chain. Uses pluggable template engines (ERB, Static, Liquid, JSONPath, LLM) to map data from one task's output into the format expected by the next task's input. Supports single-hash output (1:1 dispatch), array output (fan-out/multiply), schema validation, and sequential transform chains. Requires `legion-data` (`data_required? true`).
10
10
 
11
11
  **GitHub**: https://github.com/LegionIO/lex-transformer
12
12
  **License**: MIT
@@ -18,18 +18,30 @@ Legion Extension that transforms task payloads between services in a relationshi
18
18
  Legion::Extensions::Transformer
19
19
  ├── Actors/
20
20
  │ └── Transform # Subscription actor consuming transform requests
21
+ ├── Engines/
22
+ │ ├── Base # Abstract engine interface
23
+ │ ├── Registry # Maps engine name symbols to engine classes
24
+ │ ├── Erb # ERB rendering via tilt
25
+ │ ├── Static # JSON passthrough (no templating)
26
+ │ ├── Liquid # Liquid template rendering
27
+ │ ├── Jsonpath # Dot-notation value extraction from payload
28
+ │ └── Llm # Natural language transformation via Legion::LLM
29
+ ├── Helpers/
30
+ │ └── SchemaValidator # Validates transform output against required_keys/types schema
21
31
  ├── Runners/
22
32
  │ └── Transform # Executes template-based payload transformation
23
- │ ├── transform # Entry point: render + dispatch
24
- │ ├── render_transformation # ERB rendering via tilt, or plain JSON if no ERB tags
25
- │ ├── build_template_variables # Inject crypt/settings/cache/task into ERB scope
33
+ │ ├── transform # Entry point: render + dispatch or validate
34
+ │ ├── transform_chain # Sequential pipeline: N steps, output feeds next
35
+ │ ├── render_transformation # Engine dispatch (ERB/Static/Liquid/JSONPath/LLM)
36
+ │ ├── build_template_variables # Inject crypt/settings/cache/task into scope
26
37
  │ ├── dispatch_transformed # Route Hash (single) or Array (fan-out) results
27
38
  │ ├── dispatch_multiplied # Fan-out: create a new task per array element
28
39
  │ └── send_task # Publish transformed payload to next runner
29
- └── Transport/
30
- ├── Exchanges/Task # Publishes to the task exchange
31
- ├── Queues/Transform # Subscribes to transformation queue
32
- └── Messages/Message # Transform request message format
40
+ ├── Transport/
41
+ ├── Exchanges/Task # Publishes to the task exchange
42
+ ├── Queues/Transform # Subscribes to transformation queue
43
+ └── Messages/Message # Transform request message format
44
+ └── Client # Standalone client: transform + transform_chain (uses Engines::Registry directly)
33
45
  ```
34
46
 
35
47
  ## Key Files
@@ -39,8 +51,25 @@ Legion::Extensions::Transformer
39
51
  | `lib/legion/extensions/transformer.rb` | Entry point (`data_required? true`) |
40
52
  | `lib/legion/extensions/transformer/runners/transform.rb` | Core transformation logic |
41
53
  | `lib/legion/extensions/transformer/actors/transform.rb` | AMQP subscription actor |
54
+ | `lib/legion/extensions/transformer/client.rb` | Standalone client (transform, transform_chain) |
55
+ | `lib/legion/extensions/transformer/engines/registry.rb` | Engine name -> class lookup + auto-detection |
56
+ | `lib/legion/extensions/transformer/helpers/schema_validator.rb` | Output schema validation |
42
57
  | `lib/legion/extensions/transformer/transport.rb` | Transport setup |
43
58
 
59
+ ## Template Engines
60
+
61
+ | Engine | Name | Detection | Description |
62
+ |--------|------|-----------|-------------|
63
+ | ERB | `:erb` | `<%` or `%>` in template | Full ERB template rendering via `tilt` |
64
+ | Static | `:static` | Default (no ERB markers) | Plain JSON passthrough |
65
+ | Liquid | `:liquid` | Explicit only | Liquid template rendering (`{{ var }}`) |
66
+ | JSONPath | `:jsonpath` | Explicit only | Dot-notation value extraction from payload |
67
+ | LLM | `:llm` | Explicit only | Natural language transformation via `Legion::LLM` |
68
+
69
+ Auto-detection: ERB when template contains `<%` or `%>`, otherwise Static. Pass `engine:` to force a specific engine.
70
+
71
+ The LLM engine requires `legion-llm` to be started; it is provider-agnostic (Ollama, Bedrock, Anthropic, OpenAI, Gemini).
72
+
44
73
  ## Template Variables Available in ERB
45
74
 
46
75
  | Variable | Available when |
@@ -51,28 +80,55 @@ Legion::Extensions::Transformer
51
80
  | `cache` | Template string contains `'cache'` |
52
81
  | `task` | Template string contains `'task'` and payload has `task_id` |
53
82
 
83
+ ## Schema Validation
84
+
85
+ ```ruby
86
+ schema = {
87
+ required_keys: [:name, :email],
88
+ types: { name: String, email: String, age: Integer }
89
+ }
90
+ ```
91
+
92
+ On failure: `{ success: false, status: 'transformer.validation_failed', errors: [...] }` — no dispatch.
93
+
54
94
  ## Dispatch Behavior
55
95
 
56
96
  - **Hash result**: Updates task to `transformer.succeeded`, dispatches single task, updates to `task.queued`
57
- - **Array result**: Fan-out via `dispatch_multiplied` - creates a new task record per element, dispatches each, marks original as `task.multiplied`
58
- - **Plain JSON** (no ERB tags): Parsed directly without template rendering
97
+ - **Array result**: Fan-out via `dispatch_multiplied` creates a new task record per element, dispatches each, marks original as `task.multiplied`
98
+ - **Schema failure**: `transformer.validation_failed`, no dispatch
99
+
100
+ ## Transform Chains
101
+
102
+ `transform_chain(steps:, **payload)` runs steps sequentially. Each step specifies `:transformation`, optional `:engine`, and optional `:schema`. Output of step N is merged into the running payload for step N+1. Stops on first schema failure.
103
+
104
+ ## Standalone Client
105
+
106
+ `Legion::Extensions::Transformer::Client` includes the Transform runner:
107
+
108
+ ```ruby
109
+ require 'legion/extensions/transformer/client'
110
+ client = Legion::Extensions::Transformer::Client.new
111
+ result = client.transform(transformation: '{"x":"<%= y %>"}', payload: { y: 'hello' })
112
+ result[:success] # => true
113
+ result[:result] # => { x: "hello" }
114
+ ```
59
115
 
60
116
  ## Dependencies
61
117
 
62
118
  | Gem | Purpose |
63
119
  |-----|---------|
64
120
  | `tilt` (>= 2.3) | Template engine abstraction for ERB rendering |
65
- | `legion-data` | Required - task record creation for fan-out |
121
+ | `legion-data` | Required task record creation for fan-out |
66
122
 
67
123
  ## Testing
68
124
 
69
125
  ```bash
70
126
  bundle install
71
- bundle exec rspec
72
- bundle exec rubocop
127
+ bundle exec rspec # 86 examples, 0 failures
128
+ bundle exec rubocop # 0 offenses
73
129
  ```
74
130
 
75
- Spec files: `spec/legion/extensions/tranformer_spec.rb` (note: typo in filename), `spec/legion/extensions/transform_runner_spec.rb`
131
+ Spec files: `spec/legion/extensions/tranformer_spec.rb` (note: typo in filename is intentional in repo), `spec/legion/extensions/transform_runner_spec.rb`
76
132
 
77
133
  ---
78
134
 
@@ -2,16 +2,21 @@
2
2
 
3
3
  require_relative 'engines/registry'
4
4
  require_relative 'helpers/schema_validator'
5
+ require_relative 'definitions'
5
6
 
6
7
  module Legion
7
8
  module Extensions
8
9
  module Transformer
9
10
  class Client
10
- def transform(transformation:, payload:, engine: nil, schema: nil)
11
+ def transform(payload:, transformation: nil, engine: nil, schema: nil, engine_options: {}, name: nil)
12
+ return transform_by_name(name: name, payload: payload, engine_options: engine_options) if transformation.nil? && name
13
+
11
14
  eng = resolve_engine(engine, transformation)
12
- rendered = eng.render(transformation, payload)
15
+ rendered = eng.render(transformation, payload, **engine_options)
13
16
  rendered = parse_rendered(rendered)
14
17
 
18
+ return rendered if rendered.is_a?(Hash) && rendered[:success] == false
19
+
15
20
  if schema
16
21
  validation = Helpers::SchemaValidator.validate(schema: schema, data: rendered)
17
22
  return { success: false, status: 'transformer.validation_failed', errors: validation[:errors] } unless validation[:valid]
@@ -24,9 +29,12 @@ module Legion
24
29
  result = payload.dup
25
30
  steps.each do |step|
26
31
  eng = resolve_engine(step[:engine], step[:transformation])
27
- rendered = eng.render(step[:transformation], result)
32
+ step_opts = step[:engine_options] || {}
33
+ rendered = eng.render(step[:transformation], result, **step_opts)
28
34
  rendered = parse_rendered(rendered)
29
35
 
36
+ return rendered if rendered.is_a?(Hash) && rendered[:success] == false
37
+
30
38
  if step[:schema]
31
39
  validation = Helpers::SchemaValidator.validate(schema: step[:schema], data: rendered)
32
40
  return { success: false, status: 'transformer.validation_failed', errors: validation[:errors] } unless validation[:valid]
@@ -43,6 +51,38 @@ module Legion
43
51
 
44
52
  private
45
53
 
54
+ def transform_by_name(name:, payload:, engine_options: {})
55
+ definition = Definitions.fetch(name)
56
+ return { success: false, error: 'definition_not_found' } unless definition
57
+
58
+ if definition[:conditions] && conditioner_available?
59
+ cond_result = evaluate_conditions(definition[:conditions], payload)
60
+ return { success: false, reason: 'conditions_not_met' } unless cond_result
61
+ end
62
+
63
+ merged_opts = Definitions.merge_options(definition, **engine_options)
64
+
65
+ transform(
66
+ transformation: definition[:transformation],
67
+ payload: payload,
68
+ engine: definition[:engine],
69
+ schema: definition[:schema],
70
+ engine_options: merged_opts
71
+ )
72
+ end
73
+
74
+ def conditioner_available?
75
+ defined?(Legion::Extensions::Conditioner::Client)
76
+ end
77
+
78
+ def evaluate_conditions(conditions, payload)
79
+ client = Legion::Extensions::Conditioner::Client.new
80
+ result = client.evaluate(conditions: conditions, values: payload)
81
+ result[:passed]
82
+ rescue StandardError
83
+ true
84
+ end
85
+
46
86
  def resolve_engine(engine_name, transformation)
47
87
  if engine_name
48
88
  Engines::Registry.fetch(engine_name)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Transformer
6
+ class Definitions
7
+ class << self
8
+ def fetch(name)
9
+ defns = load_definitions
10
+ return nil unless defns&.key?(name)
11
+
12
+ symbolize_definition(defns[name])
13
+ end
14
+
15
+ def names
16
+ defns = load_definitions
17
+ return [] unless defns
18
+
19
+ defns.keys
20
+ end
21
+
22
+ def merge_options(definition, **overrides)
23
+ base = definition[:engine_options] || {}
24
+ base.merge(overrides)
25
+ end
26
+
27
+ private
28
+
29
+ def load_definitions
30
+ return nil unless defined?(Legion::Settings)
31
+
32
+ Legion::Settings.dig('lex-transformer', 'definitions')
33
+ rescue StandardError
34
+ nil
35
+ end
36
+
37
+ def symbolize_definition(raw)
38
+ defn = {}
39
+ defn[:transformation] = raw['transformation'] || raw[:transformation]
40
+ defn[:engine] = (raw['engine'] || raw[:engine])&.to_sym
41
+ defn[:engine_options] = symbolize_hash(raw['engine_options'] || raw[:engine_options] || {})
42
+ defn[:schema] = raw['schema'] || raw[:schema]
43
+ defn[:conditions] = raw['conditions'] || raw[:conditions]
44
+ defn
45
+ end
46
+
47
+ def symbolize_hash(hash)
48
+ return {} unless hash.is_a?(Hash)
49
+
50
+ hash.transform_keys(&:to_sym)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -5,7 +5,7 @@ module Legion
5
5
  module Transformer
6
6
  module Engines
7
7
  class Base
8
- def render(template, payload)
8
+ def render(template, payload, **_opts)
9
9
  raise NotImplementedError, "#{self.class}#render must be implemented"
10
10
  end
11
11
 
@@ -12,7 +12,7 @@ module Legion
12
12
  :erb
13
13
  end
14
14
 
15
- def render(template, payload)
15
+ def render(template, payload, **_opts)
16
16
  tilt_template = Tilt['erb'].new { template }
17
17
  variables = build_variables(template, payload)
18
18
  tilt_template.render(Object.new, variables)
@@ -11,7 +11,7 @@ module Legion
11
11
  :jsonpath
12
12
  end
13
13
 
14
- def render(expression, payload)
14
+ def render(expression, payload, **_opts)
15
15
  extract(expression, payload)
16
16
  end
17
17
 
@@ -11,7 +11,7 @@ module Legion
11
11
  :liquid
12
12
  end
13
13
 
14
- def render(template, payload)
14
+ def render(template, payload, **_opts)
15
15
  require 'liquid'
16
16
  liquid_template = ::Liquid::Template.parse(template)
17
17
  liquid_template.render(stringify_keys(payload))
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'timeout'
3
4
  require_relative 'base'
4
5
 
5
6
  module Legion
@@ -7,28 +8,144 @@ module Legion
7
8
  module Transformer
8
9
  module Engines
9
10
  class Llm < Base
11
+ AUTH_ERRORS = /auth|credentials|forbidden|api.key|unauthorized/i
12
+ RETRY_SLEEP = 1
13
+
10
14
  def name
11
15
  :llm
12
16
  end
13
17
 
14
- def render(prompt, payload)
15
- raise 'Legion::LLM is not available or not started' unless defined?(Legion::LLM) && Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
18
+ def render(prompt, payload, **opts)
19
+ raise 'Legion::LLM is not available or not started' unless llm_available?
16
20
 
17
- context = Legion::JSON.dump(payload)
18
- full_prompt = "#{prompt}\n\nPayload:\n```json\n#{context}\n```\n\nRespond with valid JSON only. No explanation, no markdown fences."
21
+ @last_raw = nil
22
+ resolved = resolve_options(opts)
23
+ max_retries = resolved.delete(:max_retries)
24
+ attempts = 0
25
+
26
+ loop do
27
+ result = attempt_render(prompt, payload, resolved, attempts)
28
+ return result unless result == :retry
19
29
 
20
- chat = Legion::LLM.chat(message: full_prompt)
21
- extract_response(chat)
30
+ attempts += 1
31
+ return build_failure if attempts > max_retries
32
+
33
+ sleep(RETRY_SLEEP)
34
+ end
22
35
  end
23
36
 
24
37
  private
25
38
 
39
+ def llm_available?
40
+ defined?(Legion::LLM) && Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
41
+ end
42
+
43
+ def resolve_options(opts)
44
+ defaults = settings_defaults
45
+ resolved = defaults.merge(opts)
46
+ resolved[:max_retries] = (resolved[:max_retries] || 1).to_i
47
+ resolved
48
+ end
49
+
50
+ def settings_defaults
51
+ return {} unless defined?(Legion::Settings)
52
+
53
+ settings = begin
54
+ Legion::Settings.dig('lex-transformer', 'llm')
55
+ rescue StandardError
56
+ nil
57
+ end
58
+ return {} unless settings.is_a?(Hash)
59
+
60
+ settings.transform_keys(&:to_sym)
61
+ end
62
+
63
+ def attempt_render(prompt, payload, opts, attempt)
64
+ chat = call_llm(prompt, payload, opts, attempt)
65
+ content = extract_response(chat)
66
+ validate_json(content)
67
+ content
68
+ rescue Timeout::Error, IOError, Errno::ECONNREFUSED, Errno::ECONNRESET
69
+ :retry
70
+ rescue ::JSON::ParserError
71
+ @last_raw = content
72
+ :retry
73
+ rescue RuntimeError => e
74
+ raise if auth_error?(e)
75
+
76
+ { success: false, error: e.class.to_s, message: e.message }
77
+ rescue StandardError => e
78
+ { success: false, error: e.class.to_s, message: e.message }
79
+ end
80
+
81
+ def call_llm(prompt, payload, opts, attempt)
82
+ context = Legion::JSON.dump(payload)
83
+
84
+ return call_structured(prompt, context, opts) if opts[:structured] && opts[:schema] && structured_available?
85
+
86
+ full_prompt = build_prompt(prompt, context, attempt)
87
+ full_prompt = inject_schema_into_prompt(full_prompt, opts[:schema]) if opts[:structured] && opts[:schema]
88
+
89
+ llm_opts = build_llm_opts(opts)
90
+ Legion::LLM.chat(message: full_prompt, **llm_opts)
91
+ end
92
+
93
+ def structured_available?
94
+ Legion::LLM.respond_to?(:structured)
95
+ end
96
+
97
+ def call_structured(prompt, context, opts)
98
+ llm_opts = build_llm_opts(opts)
99
+ Legion::LLM.structured(
100
+ message: "#{prompt}\n\nPayload:\n```json\n#{context}\n```",
101
+ schema: opts[:schema],
102
+ **llm_opts
103
+ )
104
+ end
105
+
106
+ def inject_schema_into_prompt(prompt, schema)
107
+ schema_json = Legion::JSON.dump(schema)
108
+ "#{prompt}\n\nYour response MUST conform to this JSON schema:\n```json\n#{schema_json}\n```"
109
+ end
110
+
111
+ def build_prompt(prompt, context, attempt)
112
+ base = "#{prompt}\n\nPayload:\n```json\n#{context}\n```\n\nRespond with valid JSON only. No explanation, no markdown fences."
113
+ return base unless attempt.positive?
114
+
115
+ "#{base}\n\nIMPORTANT: Your previous response was not valid JSON. Return ONLY a valid JSON object or array, nothing else."
116
+ end
117
+
26
118
  def extract_response(chat)
27
119
  content = chat.respond_to?(:content) ? chat.content : chat.to_s
28
120
  content = content.strip
29
121
  content = content.sub(/\A```(?:json)?\n?/, '').sub(/\n?```\z/, '') if content.start_with?('```')
30
122
  content
31
123
  end
124
+
125
+ def validate_json(content)
126
+ ::JSON.parse(content)
127
+ end
128
+
129
+ def auth_error?(error)
130
+ AUTH_ERRORS.match?(error.message)
131
+ end
132
+
133
+ def build_llm_opts(opts)
134
+ llm_opts = {}
135
+ llm_opts[:model] = opts[:model] if opts[:model]
136
+ llm_opts[:provider] = opts[:provider] if opts[:provider]
137
+ llm_opts[:temperature] = opts[:temperature] if opts[:temperature]
138
+ llm_opts[:system_prompt] = opts[:system_prompt] if opts[:system_prompt]
139
+ llm_opts
140
+ end
141
+
142
+ def build_failure
143
+ if @last_raw
144
+ { success: false, error: 'invalid_json', message: 'max retries exhausted', raw: @last_raw }
145
+ else
146
+ { success: false, error: 'timeout', message: 'max retries exhausted' }
147
+ end
148
+ end
32
149
  end
33
150
  end
34
151
  end
@@ -11,7 +11,7 @@ module Legion
11
11
  :static
12
12
  end
13
13
 
14
- def render(template, _payload)
14
+ def render(template, _payload, **_opts)
15
15
  template
16
16
  end
17
17
  end
@@ -8,8 +8,11 @@ module Legion
8
8
  module Transformer
9
9
  module Runners
10
10
  module Transform
11
- def transform(transformation:, engine: nil, schema: nil, **payload)
12
- payload[:args] = render_transformation(transformation, payload, engine: engine)
11
+ def transform(transformation:, engine: nil, schema: nil, engine_options: {}, **payload)
12
+ payload[:args] = render_transformation(transformation, payload, engine: engine, engine_options: engine_options)
13
+
14
+ return payload[:args] if payload[:args].is_a?(Hash) && payload[:args][:success] == false
15
+
13
16
  if schema
14
17
  validation = Helpers::SchemaValidator.validate(schema: schema, data: payload[:args])
15
18
  return { success: false, status: 'transformer.validation_failed', errors: validation[:errors] } unless validation[:valid]
@@ -22,8 +25,12 @@ module Legion
22
25
  def transform_chain(steps:, **payload)
23
26
  result = payload
24
27
  steps.each do |step|
25
- rendered = render_transformation(step[:transformation], result, engine: step[:engine])
28
+ step_opts = step[:engine_options] || {}
29
+ rendered = render_transformation(step[:transformation], result, engine: step[:engine], engine_options: step_opts)
26
30
  rendered = from_json(rendered) if rendered.is_a?(String)
31
+
32
+ return rendered if rendered.is_a?(Hash) && rendered[:success] == false
33
+
27
34
  if step[:schema]
28
35
  validation = Helpers::SchemaValidator.validate(schema: step[:schema], data: rendered)
29
36
  return { success: false, status: 'transformer.validation_failed', errors: validation[:errors] } unless validation[:valid]
@@ -33,14 +40,16 @@ module Legion
33
40
  { success: true, **result }
34
41
  end
35
42
 
36
- def render_transformation(transformation, payload, engine: nil)
43
+ def render_transformation(transformation, payload, engine: nil, engine_options: {})
37
44
  eng = if engine
38
45
  Engines::Registry.fetch(engine)
39
46
  else
40
47
  Engines::Registry.detect(transformation)
41
48
  end
42
49
 
43
- rendered = eng.render(transformation, payload)
50
+ rendered = eng.render(transformation, payload, **engine_options)
51
+ return rendered if rendered.is_a?(Hash) && rendered[:success] == false
52
+
44
53
  rendered.is_a?(String) ? from_json(rendered) : rendered
45
54
  end
46
55
 
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Transformer
6
- VERSION = '0.2.1'
6
+ VERSION = '0.3.0'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-transformer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -47,6 +47,7 @@ files:
47
47
  - lib/legion/extensions/transformer.rb
48
48
  - lib/legion/extensions/transformer/actors/transform.rb
49
49
  - lib/legion/extensions/transformer/client.rb
50
+ - lib/legion/extensions/transformer/definitions.rb
50
51
  - lib/legion/extensions/transformer/engines/base.rb
51
52
  - lib/legion/extensions/transformer/engines/erb.rb
52
53
  - lib/legion/extensions/transformer/engines/jsonpath.rb