lex-transformer 0.1.4 → 0.2.1
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 +4 -4
- data/.github/workflows/ci.yml +16 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +37 -21
- data/CHANGELOG.md +35 -0
- data/CLAUDE.md +79 -0
- data/Dockerfile +1 -1
- data/Gemfile +13 -0
- data/README.md +121 -19
- data/Rakefile +2 -0
- data/docker_deploy.rb +1 -0
- data/lex-transformer.gemspec +13 -17
- data/lib/legion/extensions/transformer/actors/transform.rb +20 -14
- data/lib/legion/extensions/transformer/client.rb +64 -0
- data/lib/legion/extensions/transformer/engines/base.rb +19 -0
- data/lib/legion/extensions/transformer/engines/erb.rb +37 -0
- data/lib/legion/extensions/transformer/engines/jsonpath.rb +41 -0
- data/lib/legion/extensions/transformer/engines/liquid.rb +31 -0
- data/lib/legion/extensions/transformer/engines/llm.rb +36 -0
- data/lib/legion/extensions/transformer/engines/registry.rb +51 -0
- data/lib/legion/extensions/transformer/engines/static.rb +21 -0
- data/lib/legion/extensions/transformer/helpers/schema_validator.rb +48 -0
- data/lib/legion/extensions/transformer/runners/transform.rb +70 -52
- data/lib/legion/extensions/transformer/transport/exchanges/task.rb +10 -4
- data/lib/legion/extensions/transformer/transport/messages/message.rb +27 -17
- data/lib/legion/extensions/transformer/transport/queues/transform.rb +12 -6
- data/lib/legion/extensions/transformer/transport.rb +19 -13
- data/lib/legion/extensions/transformer/version.rb +3 -1
- data/lib/legion/extensions/transformer.rb +3 -0
- metadata +28 -76
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile.lock +0 -58
- data/bitbucket-pipelines.yml +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7f4f19cb531c6f7cb50302bb3f2aab973b55976bb05d1b870ac2ad4f7bdb9a0
|
|
4
|
+
data.tar.gz: a445f592b23aae48a5510a7c6f17ee3cf0ea0a29dd3c9d67bc405b5b208a0c72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 21096cee7c407f8df2aa5307e3fe71e35008103a798bc7f9c9cd8dfd1724728af7d65fbeeada8b5df657c412bdcfd44c60fd100efb2a3f63815c6615e63f9917
|
|
7
|
+
data.tar.gz: 4e6348dc31ae9217b34040010ecf8b613b8b0e97ae1995854a3f30c821bce8a5b6d190bb283ad9a06e305fe128b68bd8a3533772451d608c55912659c1970ec9
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
ci:
|
|
9
|
+
uses: LegionIO/.github/.github/workflows/ci.yml@main
|
|
10
|
+
|
|
11
|
+
release:
|
|
12
|
+
needs: ci
|
|
13
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
14
|
+
uses: LegionIO/.github/.github/workflows/release.yml@main
|
|
15
|
+
secrets:
|
|
16
|
+
rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -1,33 +1,49 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.4
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
|
|
1
6
|
Layout/LineLength:
|
|
2
|
-
Max:
|
|
7
|
+
Max: 160
|
|
8
|
+
Layout/SpaceAroundEqualsInParameterDefault:
|
|
9
|
+
EnforcedStyle: space
|
|
10
|
+
Layout/HashAlignment:
|
|
11
|
+
EnforcedHashRocketStyle: table
|
|
12
|
+
EnforcedColonStyle: table
|
|
13
|
+
|
|
3
14
|
Metrics/MethodLength:
|
|
4
15
|
Max: 50
|
|
5
16
|
Metrics/ClassLength:
|
|
6
17
|
Max: 1500
|
|
18
|
+
Metrics/ModuleLength:
|
|
19
|
+
Max: 1500
|
|
7
20
|
Metrics/BlockLength:
|
|
8
|
-
Max:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Metrics/CyclomaticComplexity:
|
|
12
|
-
Max: 117
|
|
21
|
+
Max: 40
|
|
22
|
+
Exclude:
|
|
23
|
+
- 'spec/**/*'
|
|
13
24
|
Metrics/AbcSize:
|
|
14
|
-
Max:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
Max: 60
|
|
26
|
+
Metrics/CyclomaticComplexity:
|
|
27
|
+
Max: 15
|
|
28
|
+
Metrics/PerceivedComplexity:
|
|
29
|
+
Max: 17
|
|
30
|
+
Metrics/ParameterLists:
|
|
31
|
+
Enabled: false
|
|
32
|
+
|
|
22
33
|
Style/Documentation:
|
|
23
34
|
Enabled: false
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
NewCops: enable
|
|
27
|
-
SuggestExtensions: false
|
|
35
|
+
Style/SymbolArray:
|
|
36
|
+
Enabled: true
|
|
28
37
|
Style/FrozenStringLiteralComment:
|
|
29
|
-
Enabled:
|
|
38
|
+
Enabled: true
|
|
39
|
+
EnforcedStyle: always
|
|
40
|
+
|
|
30
41
|
Naming/FileName:
|
|
31
42
|
Enabled: false
|
|
32
|
-
|
|
33
|
-
Enabled: false
|
|
43
|
+
Naming/PredicateMethod:
|
|
44
|
+
Enabled: false
|
|
45
|
+
Naming/PredicatePrefix:
|
|
46
|
+
Enabled: false
|
|
47
|
+
|
|
48
|
+
Gemspec/DevelopmentDependencies:
|
|
49
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.1] - 2026-03-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- LLM transform engine (`engines/llm.rb`) — provider-agnostic natural language transformation via `Legion::LLM.chat`
|
|
7
|
+
- Registered as `:llm` in engine registry (explicit only, no auto-detection)
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2026-03-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Pluggable engine system with abstract `Engines::Base` and `Engines::Registry`
|
|
13
|
+
- 4 template engines: ERB (extracted from runner), Static (JSON passthrough), Liquid, JSONPath (dot-notation extraction)
|
|
14
|
+
- `Engines::Registry.detect` auto-selects ERB or static engine based on template content
|
|
15
|
+
- `render_transformation` accepts optional `engine:` keyword to force a specific engine
|
|
16
|
+
- `Helpers::SchemaValidator` validates transformed output against declared schemas (required keys, type checks)
|
|
17
|
+
- `transform` accepts optional `schema:` parameter for post-render validation
|
|
18
|
+
- `transform_chain` method for sequential multi-step pipelines with per-step engine selection and schema validation
|
|
19
|
+
- Standalone `Client` class for framework-independent usage with `transform` and `transform_chain` methods
|
|
20
|
+
- SimpleCov coverage reporting
|
|
21
|
+
- Modern packaging: grouped test dependencies, `require_relative` in gemspec, `rubocop-rspec`
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- `dispatch_multiplied` payload mutation bug: `new_payload = payload` replaced with `new_payload = payload.dup`
|
|
25
|
+
- Spec filename typo: `tranformer_spec.rb` renamed to `transformer_spec.rb`
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- ERB rendering logic extracted from runner into `Engines::Erb` class
|
|
29
|
+
- `build_template_variables` moved to `Engines::Erb` as private `build_variables`
|
|
30
|
+
- Runner no longer directly requires `tilt` (delegated to `Engines::Erb`)
|
|
31
|
+
|
|
32
|
+
## [0.1.4] - 2026-03-13
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- Initial release
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# lex-transformer: Payload Transformation Engine for LegionIO
|
|
2
|
+
|
|
3
|
+
**Repository Level 3 Documentation**
|
|
4
|
+
- **Parent**: `/Users/miverso2/rubymine/legion/extensions-core/CLAUDE.md`
|
|
5
|
+
- **Grandparent**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
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`).
|
|
10
|
+
|
|
11
|
+
**GitHub**: https://github.com/LegionIO/lex-transformer
|
|
12
|
+
**License**: MIT
|
|
13
|
+
**Version**: 0.2.1
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Legion::Extensions::Transformer
|
|
19
|
+
├── Actors/
|
|
20
|
+
│ └── Transform # Subscription actor consuming transform requests
|
|
21
|
+
├── Runners/
|
|
22
|
+
│ └── 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
|
|
26
|
+
│ ├── dispatch_transformed # Route Hash (single) or Array (fan-out) results
|
|
27
|
+
│ ├── dispatch_multiplied # Fan-out: create a new task per array element
|
|
28
|
+
│ └── 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
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Key Files
|
|
36
|
+
|
|
37
|
+
| Path | Purpose |
|
|
38
|
+
|------|---------|
|
|
39
|
+
| `lib/legion/extensions/transformer.rb` | Entry point (`data_required? true`) |
|
|
40
|
+
| `lib/legion/extensions/transformer/runners/transform.rb` | Core transformation logic |
|
|
41
|
+
| `lib/legion/extensions/transformer/actors/transform.rb` | AMQP subscription actor |
|
|
42
|
+
| `lib/legion/extensions/transformer/transport.rb` | Transport setup |
|
|
43
|
+
|
|
44
|
+
## Template Variables Available in ERB
|
|
45
|
+
|
|
46
|
+
| Variable | Available when |
|
|
47
|
+
|----------|---------------|
|
|
48
|
+
| All payload keys | Always (splatted into template scope) |
|
|
49
|
+
| `crypt` | Template string contains `'crypt'` |
|
|
50
|
+
| `settings` | Template string contains `'settings'` |
|
|
51
|
+
| `cache` | Template string contains `'cache'` |
|
|
52
|
+
| `task` | Template string contains `'task'` and payload has `task_id` |
|
|
53
|
+
|
|
54
|
+
## Dispatch Behavior
|
|
55
|
+
|
|
56
|
+
- **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
|
|
59
|
+
|
|
60
|
+
## Dependencies
|
|
61
|
+
|
|
62
|
+
| Gem | Purpose |
|
|
63
|
+
|-----|---------|
|
|
64
|
+
| `tilt` (>= 2.3) | Template engine abstraction for ERB rendering |
|
|
65
|
+
| `legion-data` | Required - task record creation for fan-out |
|
|
66
|
+
|
|
67
|
+
## Testing
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
bundle install
|
|
71
|
+
bundle exec rspec
|
|
72
|
+
bundle exec rubocop
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Spec files: `spec/legion/extensions/tranformer_spec.rb` (note: typo in filename), `spec/legion/extensions/transform_runner_spec.rb`
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
**Maintained By**: Matthew Iverson (@Esity)
|
data/Dockerfile
CHANGED
data/Gemfile
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
source 'https://rubygems.org'
|
|
2
4
|
|
|
3
5
|
gemspec
|
|
6
|
+
|
|
7
|
+
group :test do
|
|
8
|
+
gem 'base64'
|
|
9
|
+
gem 'liquid', '~> 5.0'
|
|
10
|
+
gem 'rake'
|
|
11
|
+
gem 'rspec', '~> 3.13'
|
|
12
|
+
gem 'rspec_junit_formatter'
|
|
13
|
+
gem 'rubocop', '~> 1.75'
|
|
14
|
+
gem 'rubocop-rspec'
|
|
15
|
+
gem 'simplecov'
|
|
16
|
+
end
|
data/README.md
CHANGED
|
@@ -1,38 +1,140 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lex-transformer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Payload transformation engine for [LegionIO](https://github.com/LegionIO/LegionIO). Transforms task payloads between services in a relationship chain using pluggable template engines. Maps data from one task's output into the format expected by the next task's input.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
gem install lex-transformer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or add to your Gemfile:
|
|
8
12
|
|
|
9
13
|
```ruby
|
|
10
14
|
gem 'lex-transformer'
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
## Standalone Client
|
|
18
|
+
|
|
19
|
+
Use the transformer without the full LegionIO framework:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require 'legion/extensions/transformer/client'
|
|
23
|
+
|
|
24
|
+
client = Legion::Extensions::Transformer::Client.new
|
|
25
|
+
|
|
26
|
+
# Single transform
|
|
27
|
+
result = client.transform(
|
|
28
|
+
transformation: '{"greeting":"hello <%= name %>"}',
|
|
29
|
+
payload: { name: 'world' }
|
|
30
|
+
)
|
|
31
|
+
result[:success] # => true
|
|
32
|
+
result[:result] # => { greeting: "hello world" }
|
|
33
|
+
|
|
34
|
+
# With explicit engine
|
|
35
|
+
result = client.transform(
|
|
36
|
+
transformation: '{"greeting":"hello {{ name }}"}',
|
|
37
|
+
payload: { name: 'world' },
|
|
38
|
+
engine: :liquid
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# With schema validation
|
|
42
|
+
result = client.transform(
|
|
43
|
+
transformation: '{"name":"test"}',
|
|
44
|
+
payload: {},
|
|
45
|
+
schema: { required_keys: [:name, :email] }
|
|
46
|
+
)
|
|
47
|
+
result[:success] # => false
|
|
48
|
+
result[:status] # => "transformer.validation_failed"
|
|
49
|
+
result[:errors] # => ["missing required key: email"]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Transform Chains
|
|
53
|
+
|
|
54
|
+
Pipe data through sequential transformation steps:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
result = client.transform_chain(
|
|
58
|
+
steps: [
|
|
59
|
+
{ transformation: '{"user":"<%= login %>"}', schema: { required_keys: [:user] } },
|
|
60
|
+
{ transformation: '{"message":"Welcome, <%= user %>!"}' }
|
|
61
|
+
],
|
|
62
|
+
payload: { login: 'alice' }
|
|
63
|
+
)
|
|
64
|
+
result[:success] # => true
|
|
65
|
+
result[:result][:args] # => { message: "Welcome, alice!" }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Each step's output merges into the running payload, so subsequent steps can reference keys from earlier steps.
|
|
69
|
+
|
|
70
|
+
## Engines
|
|
71
|
+
|
|
72
|
+
| Engine | Name | Detection | Description |
|
|
73
|
+
|--------|------|-----------|-------------|
|
|
74
|
+
| ERB | `:erb` | `<%` or `%>` in template | Full ERB template rendering via `tilt` |
|
|
75
|
+
| Static | `:static` | Default (no ERB markers) | Plain JSON passthrough |
|
|
76
|
+
| Liquid | `:liquid` | Explicit only | Liquid template rendering (`{{ var }}`) |
|
|
77
|
+
| JSONPath | `:jsonpath` | Explicit only | Dot-notation value extraction from payload |
|
|
78
|
+
| LLM | `:llm` | Explicit only | Natural language transformation via Legion::LLM |
|
|
14
79
|
|
|
15
|
-
|
|
80
|
+
Auto-detection selects ERB when the template contains ERB markers, otherwise falls back to Static. Use the `engine:` parameter to force a specific engine.
|
|
16
81
|
|
|
17
|
-
|
|
82
|
+
### LLM Engine
|
|
18
83
|
|
|
19
|
-
|
|
84
|
+
The LLM engine uses natural language instructions as the "template":
|
|
20
85
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
86
|
+
```ruby
|
|
87
|
+
client.transform(
|
|
88
|
+
transformation: "Summarize this webhook payload into a Slack notification with the PR title and author",
|
|
89
|
+
payload: { action: "opened", pull_request: { title: "Fix auth", user: { login: "alice" } } },
|
|
90
|
+
engine: :llm
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Requires `legion-llm` to be started. Provider-agnostic — uses whatever LLM provider is configured (Ollama, Bedrock, Anthropic, OpenAI, Gemini).
|
|
95
|
+
|
|
96
|
+
## Schema Validation
|
|
97
|
+
|
|
98
|
+
Validate transformed output against a declared schema:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
schema = {
|
|
102
|
+
required_keys: [:name, :email], # keys that must be present
|
|
103
|
+
types: { name: String, email: String, age: Integer } # optional type checks
|
|
30
104
|
}
|
|
31
105
|
```
|
|
32
106
|
|
|
33
|
-
|
|
34
|
-
|
|
107
|
+
When validation fails, the transform returns `{ success: false, status: 'transformer.validation_failed', errors: [...] }`.
|
|
108
|
+
|
|
109
|
+
## Runners
|
|
110
|
+
|
|
111
|
+
### Transform
|
|
112
|
+
|
|
113
|
+
#### `transform(transformation:, engine: nil, schema: nil, **payload)`
|
|
114
|
+
|
|
115
|
+
Renders the transformation template against the payload, optionally validates the result, then dispatches:
|
|
116
|
+
|
|
117
|
+
- Hash result: routes to next task (`task.queued`)
|
|
118
|
+
- Array result: fan-out via `dispatch_multiplied` (one task per element, marked `task.multiplied`)
|
|
119
|
+
- Schema failure: `transformer.validation_failed`, no dispatch
|
|
120
|
+
|
|
121
|
+
#### `transform_chain(steps:, **payload)`
|
|
122
|
+
|
|
123
|
+
Sequential pipeline. Each step has `:transformation`, optional `:engine`, optional `:schema`. Output of step N feeds into step N+1. Stops on first schema failure.
|
|
124
|
+
|
|
125
|
+
## Transport
|
|
126
|
+
|
|
127
|
+
- **Exchange**: `task` (inherits from `Legion::Transport::Exchanges::Task`)
|
|
128
|
+
- **Queue**: `task.transform`
|
|
129
|
+
- **Routing keys**: `task.subtask`, `task.subtask.transform`
|
|
130
|
+
|
|
131
|
+
## Requirements
|
|
132
|
+
|
|
133
|
+
- Ruby >= 3.4
|
|
134
|
+
- [LegionIO](https://github.com/LegionIO/LegionIO) framework (for AMQP actor mode)
|
|
135
|
+
- `tilt` >= 2.3
|
|
136
|
+
- Standalone Client works without the framework
|
|
35
137
|
|
|
36
138
|
## License
|
|
37
139
|
|
|
38
|
-
|
|
140
|
+
MIT
|
data/Rakefile
CHANGED
data/docker_deploy.rb
CHANGED
data/lex-transformer.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/transformer/version'
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = 'lex-transformer'
|
|
@@ -8,27 +8,23 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ['Esity']
|
|
9
9
|
spec.email = ['matthewdiverson@gmail.com']
|
|
10
10
|
|
|
11
|
-
spec.summary = '
|
|
12
|
-
spec.description = '
|
|
13
|
-
spec.homepage = 'https://
|
|
11
|
+
spec.summary = 'Payload transformation engine for LegionIO task chains'
|
|
12
|
+
spec.description = 'Template-based payload transformation between tasks with multiple engines, schema validation, and composition'
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-transformer'
|
|
14
14
|
spec.license = 'MIT'
|
|
15
|
-
spec.required_ruby_version =
|
|
15
|
+
spec.required_ruby_version = '>= 3.4'
|
|
16
16
|
|
|
17
17
|
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
-
spec.metadata['source_code_uri'] = 'https://
|
|
19
|
-
spec.metadata['documentation_uri'] = 'https://
|
|
20
|
-
spec.metadata['changelog_uri'] = 'https://
|
|
21
|
-
spec.metadata['bug_tracker_uri'] = 'https://
|
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-transformer'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-transformer'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-transformer/blob/main/CHANGELOG.md'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-transformer/issues'
|
|
22
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
22
23
|
|
|
23
24
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
24
25
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
25
26
|
end
|
|
26
27
|
spec.require_paths = ['lib']
|
|
27
28
|
|
|
28
|
-
spec.add_dependency 'tilt'
|
|
29
|
-
|
|
30
|
-
spec.add_development_dependency 'bundler', '>= 2'
|
|
31
|
-
spec.add_development_dependency 'rake'
|
|
32
|
-
spec.add_development_dependency 'rspec'
|
|
33
|
-
spec.add_development_dependency 'rubocop'
|
|
29
|
+
spec.add_dependency 'tilt', '>= 2.3'
|
|
34
30
|
end
|
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
module Actor
|
|
3
|
-
class Transform < Legion::Extensions::Actors::Subscription
|
|
4
|
-
def runner_function
|
|
5
|
-
'transform'
|
|
6
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Transformer
|
|
6
|
+
module Actor
|
|
7
|
+
class Transform < Legion::Extensions::Actors::Subscription
|
|
8
|
+
def runner_function
|
|
9
|
+
'transform'
|
|
10
|
+
end
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
def check_subtask?
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate_task?
|
|
17
|
+
false
|
|
18
|
+
end
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
def use_runner?
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
end
|
|
18
24
|
end
|
|
19
25
|
end
|
|
20
26
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'engines/registry'
|
|
4
|
+
require_relative 'helpers/schema_validator'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Transformer
|
|
9
|
+
class Client
|
|
10
|
+
def transform(transformation:, payload:, engine: nil, schema: nil)
|
|
11
|
+
eng = resolve_engine(engine, transformation)
|
|
12
|
+
rendered = eng.render(transformation, payload)
|
|
13
|
+
rendered = parse_rendered(rendered)
|
|
14
|
+
|
|
15
|
+
if schema
|
|
16
|
+
validation = Helpers::SchemaValidator.validate(schema: schema, data: rendered)
|
|
17
|
+
return { success: false, status: 'transformer.validation_failed', errors: validation[:errors] } unless validation[:valid]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
{ success: true, result: rendered }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def transform_chain(steps:, payload:)
|
|
24
|
+
result = payload.dup
|
|
25
|
+
steps.each do |step|
|
|
26
|
+
eng = resolve_engine(step[:engine], step[:transformation])
|
|
27
|
+
rendered = eng.render(step[:transformation], result)
|
|
28
|
+
rendered = parse_rendered(rendered)
|
|
29
|
+
|
|
30
|
+
if step[:schema]
|
|
31
|
+
validation = Helpers::SchemaValidator.validate(schema: step[:schema], data: rendered)
|
|
32
|
+
return { success: false, status: 'transformer.validation_failed', errors: validation[:errors] } unless validation[:valid]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if rendered.is_a?(Hash)
|
|
36
|
+
result = result.merge({ args: rendered }.merge(rendered))
|
|
37
|
+
else
|
|
38
|
+
result[:args] = rendered
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
{ success: true, result: result }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def resolve_engine(engine_name, transformation)
|
|
47
|
+
if engine_name
|
|
48
|
+
Engines::Registry.fetch(engine_name)
|
|
49
|
+
else
|
|
50
|
+
Engines::Registry.detect(transformation)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_rendered(rendered)
|
|
55
|
+
return rendered unless rendered.is_a?(String)
|
|
56
|
+
|
|
57
|
+
Legion::JSON.load(rendered)
|
|
58
|
+
rescue StandardError
|
|
59
|
+
rendered
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Transformer
|
|
6
|
+
module Engines
|
|
7
|
+
class Base
|
|
8
|
+
def render(template, payload)
|
|
9
|
+
raise NotImplementedError, "#{self.class}#render must be implemented"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def name
|
|
13
|
+
raise NotImplementedError, "#{self.class}#name must be implemented"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tilt'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Transformer
|
|
9
|
+
module Engines
|
|
10
|
+
class Erb < Base
|
|
11
|
+
def name
|
|
12
|
+
:erb
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render(template, payload)
|
|
16
|
+
tilt_template = Tilt['erb'].new { template }
|
|
17
|
+
variables = build_variables(template, payload)
|
|
18
|
+
tilt_template.render(Object.new, variables)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def build_variables(template, payload)
|
|
24
|
+
variables = { **payload }
|
|
25
|
+
variables[:crypt] = Legion::Crypt if defined?(Legion::Crypt) && template.include?('crypt')
|
|
26
|
+
variables[:settings] = Legion::Settings if defined?(Legion::Settings) && template.include?('settings')
|
|
27
|
+
variables[:cache] = Legion::Cache if defined?(Legion::Cache) && template.include?('cache')
|
|
28
|
+
if payload.key?(:task_id) && template.include?('task') && defined?(Legion::Data::Model::Task)
|
|
29
|
+
variables[:task] = Legion::Data::Model::Task[payload[:task_id]]
|
|
30
|
+
end
|
|
31
|
+
variables
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Transformer
|
|
8
|
+
module Engines
|
|
9
|
+
class Jsonpath < Base
|
|
10
|
+
def name
|
|
11
|
+
:jsonpath
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render(expression, payload)
|
|
15
|
+
extract(expression, payload)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def extract(path, data)
|
|
21
|
+
path = path.delete_prefix('$.') if path.start_with?('$.')
|
|
22
|
+
segments = path.split('.')
|
|
23
|
+
result = data
|
|
24
|
+
segments.each do |segment|
|
|
25
|
+
case result
|
|
26
|
+
when Hash
|
|
27
|
+
key = result.key?(segment.to_sym) ? segment.to_sym : segment
|
|
28
|
+
result = result[key]
|
|
29
|
+
when Array
|
|
30
|
+
result = result[segment.to_i]
|
|
31
|
+
else
|
|
32
|
+
return nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|