lex-conditioner 0.2.5 → 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: 7d7c725a33a160829135b9b2269533484ab211ee5654951d929ce49dbfffcbbe
4
- data.tar.gz: c4b994cf4d00aac64316b4023b63fb02cb420288fc0eb310868c3ad9a49ec856
3
+ metadata.gz: 906d2b96665278b78cb003349e7d5733546ededdfd2eade4f61e270f5c778a7f
4
+ data.tar.gz: 60bbb47534fc56690960b82717dbfbb5dc76edd79465517f481f881686edde6c
5
5
  SHA512:
6
- metadata.gz: 15f92eb0f63890df974300c03c72c082228b61ad165c29da251a6955060f98bbbdb25bf80d2841f2c284a3deeb6c5e099e826747e85e4a74ce9d4fd8eca83acb
7
- data.tar.gz: 4a41130e68f0a464e48ff540fc442b91c7f2cef10aa0ad165484b43cf6895623f6df07be4ea8f856de6b865a8ba3d197a3771f4cc781e8f19608c8d6cce6413a
6
+ metadata.gz: 8477f380f05f478cd823f8acf6e435a691f303682c07d849016445770f890a55c3f5fa2f755dd9b3050d92e56dc1cd8e75d55f2c5c49039a4c87c70018337a1c
7
+ data.tar.gz: e5b6063b50c13fd4e454356a6df9afe724ceb0653868e32180ffc9e49e7e1c35bcf500fd05981c10b2a4d9a96b523141d63687186950416ed2d7b5a6b860fade
@@ -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
@@ -9,3 +9,4 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+ Gemfile.lock
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: 120
7
+ Max: 160
8
+ Layout/SpaceAroundEqualsInParameterDefault:
9
+ EnforcedStyle: space
10
+ Layout/HashAlignment:
11
+ EnforcedHashRocketStyle: table
12
+ EnforcedColonStyle: table
13
+
3
14
  Metrics/MethodLength:
4
- Max: 30
15
+ Max: 50
5
16
  Metrics/ClassLength:
6
17
  Max: 1500
18
+ Metrics/ModuleLength:
19
+ Max: 1500
7
20
  Metrics/BlockLength:
8
- Max: 50
21
+ Max: 40
22
+ Exclude:
23
+ - 'spec/**/*'
9
24
  Metrics/AbcSize:
10
- Max: 20
25
+ Max: 60
11
26
  Metrics/CyclomaticComplexity:
12
- Max: 20
27
+ Max: 15
13
28
  Metrics/PerceivedComplexity:
14
- Max: 20
15
- Layout/SpaceAroundEqualsInParameterDefault:
16
- EnforcedStyle: space
17
- Style/SymbolArray:
18
- Enabled: true
19
- Layout/HashAlignment:
20
- EnforcedHashRocketStyle: table
21
- EnforcedColonStyle: table
29
+ Max: 17
30
+ Metrics/ParameterLists:
31
+ Enabled: false
32
+
22
33
  Style/Documentation:
23
34
  Enabled: false
24
- AllCops:
25
- TargetRubyVersion: 2.5
26
- NewCops: enable
27
- SuggestExtensions: false
35
+ Style/SymbolArray:
36
+ Enabled: true
28
37
  Style/FrozenStringLiteralComment:
29
- Enabled: false
38
+ Enabled: true
39
+ EnforcedStyle: always
40
+
30
41
  Naming/FileName:
31
42
  Enabled: false
32
- Style/ClassAndModuleChildren:
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,19 @@
1
+ # Changelog
2
+
3
+ ## [0.3.0] - 2026-03-17
4
+
5
+ ### Added
6
+ - 14 new operators: `greater_than`, `less_than`, `greater_or_equal`, `less_or_equal`, `between` (numeric); `contains`, `starts_with`, `ends_with`, `matches` (string); `in_set`, `not_in_set`, `empty`, `not_empty`, `size_equal` (collection) — 23 total
7
+ - Structured condition explanation via `Condition#explain` returning per-rule results with `fact`, `operator`, `value`, `actual`, and `result`
8
+ - Standalone `Client` class for framework-independent evaluation
9
+ - SimpleCov coverage reporting
10
+ - Modern packaging: grouped test dependencies, `require_relative` in gemspec, `rubocop-rspec`
11
+
12
+ ### Fixed
13
+ - `Comparator.false?` returning `nil` instead of `false` for truthy values
14
+ - Typo `funciton_id` corrected to `function_id` in runner `send_task` whitelist
15
+
16
+ ## [0.2.5] - 2026-03-13
17
+
18
+ ### Added
19
+ - Initial release
data/CLAUDE.md ADDED
@@ -0,0 +1,89 @@
1
+ # lex-conditioner: Conditional Rule 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 applies conditional statements to tasks within relationships. Evaluates JSON-based rules against task payloads to determine whether downstream tasks should execute, enabling branching logic in task chains.
10
+
11
+ **GitHub**: https://github.com/LegionIO/lex-conditioner
12
+ **License**: MIT
13
+ **Version**: 0.3.0
14
+
15
+ ## Architecture
16
+
17
+ ```
18
+ Legion::Extensions::Conditioner
19
+ ├── Actors/
20
+ │ └── Conditioner # Subscription actor consuming condition evaluation requests
21
+ ├── Runners/
22
+ │ └── Conditioner # Executes conditional logic against task payloads
23
+ │ ├── check # Main entry point: evaluates conditions, dispatches or fails
24
+ │ └── send_task # Routes to transformation or task queue based on result
25
+ ├── Helpers/
26
+ │ ├── Condition # Condition parsing (all/any nesting, dotted-path facts)
27
+ │ └── Comparator # Comparison operator implementations (class methods)
28
+ └── Transport/
29
+ ├── Exchanges/Task # Publishes to the task exchange
30
+ ├── Queues/Conditioner # Subscribes to condition evaluation queue
31
+ └── Messages/Conditioner # Message format for condition requests
32
+ ```
33
+
34
+ ## Key Files
35
+
36
+ | Path | Purpose |
37
+ |------|---------|
38
+ | `lib/legion/extensions/conditioner.rb` | Entry point, extension registration |
39
+ | `lib/legion/extensions/conditioner/runners/conditioner.rb` | Core condition evaluation logic |
40
+ | `lib/legion/extensions/conditioner/helpers/condition.rb` | Condition parsing and all/any evaluation |
41
+ | `lib/legion/extensions/conditioner/helpers/comparator.rb` | Comparison operator implementations |
42
+ | `lib/legion/extensions/conditioner/actors/conditioner.rb` | AMQP subscription actor |
43
+
44
+ ## Condition Rule Format
45
+
46
+ Rules use `all`/`any` grouping with `fact`/`operator`/`value` entries. Facts use dot notation to address nested payload keys.
47
+
48
+ ### Operators
49
+
50
+ **Binary** (require `fact` + `value`):
51
+ - `equal` - exact equality
52
+ - `not_equal` - inequality
53
+
54
+ **Unary** (require `fact` only):
55
+ - `nil` - value is nil
56
+ - `not_nil` - value is not nil
57
+ - `is_true` - value is truthy
58
+ - `is_false` - value is falsy
59
+ - `is_string` - value is a String
60
+ - `is_array` - value is an Array
61
+ - `is_integer` - value is an Integer
62
+
63
+ ## Condition Evaluation Logic
64
+
65
+ After evaluation, the runner sets task status based on outcome:
66
+ - Condition passes + transformation present: `transformation.queued` (routes to `task.subtask.transform`)
67
+ - Condition passes + routing key present: `task.queued` (routes to `runner_routing_key`)
68
+ - Condition passes but no routing info: `task.exception` (`send_task` is still called and raises `MissingArgument`)
69
+ - Condition fails: `conditioner.failed` (`send_task` is skipped)
70
+
71
+ ## Dependencies
72
+
73
+ | Gem | Purpose |
74
+ |-----|---------|
75
+ | `legion-exceptions` | Exception handling |
76
+
77
+ ## Testing
78
+
79
+ ```bash
80
+ bundle install
81
+ bundle exec rspec
82
+ bundle exec rubocop
83
+ ```
84
+
85
+ Spec files: `spec/legion/extensions/conditioner_spec.rb`, `spec/legion/extensions/comparator_spec.rb`, `spec/legion/extensions/condition_spec.rb`, `spec/legion/extensions/conditioner/runners/conditioner_spec.rb`
86
+
87
+ ---
88
+
89
+ **Maintained By**: Matthew Iverson (@Esity)
data/Dockerfile CHANGED
@@ -5,4 +5,4 @@ RUN apk update && apk add build-base tzdata postgresql-dev mysql-client mariadb-
5
5
 
6
6
  COPY . ./
7
7
  RUN gem install lex-conditioner legion-data --no-document --no-prerelease
8
- CMD ruby $(which legionio)
8
+ CMD ruby --yjit $(which legionio)
data/Gemfile CHANGED
@@ -1,3 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
6
+
7
+ group :test do
8
+ gem 'rake'
9
+ gem 'rspec', '~> 3.13'
10
+ gem 'rspec_junit_formatter'
11
+ gem 'rubocop', '~> 1.75'
12
+ gem 'rubocop-rspec'
13
+ gem 'simplecov'
14
+ end
data/README.md CHANGED
@@ -1,38 +1,125 @@
1
- # Legion::Extensions::Conditioner
1
+ # lex-conditioner
2
2
 
3
- A core Legion Extension used to evaluate conditions for sub tasks to run
3
+ Conditional rule engine for [LegionIO](https://github.com/LegionIO/LegionIO). Evaluates JSON-based rules against task payloads to determine whether downstream tasks should execute, enabling branching logic in task chains.
4
4
 
5
5
  ## Installation
6
6
 
7
- Add this line to your application's Gemfile:
7
+ ```bash
8
+ gem install lex-conditioner
9
+ ```
10
+
11
+ Or add to your Gemfile:
8
12
 
9
13
  ```ruby
10
14
  gem 'lex-conditioner'
11
15
  ```
12
16
 
13
- And then execute:
17
+ ## Standalone Client
18
+
19
+ Use the conditioner without the full LegionIO framework:
20
+
21
+ ```ruby
22
+ require 'legion/extensions/conditioner/client'
23
+
24
+ client = Legion::Extensions::Conditioner::Client.new
25
+
26
+ result = client.evaluate(
27
+ conditions: { all: [
28
+ { fact: 'response.code', operator: 'greater_or_equal', value: 200 },
29
+ { fact: 'response.code', operator: 'less_than', value: 300 }
30
+ ] },
31
+ values: { response: { code: 200, body: 'OK' } }
32
+ )
33
+
34
+ result[:valid] # => true
35
+ result[:explanation] # => { valid: true, group: :all, rules: [...] }
36
+ ```
37
+
38
+ The explanation includes per-rule results with actual values:
14
39
 
15
- $ bundle install
40
+ ```ruby
41
+ result[:explanation][:rules]
42
+ # => [
43
+ # { fact: "response.code", operator: "greater_or_equal", value: 200, actual: 200, result: true },
44
+ # { fact: "response.code", operator: "less_than", value: 300, actual: 200, result: true }
45
+ # ]
46
+ ```
16
47
 
17
- Or install it yourself as:
48
+ ## Condition Format
18
49
 
19
- $ gem install lex-conditioner
50
+ Rules use `all`/`any` grouping with `fact`/`operator`/`value` entries. Facts use dot notation for nested keys.
20
51
 
21
- ## Adding to Legion
22
- You can manually install with a `gem install lex-http` command or by adding it into your settings with something like this
23
52
  ```json
24
53
  {
25
- "extensions": {
26
- "conditioner": {
27
- "enabled": true, "workers": 1
28
- }
29
- }
54
+ "all": [
55
+ { "fact": "response.code", "operator": "greater_or_equal", "value": 200 },
56
+ { "any": [
57
+ { "fact": "action", "operator": "equal", "value": "opened" },
58
+ { "fact": "action", "operator": "equal", "value": "reopened" }
59
+ ]}
60
+ ]
30
61
  }
31
62
  ```
32
63
 
33
- ## Usage
64
+ ## Operators
65
+
66
+ ### Binary (require `fact` + `value`)
67
+
68
+ | Operator | Description |
69
+ |----------|-------------|
70
+ | `equal` | Exact equality |
71
+ | `not_equal` | Inequality |
72
+ | `greater_than` | Numeric greater than |
73
+ | `less_than` | Numeric less than |
74
+ | `greater_or_equal` | Numeric greater than or equal |
75
+ | `less_or_equal` | Numeric less than or equal |
76
+ | `between` | Value within range (value is `[min, max]`) |
77
+ | `contains` | String contains substring |
78
+ | `starts_with` | String starts with prefix |
79
+ | `ends_with` | String ends with suffix |
80
+ | `matches` | String matches regex pattern |
81
+ | `in_set` | Value is in the given array |
82
+ | `not_in_set` | Value is not in the given array |
83
+ | `size_equal` | Collection/string size equals value |
84
+
85
+ ### Unary (require `fact` only)
86
+
87
+ | Operator | Description |
88
+ |----------|-------------|
89
+ | `nil` | Value is nil |
90
+ | `not_nil` | Value is not nil |
91
+ | `is_true` | Value is truthy |
92
+ | `is_false` | Value is falsy |
93
+ | `is_string` | Value is a String |
94
+ | `is_array` | Value is an Array |
95
+ | `is_integer` | Value is an Integer |
96
+ | `empty` | Value is nil or empty |
97
+ | `not_empty` | Value is present and not empty |
98
+
99
+ ## Runners
100
+
101
+ ### Conditioner
102
+
103
+ #### `check(conditions:, **payload)`
104
+
105
+ Evaluates conditions against the payload. Routes to transformation or task queue based on result:
106
+
107
+ - Condition passes + transformation present: routes to `task.subtask.transform`
108
+ - Condition passes + routing key present: routes to `runner_routing_key`
109
+ - Condition fails: `conditioner.failed`, no dispatch
110
+
111
+ ## Transport
112
+
113
+ - **Exchange**: `task` (inherits from `Legion::Transport::Exchanges::Task`)
114
+ - **Queue**: `task.conditioner`
115
+ - **Routing keys**: `task.subtask`, `task.subtask.conditioner`
116
+
117
+ ## Requirements
34
118
 
119
+ - Ruby >= 3.4
120
+ - [LegionIO](https://github.com/LegionIO/LegionIO) framework (for AMQP actor mode)
121
+ - Standalone Client works without the framework
35
122
 
36
123
  ## License
37
124
 
38
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
125
+ MIT
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rspec/core/rake_task'
3
5
 
data/docker_deploy.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  name = 'conditioner'
4
5
  require "./lib/legion/extensions/#{name}/version"
@@ -1,6 +1,6 @@
1
- lib = File.expand_path('lib', __dir__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require 'legion/extensions/conditioner/version'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/conditioner/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'lex-conditioner'
@@ -8,28 +8,21 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ['Esity']
9
9
  spec.email = ['matthewdiverson@gmail.com']
10
10
 
11
- spec.summary = 'LEX-Conditioner is used to apply conditional statements to tasks'
12
- spec.description = 'Runs relationship conditional statements against tasks in a relationship'
13
- spec.homepage = 'https://bitbucket.org/legion-io/lex-conditioner'
11
+ spec.summary = 'Conditional rule engine for LegionIO task chains'
12
+ spec.description = 'Evaluates JSON-based rules against task payloads with 18+ operators, structured explanations, and standalone client support'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-conditioner'
14
14
  spec.license = 'MIT'
15
- spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
15
+ spec.required_ruby_version = '>= 3.4'
16
16
 
17
17
  spec.metadata['homepage_uri'] = spec.homepage
18
- spec.metadata['source_code_uri'] = 'https://bitbucket.org/legion-io/lex-conditioner'
19
- spec.metadata['documentation_uri'] = 'https://legionio.atlassian.net/wiki/spaces/LEX/pages/614957181'
20
- spec.metadata['changelog_uri'] = 'https://legionio.atlassian.net/wiki/spaces/LEX/pages/612270436'
21
- spec.metadata['bug_tracker_uri'] = 'https://bitbucket.org/legion-io/lex-conditioner/issues'
18
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-conditioner'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-conditioner'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-conditioner/blob/main/CHANGELOG.md'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-conditioner/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
- spec.add_development_dependency 'legionio'
29
- spec.add_development_dependency 'rake'
30
- spec.add_development_dependency 'rspec'
31
- spec.add_development_dependency 'rubocop'
32
- spec.add_development_dependency 'simplecov'
33
-
34
- spec.add_dependency 'legion-exceptions'
35
28
  end
@@ -1,16 +1,22 @@
1
- module Legion::Extensions::Conditioner
2
- module Actor
3
- class Conditioner < Legion::Extensions::Actors::Subscription
4
- def runner_function
5
- 'check'
6
- end
1
+ # frozen_string_literal: true
7
2
 
8
- def check_subtask?
9
- false
10
- end
3
+ module Legion
4
+ module Extensions
5
+ module Conditioner
6
+ module Actor
7
+ class Conditioner < Legion::Extensions::Actors::Subscription
8
+ def runner_function
9
+ 'check'
10
+ end
11
+
12
+ def check_subtask?
13
+ false
14
+ end
11
15
 
12
- def generate_task?
13
- false
16
+ def generate_task?
17
+ false
18
+ end
19
+ end
14
20
  end
15
21
  end
16
22
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helpers/condition'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Conditioner
8
+ class Client
9
+ def evaluate(conditions:, values:)
10
+ conditions_json = conditions.is_a?(String) ? conditions : Legion::JSON.dump(conditions)
11
+ condition = Condition.new(conditions: conditions_json, values: values)
12
+ {
13
+ valid: condition.valid?,
14
+ explanation: condition.explain
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Legion
2
4
  module Extensions
3
5
  module Conditioner
@@ -18,25 +20,84 @@ module Legion
18
20
  !values[fact].nil?
19
21
  end
20
22
 
21
- def self.is_false?(fact, values) # rubocop:disable Naming/PredicateName
22
- true unless values[fact]
23
+ def self.false?(fact, values)
24
+ values[fact] ? false : true
23
25
  end
24
26
 
25
- def self.is_true?(fact, values) # rubocop:disable Naming/PredicateName
27
+ def self.true?(fact, values)
26
28
  values[fact]
27
29
  end
28
30
 
29
- def self.is_array?(fact, values) # rubocop:disable Naming/PredicateName
30
- !values[fact]
31
+ def self.array?(fact, values)
32
+ values[fact].is_a? Array
31
33
  end
32
34
 
33
- def self.is_string?(fact, values) # rubocop:disable Naming/PredicateName
35
+ def self.string?(fact, values)
34
36
  values[fact].is_a? String
35
37
  end
36
38
 
37
- def self.is_integer?(fact, values) # rubocop:disable Naming/PredicateName
39
+ def self.integer?(fact, values)
38
40
  values[fact].is_a? Integer
39
41
  end
42
+
43
+ def self.greater_than?(fact, value, values)
44
+ values[fact] > value
45
+ end
46
+
47
+ def self.less_than?(fact, value, values)
48
+ values[fact] < value
49
+ end
50
+
51
+ def self.greater_or_equal?(fact, value, values)
52
+ values[fact] >= value
53
+ end
54
+
55
+ def self.less_or_equal?(fact, value, values)
56
+ values[fact] <= value
57
+ end
58
+
59
+ def self.between?(fact, value, values)
60
+ values[fact].between?(value[0], value[1])
61
+ end
62
+
63
+ def self.contains?(fact, value, values)
64
+ values[fact].to_s.include?(value.to_s)
65
+ end
66
+
67
+ def self.starts_with?(fact, value, values)
68
+ values[fact].to_s.start_with?(value.to_s)
69
+ end
70
+
71
+ def self.ends_with?(fact, value, values)
72
+ values[fact].to_s.end_with?(value.to_s)
73
+ end
74
+
75
+ def self.matches?(fact, value, values)
76
+ Regexp.new(value).match?(values[fact].to_s)
77
+ end
78
+
79
+ def self.in_set?(fact, value, values)
80
+ Array(value).include?(values[fact])
81
+ end
82
+
83
+ def self.not_in_set?(fact, value, values)
84
+ !Array(value).include?(values[fact])
85
+ end
86
+
87
+ def self.empty?(fact, values)
88
+ val = values[fact]
89
+ val.nil? || (val.respond_to?(:empty?) && val.empty?)
90
+ end
91
+
92
+ def self.not_empty?(fact, values)
93
+ val = values[fact]
94
+ !val.nil? && !(val.respond_to?(:empty?) && val.empty?)
95
+ end
96
+
97
+ def self.size_equal?(fact, value, values)
98
+ val = values[fact]
99
+ val.respond_to?(:size) && val.size == value
100
+ end
40
101
  end
41
102
  end
42
103
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'comparator'
2
4
 
3
5
  module Legion
@@ -33,32 +35,19 @@ module Legion
33
35
  raise Legion::Exception::WrongType::Hash, @conditions.class unless @conditions.is_a? Hash
34
36
  end
35
37
 
36
- def validate_test(conditions = @conditions) # rubocop:disable Metrics/AbcSize
38
+ BINARY_OPS = %w[equal not_equal greater_than less_than greater_or_equal less_or_equal between contains starts_with ends_with matches in_set not_in_set
39
+ size_equal].freeze
40
+ UNARY_OPS = %w[nil not_nil is_false is_true is_string is_array is_integer empty not_empty].freeze
41
+ UNARY_METHOD_MAP = {
42
+ 'is_false' => :false?, 'is_true' => :true?,
43
+ 'is_string' => :string?, 'is_array' => :array?, 'is_integer' => :integer?,
44
+ 'empty' => :empty?, 'not_empty' => :not_empty?
45
+ }.freeze
46
+
47
+ def validate_test(conditions = @conditions)
37
48
  conditions.each do |condition|
38
49
  condition[1].each do |rule|
39
- result = validate_test('conditions' => { 'all' => rule[:all] }) if rule.include? :all
40
- result = validate_test('conditions' => { 'any' => rule[:any] }) if rule.include? :any
41
- case rule[:operator]
42
- when 'equal'
43
- result = Legion::Extensions::Conditioner::Comparator.equal?(rule[:fact], rule[:value], @values)
44
- when 'not_equal'
45
- result = Legion::Extensions::Conditioner::Comparator.not_equal?(rule[:fact], rule[:value], @values)
46
- when 'nil'
47
- result = Legion::Extensions::Conditioner::Comparator.nil?(rule[:fact], @values)
48
- when 'not_nil'
49
- result = Legion::Extensions::Conditioner::Comparator.not_nil?(rule[:fact], @values)
50
- when 'is_false'
51
- result = Legion::Extensions::Conditioner::Comparator.is_false?(rule[:fact], @values)
52
- when 'is_true'
53
- result = Legion::Extensions::Conditioner::Comparator.is_true?(rule[:fact], @values)
54
- when 'is_string'
55
- result = Legion::Extensions::Conditioner::Comparator.is_string?(rule[:fact], @values)
56
- when 'is_array'
57
- result = Legion::Extensions::Conditioner::Comparator.is_array?(rule[:fact], @values)
58
- when 'is_integer'
59
- result = Legion::Extensions::Conditioner::Comparator.is_integer?(rule[:fact], @values)
60
- end
61
-
50
+ result = evaluate_rule(rule)
62
51
  return true if condition[0] == :any && result == true
63
52
  return false if condition[0] == :all && result == false
64
53
  end
@@ -67,10 +56,56 @@ module Legion
67
56
  end
68
57
  end
69
58
 
59
+ def evaluate_rule(rule)
60
+ return validate_test(all: rule[:all]) if rule.include?(:all)
61
+ return validate_test(any: rule[:any]) if rule.include?(:any)
62
+
63
+ comp = Legion::Extensions::Conditioner::Comparator
64
+ op = rule[:operator]
65
+
66
+ if BINARY_OPS.include?(op)
67
+ comp.send(:"#{op}?", rule[:fact], rule[:value], @values)
68
+ elsif UNARY_OPS.include?(op)
69
+ method_name = UNARY_METHOD_MAP[op] || :"#{op}?"
70
+ comp.send(method_name, rule[:fact], @values)
71
+ end
72
+ end
73
+
70
74
  def valid?
71
75
  @valid = validate_test if @valid.nil?
72
76
  @valid
73
77
  end
78
+
79
+ def explain_test(conditions = @conditions)
80
+ group = conditions.keys.first
81
+ rules = conditions[group].map do |rule|
82
+ explain_rule(rule)
83
+ end
84
+
85
+ valid = group == :all ? rules.all? { |r| r[:result] } : rules.any? { |r| r[:result] }
86
+ { valid: valid, group: group, rules: rules }
87
+ end
88
+
89
+ def explain_rule(rule)
90
+ if rule.include?(:all)
91
+ result = explain_test(all: rule[:all])
92
+ return { group: :all, rules: result[:rules], result: result[:valid] }
93
+ end
94
+ if rule.include?(:any)
95
+ result = explain_test(any: rule[:any])
96
+ return { group: :any, rules: result[:rules], result: result[:valid] }
97
+ end
98
+
99
+ result = evaluate_rule(rule)
100
+ explanation = { fact: rule[:fact], operator: rule[:operator], result: result }
101
+ explanation[:value] = rule[:value] if rule.key?(:value)
102
+ explanation[:actual] = @values[rule[:fact]]
103
+ explanation
104
+ end
105
+
106
+ def explain
107
+ @explain ||= explain_test
108
+ end
74
109
  end
75
110
  end
76
111
  end
@@ -1,61 +1,67 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'legion/extensions/conditioner/helpers/condition'
2
4
 
3
- module Legion::Extensions::Conditioner
4
- module Runners
5
+ module Legion
6
+ module Extensions
5
7
  module Conditioner
6
- def check(conditions:, **payload) # rubocop:disable Metrics/AbcSize
7
- conditioner = Legion::Extensions::Conditioner::Condition.new(conditions: conditions,
8
- values: payload,
9
- type: payload[:type])
10
-
11
- status = if conditioner.valid? && payload.key?(:transformation)
12
- 'transformation.queued'
13
- elsif conditioner.valid? && payload.key?(:runner_routing_key)
14
- 'task.queued'
15
- elsif conditioner.valid?
16
- 'task.exception'
17
- else
18
- 'conditioner.failed'
19
- end
20
-
21
- send_task(**payload) unless status == 'conditioner.failed'
22
- task_update(payload[:task_id], status, **payload) unless payload[:task_id].nil?
23
-
24
- if payload[:debug] && payload.key?(:task_id)
25
- generate_task_log(task_id: payload[:task_id],
26
- function: 'check',
27
- valid: conditioner.valid?,
28
- conditions: payload[:conditions],
29
- values: payload)
30
- end
8
+ module Runners
9
+ module Conditioner
10
+ def check(conditions:, **payload)
11
+ conditioner = Legion::Extensions::Conditioner::Condition.new(conditions: conditions,
12
+ values: payload,
13
+ type: payload[:type])
31
14
 
32
- { success: true, valid: conditioner.valid? }
33
- rescue StandardError => e
34
- Legion::Logging.error 'LEX::Conditioner::Runners::Condition had an exception'
35
- Legion::Logging.warn e.message
36
- Legion::Logging.warn e.backtrace
37
- task_update(payload[:task_id], 'conditioner.exception', **payload) unless payload[:task_id].nil?
38
- end
15
+ status = if conditioner.valid? && payload.key?(:transformation)
16
+ 'transformation.queued'
17
+ elsif conditioner.valid? && payload.key?(:runner_routing_key)
18
+ 'task.queued'
19
+ elsif conditioner.valid?
20
+ 'task.exception'
21
+ else
22
+ 'conditioner.failed'
23
+ end
39
24
 
40
- def send_task(**opts)
41
- subtask_hash = {}
42
- %i[runner_routing_key relationship_id chain_id trigger_runner_id trigger_function_id funciton_id function runner_id runner_class transformation debug task_id results].each do |column| # rubocop:disable Layout/LineLength
43
- subtask_hash[column] = opts[column] if opts.key? column
44
- end
25
+ send_task(**payload) unless status == 'conditioner.failed'
26
+ task_update(payload[:task_id], status, **payload) unless payload[:task_id].nil?
45
27
 
46
- subtask_hash[:routing_key] = if subtask_hash.key? :transformation
47
- 'task.subtask.transform'
48
- elsif subtask_hash.key? :runner_routing_key
49
- subtask_hash[:runner_routing_key]
50
- end
28
+ if payload[:debug] && payload.key?(:task_id)
29
+ generate_task_log(task_id: payload[:task_id],
30
+ function: 'check',
31
+ valid: conditioner.valid?,
32
+ conditions: payload[:conditions],
33
+ values: payload)
34
+ end
51
35
 
52
- raise Legion::Exception::MissingArgument 'Missing :routing_key' unless subtask_hash.key? :routing_key
36
+ { success: true, valid: conditioner.valid? }
37
+ rescue StandardError => e
38
+ Legion::Logging.error 'LEX::Conditioner::Runners::Condition had an exception'
39
+ Legion::Logging.warn e.message
40
+ Legion::Logging.warn e.backtrace
41
+ task_update(payload[:task_id], 'conditioner.exception', **payload) unless payload[:task_id].nil?
42
+ end
53
43
 
54
- Legion::Transport::Messages::SubTask.new(**subtask_hash).publish
55
- end
44
+ def send_task(**opts)
45
+ subtask_hash = {}
46
+ %i[runner_routing_key relationship_id chain_id trigger_runner_id trigger_function_id function_id function runner_id runner_class transformation debug task_id results].each do |column| # rubocop:disable Layout/LineLength
47
+ subtask_hash[column] = opts[column] if opts.key? column
48
+ end
56
49
 
57
- include Legion::Extensions::Helpers::Lex
58
- include Legion::Extensions::Helpers::Task
50
+ subtask_hash[:routing_key] = if subtask_hash.key? :transformation
51
+ 'task.subtask.transform'
52
+ elsif subtask_hash.key? :runner_routing_key
53
+ subtask_hash[:runner_routing_key]
54
+ end
55
+
56
+ raise Legion::Exception::MissingArgument 'Missing :routing_key' unless subtask_hash.key? :routing_key
57
+
58
+ Legion::Transport::Messages::SubTask.new(**subtask_hash).publish
59
+ end
60
+
61
+ include Legion::Extensions::Helpers::Lex
62
+ include Legion::Extensions::Helpers::Task
63
+ end
64
+ end
59
65
  end
60
66
  end
61
67
  end
@@ -1,7 +1,13 @@
1
- module Legion::Extensions::Conditioner
2
- module Transport
3
- module Exchanges
4
- class Conditioner < Legion::Transport::Exchanges::Task
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Conditioner
6
+ module Transport
7
+ module Exchanges
8
+ class Conditioner < Legion::Transport::Exchanges::Task
9
+ end
10
+ end
5
11
  end
6
12
  end
7
13
  end
@@ -1,13 +1,19 @@
1
- module Legion::Extensions::Conditioner
2
- module Transport
3
- module Messages
4
- class Conditioner < Legion::Transport::Message
5
- def routing_key
6
- 'task.conditioner.succeeded'
7
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Conditioner
6
+ module Transport
7
+ module Messages
8
+ class Conditioner < Legion::Transport::Message
9
+ def routing_key
10
+ 'task.conditioner.succeeded'
11
+ end
8
12
 
9
- def exchange
10
- Legion::Transport::Exchanges::Task
13
+ def exchange
14
+ Legion::Transport::Exchanges::Task
15
+ end
16
+ end
11
17
  end
12
18
  end
13
19
  end
@@ -1,9 +1,15 @@
1
- module Legion::Extensions::Conditioner
2
- module Transport
3
- module Queues
4
- class Conditioner < Legion::Transport::Queue
5
- def queue_name
6
- 'task.conditioner'
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Conditioner
6
+ module Transport
7
+ module Queues
8
+ class Conditioner < Legion::Transport::Queue
9
+ def queue_name
10
+ 'task.conditioner'
11
+ end
12
+ end
7
13
  end
8
14
  end
9
15
  end
@@ -1,13 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'legion/extensions/transport'
2
4
 
3
- module Legion::Extensions::Conditioner
4
- module Transport
5
- extend Legion::Extensions::Transport
6
- def self.additional_e_to_q
7
- [
8
- { from: 'task', to: 'conditioner', routing_key: 'task.subtask' },
9
- { from: 'task', to: 'conditioner', routing_key: 'task.subtask.conditioner' }
10
- ]
5
+ module Legion
6
+ module Extensions
7
+ module Conditioner
8
+ module Transport
9
+ extend Legion::Extensions::Transport
10
+
11
+ def self.additional_e_to_q
12
+ [
13
+ { from: 'task', to: 'conditioner', routing_key: 'task.subtask' },
14
+ { from: 'task', to: 'conditioner', routing_key: 'task.subtask.conditioner' }
15
+ ]
16
+ end
17
+ end
11
18
  end
12
19
  end
13
20
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Legion
2
4
  module Extensions
3
5
  module Conditioner
4
- VERSION = '0.2.5'.freeze
6
+ VERSION = '0.3.0'
5
7
  end
6
8
  end
7
9
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'legion/extensions/conditioner/version'
4
+ require_relative 'conditioner/client'
2
5
 
3
6
  module Legion
4
7
  module Extensions
metadata CHANGED
@@ -1,120 +1,38 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-conditioner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-03-03 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: legionio
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: rubocop
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: simplecov
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: legion-exceptions
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :runtime
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- description: Runs relationship conditional statements against tasks in a relationship
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Evaluates JSON-based rules against task payloads with 18+ operators,
13
+ structured explanations, and standalone client support
98
14
  email:
99
15
  - matthewdiverson@gmail.com
100
16
  executables: []
101
17
  extensions: []
102
18
  extra_rdoc_files: []
103
19
  files:
20
+ - ".github/workflows/ci.yml"
104
21
  - ".gitignore"
105
22
  - ".rspec"
106
23
  - ".rubocop.yml"
24
+ - CHANGELOG.md
25
+ - CLAUDE.md
107
26
  - Dockerfile
108
27
  - Gemfile
109
- - Gemfile.lock
110
28
  - LICENSE.txt
111
29
  - README.md
112
30
  - Rakefile
113
- - bitbucket-pipelines.yml
114
31
  - docker_deploy.rb
115
32
  - lex-conditioner.gemspec
116
33
  - lib/legion/extensions/conditioner.rb
117
34
  - lib/legion/extensions/conditioner/actors/conditioner.rb
35
+ - lib/legion/extensions/conditioner/client.rb
118
36
  - lib/legion/extensions/conditioner/helpers/comparator.rb
119
37
  - lib/legion/extensions/conditioner/helpers/condition.rb
120
38
  - lib/legion/extensions/conditioner/runners/conditioner.rb
@@ -123,16 +41,16 @@ files:
123
41
  - lib/legion/extensions/conditioner/transport/messages/conditioner.rb
124
42
  - lib/legion/extensions/conditioner/transport/queues/conditioner.rb
125
43
  - lib/legion/extensions/conditioner/version.rb
126
- homepage: https://bitbucket.org/legion-io/lex-conditioner
44
+ homepage: https://github.com/LegionIO/lex-conditioner
127
45
  licenses:
128
46
  - MIT
129
47
  metadata:
130
- homepage_uri: https://bitbucket.org/legion-io/lex-conditioner
131
- source_code_uri: https://bitbucket.org/legion-io/lex-conditioner
132
- documentation_uri: https://legionio.atlassian.net/wiki/spaces/LEX/pages/614957181
133
- changelog_uri: https://legionio.atlassian.net/wiki/spaces/LEX/pages/612270436
134
- bug_tracker_uri: https://bitbucket.org/legion-io/lex-conditioner/issues
135
- post_install_message:
48
+ homepage_uri: https://github.com/LegionIO/lex-conditioner
49
+ source_code_uri: https://github.com/LegionIO/lex-conditioner
50
+ documentation_uri: https://github.com/LegionIO/lex-conditioner
51
+ changelog_uri: https://github.com/LegionIO/lex-conditioner/blob/main/CHANGELOG.md
52
+ bug_tracker_uri: https://github.com/LegionIO/lex-conditioner/issues
53
+ rubygems_mfa_required: 'true'
136
54
  rdoc_options: []
137
55
  require_paths:
138
56
  - lib
@@ -140,15 +58,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
140
58
  requirements:
141
59
  - - ">="
142
60
  - !ruby/object:Gem::Version
143
- version: 2.5.0
61
+ version: '3.4'
144
62
  required_rubygems_version: !ruby/object:Gem::Requirement
145
63
  requirements:
146
64
  - - ">="
147
65
  - !ruby/object:Gem::Version
148
66
  version: '0'
149
67
  requirements: []
150
- rubygems_version: 3.1.4
151
- signing_key:
68
+ rubygems_version: 3.6.9
152
69
  specification_version: 4
153
- summary: LEX-Conditioner is used to apply conditional statements to tasks
70
+ summary: Conditional rule engine for LegionIO task chains
154
71
  test_files: []
data/Gemfile.lock DELETED
@@ -1,119 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- lex-conditioner (0.2.5)
5
- legion-exceptions
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- amq-protocol (2.3.2)
11
- ast (2.4.2)
12
- aws-eventstream (1.1.0)
13
- aws-sigv4 (1.2.2)
14
- aws-eventstream (~> 1, >= 1.0.2)
15
- bunny (2.17.0)
16
- amq-protocol (~> 2.3, >= 2.3.1)
17
- concurrent-ruby (1.1.8)
18
- concurrent-ruby-ext (1.1.8)
19
- concurrent-ruby (= 1.1.8)
20
- connection_pool (2.2.3)
21
- daemons (1.3.1)
22
- dalli (2.7.11)
23
- diff-lcs (1.4.4)
24
- docile (1.3.5)
25
- json_pure (2.5.1)
26
- legion-cache (1.1.1)
27
- connection_pool (>= 2.2.3)
28
- dalli (>= 2.7)
29
- redis (>= 4.2)
30
- legion-crypt (0.3.0)
31
- vault (>= 0.15.0)
32
- legion-exceptions (1.1.5)
33
- legion-json (1.1.4)
34
- json_pure
35
- legion-exceptions (>= 1.1.5)
36
- multi_json
37
- legion-logging (1.1.4)
38
- rainbow (~> 3)
39
- legion-settings (1.1.3)
40
- legion-json
41
- legion-logging
42
- legion-transport (1.1.9)
43
- bunny (>= 2.17.0)
44
- concurrent-ruby (>= 1.1.7)
45
- legion-json
46
- legionio (0.4.3)
47
- concurrent-ruby (>= 1.1.7)
48
- concurrent-ruby-ext (>= 1.1.7)
49
- daemons (>= 1.3.1)
50
- legion-cache
51
- legion-crypt (>= 0.2.0)
52
- legion-exceptions
53
- legion-json
54
- legion-logging
55
- legion-settings
56
- legion-transport (>= 1.1.9)
57
- lex-node
58
- oj (>= 3.10)
59
- thor (>= 1)
60
- lex-node (0.1.7)
61
- multi_json (1.15.0)
62
- oj (3.11.2)
63
- parallel (1.20.1)
64
- parser (3.0.0.0)
65
- ast (~> 2.4.1)
66
- rainbow (3.0.0)
67
- rake (13.0.3)
68
- redis (4.2.5)
69
- regexp_parser (2.1.1)
70
- rexml (3.2.4)
71
- rspec (3.10.0)
72
- rspec-core (~> 3.10.0)
73
- rspec-expectations (~> 3.10.0)
74
- rspec-mocks (~> 3.10.0)
75
- rspec-core (3.10.1)
76
- rspec-support (~> 3.10.0)
77
- rspec-expectations (3.10.1)
78
- diff-lcs (>= 1.2.0, < 2.0)
79
- rspec-support (~> 3.10.0)
80
- rspec-mocks (3.10.2)
81
- diff-lcs (>= 1.2.0, < 2.0)
82
- rspec-support (~> 3.10.0)
83
- rspec-support (3.10.2)
84
- rubocop (1.11.0)
85
- parallel (~> 1.10)
86
- parser (>= 3.0.0.0)
87
- rainbow (>= 2.2.2, < 4.0)
88
- regexp_parser (>= 1.8, < 3.0)
89
- rexml
90
- rubocop-ast (>= 1.2.0, < 2.0)
91
- ruby-progressbar (~> 1.7)
92
- unicode-display_width (>= 1.4.0, < 3.0)
93
- rubocop-ast (1.4.1)
94
- parser (>= 2.7.1.5)
95
- ruby-progressbar (1.11.0)
96
- simplecov (0.21.2)
97
- docile (~> 1.1)
98
- simplecov-html (~> 0.11)
99
- simplecov_json_formatter (~> 0.1)
100
- simplecov-html (0.12.3)
101
- simplecov_json_formatter (0.1.2)
102
- thor (1.1.0)
103
- unicode-display_width (2.0.0)
104
- vault (0.15.0)
105
- aws-sigv4
106
-
107
- PLATFORMS
108
- ruby
109
-
110
- DEPENDENCIES
111
- legionio
112
- lex-conditioner!
113
- rake
114
- rspec
115
- rubocop
116
- simplecov
117
-
118
- BUNDLED WITH
119
- 2.2.6
@@ -1,19 +0,0 @@
1
- image: ruby:2.7
2
-
3
- pipelines:
4
- tags:
5
- "v*":
6
- - step:
7
- name: Push to RubyGems
8
- deployment: RubyGems
9
- script:
10
- - gem install gem-release
11
- - (umask 077 ; echo $gem_creds | base64 --decode > ~/.gem/credentials)
12
- - gem release
13
- artifacts:
14
- - pkg/**
15
- - step:
16
- name: Push to Docker
17
- deployment: Docker
18
- script:
19
- - './docker_deploy.rb'