├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGES.md ├── Gemfile ├── README.md ├── Rakefile ├── lib └── trailblazer │ ├── operation.rb │ └── operation │ ├── public_call.rb │ ├── railway.rb │ ├── result.rb │ ├── testing.rb │ ├── version.rb │ └── wtf.rb ├── test ├── activity_interface_test.rb ├── benchmark │ └── skill_resolver_benchmark.rb ├── docs │ ├── autogenerated │ │ ├── activity_basics_test.rb │ │ ├── composable_variable_mapping_test.rb │ │ ├── fast_track_layout_test.rb │ │ ├── mechanics_test.rb │ │ ├── sequence_options_test.rb │ │ ├── subprocess_test.rb │ │ └── wiring_api_test.rb │ ├── developer_test.rb │ ├── operation_test.rb │ └── step_dsl_test.rb ├── operation_test.rb ├── result_test.rb ├── test_helper.rb └── trace_test.rb └── trailblazer-operation.gemspec /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | ruby: [2.5, 2.6, 2.7, '3.0', '3.1', '3.2', "3.3", "head", 'jruby'] 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: ruby/setup-ruby@v1 13 | with: 14 | ruby-version: ${{ matrix.ruby }} 15 | bundler-cache: true 16 | - run: bundle exec rake 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | lib/bundler/man 11 | pkg 12 | rdoc 13 | spec/reports 14 | test/tmp 15 | test/version_tmp 16 | tmp 17 | /**/*.log 18 | .rubocop-*yml 19 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | * Signature changed to Operation.call(options, &block). We removed the second positional argument `flow_options`. 4 | * Removed `:invoke_class` in public_call 5 | * Removed `Result#slice` and `Result#inspect`. Renamed `Result#to_hash` to `#to_h`. 6 | * Removed `Result#event` deprecation. 7 | 8 | ## 0.11.0 9 | 10 | * Introduce `Operation.call_with_public_interface_from_call` which merges `ctx` and `circuit_options` 11 | accordingly, so the overrider of `Operation.call_with_public_interface` gets correct args. 12 | * Removing `Operation.trace`, you've been warned! Use `Operation.wtf?`. This also removes `Result#wtf`. 13 | See [https://github.com/trailblazer/trailblazer-operation/blob/3f821c7d576e7ccccf580fbd8c9305501fdc5d2c/test/trace_test.rb#L22](this sample test case) 14 | if you need a more low-level interface to tracing. 15 | * No need to pass `:exec_context` in `#call_with_public_interface`. This is done in `Strategy.call`. 16 | * Rename `Result#event` to `Result#terminus` for consistency. Deprecate `Result#event`. 17 | 18 | ## 0.10.1 19 | 20 | * Deprecate `Operation.trace` and `Result#wtf?` in favor of `Operation.wtf?`. 21 | * Use `trailblazer-developer-0.1.0`. 22 | 23 | ## 0.10.0 24 | 25 | * Require `trailblazer-activity-dsl-linear-1.2.0`. 26 | * Remove `Railway::Macaroni`. 27 | * Remove `Operation::Callable`. 28 | * Introduce `Operation::INITIAL_WRAP_STATIC` that is computed once at compile-time, not 29 | with every `#call`. 30 | 31 | ## 0.9.0 32 | 33 | * Use `trailblazer-activity-dsl-linear` 1.1.0. 34 | * Pass `:container_activity` to `TaskWrap.invoke` instead of the superseded `:static_wrap` option. 35 | 36 | ## 0.8.0 37 | 38 | * Use `trailblazer-activity-dsl-linear` 1.0.0. 39 | 40 | ## 0.7.5 41 | 42 | * Upgrade `trailblazer-activity` & `trailblazer-activity-dsl-linear` patch versions. 43 | 44 | ## 0.7.4 45 | 46 | * Fix `Operation.call` being called twice before delegating to `call_with_circuit_interface`. This is done via a special `call_task` in the operation's taskWrap. 47 | 48 | ## 0.7.3 49 | 50 | * Revert trailblazer-developer to a runtime dependency. 51 | 52 | ## 0.7.2 53 | 54 | * Bugfix: when calling `Operation.call(params: {}, "current_user" => user)` the stringified variables got lost in Ruby < 3. 55 | 56 | ## 0.7.1 57 | 58 | * In `Operation.call_with_public_interface`, pass `self` and not `@activity` to the `invoke`r. This fixes tracing as it now catches the actual Operation class, not an activity instance. 59 | 60 | ## 0.7.0 61 | 62 | * Compatible with Ruby 2.4-3.0. 63 | * Add `Operation.wtf?`. 64 | * Add `Operation.call_with_flow_options` to allow using explicit aliasing in Ruby < 3.0. 65 | 66 | ## 0.6.6 67 | 68 | * Rename `Operation.flow_options` to `Operation.flow_options_for_public_call`. 69 | * Operations can also accept `flow_options` at run-time now :beers:, giving them precedence over `Operation.flow_options_for_public_call`. 70 | 71 | ## 0.6.5 72 | 73 | * Upgrade `trailblazer-activity` & `trailblazer-activity-dsl-linear` versions to utilise new `trailblazer-context` :drum: 74 | 75 | ## 0.6.4 76 | 77 | * Remove container support. Containers should be part of `ctx` itself 78 | 79 | ## 0.6.3 80 | 81 | * Require forwardable module from standard lib. 82 | 83 | ## 0.6.2 84 | 85 | * Fix Trace so it works with Ruby <= 2.4 86 | 87 | ## 0.6.1 88 | 89 | * Reintroduce `ClassDependencies` by leveraging `State.fields`. 90 | 91 | ## 0.6.0 92 | 93 | * Require newest `activity` gem. 94 | 95 | ## 0.5.3 96 | 97 | * New `context` API. 98 | 99 | ## 0.5.2 100 | 101 | * Use `trailblazer-activity-dsl-linear-0.1.6.` 102 | 103 | ## 0.5.1 104 | 105 | * Remove Inspect. this now sits in the `developer` gem as `Developer.railway`. 106 | 107 | ## 0.5.0 108 | 109 | * Minimal API around `Activity::FastTrack` to support the old public call style. 110 | 111 | 112 | ## 0.4.1 113 | 114 | * Use `activity-0.7.1`. 115 | 116 | ## 0.4.0 117 | 118 | * Use `activity-0.7.0`. 119 | 120 | ## 0.3.1 121 | 122 | * Moved `VariableMapping` to the `activity` gem. 123 | 124 | ## 0.3.0 125 | 126 | * Use `activity` 0.6.0. 127 | * Remove `Operation::__call__` in favor of one `call` that dispatches to either 128 | * `call_with_public_interface` this implements the complicated public `Operation.()` semantic and will be faded out with the rise of workflow engines. 129 | * `call_with_circuit_interface` is the circuit-compatible version that will be invoked on nested operations. 130 | 131 | This might seem a bit "magical" but simplifies the interface a lot. In better languages, you could use method overloading for that, in Ruby, we have to 132 | do that ourselves. This decision was made with the deprecation of `Operation.()` in mind. In the future, operations will mostly be invoked from 133 | workflow engines and not directly, where the engine takes care of applying the correct interface. 134 | 135 | ## 0.2.5 136 | 137 | * Minor fixes for activity 0.5.2. 138 | 139 | ## 0.2.4 140 | 141 | * Use `Activity::FastTrack` signals. 142 | 143 | ## 0.2.2 144 | 145 | * Use `activity-0.4.2`. 146 | 147 | ## 0.2.1 148 | 149 | * Use `activity-0.4.1`. 150 | 151 | ## 0.2.0 152 | 153 | * Cleanly separate `Activity` and `Operation` responsibilities. An operation is nothing more but a class around an activity, hosting instance methods and implementing inheritance. 154 | 155 | ## 0.1.4 156 | 157 | * `TaskWrap.arguments_for_call` now returns the correct `circuit_options` where the `:runner` etc.'s already merged. 158 | 159 | ## 0.1.3 160 | 161 | * New taskWrap API for `activity` 0.3.2. 162 | 163 | ## 0.1.2 164 | 165 | * Add @mensfeld's "Macaroni" step style for a keyword-only signature for steps. 166 | 167 | ## 0.1.0 168 | 169 | inspect: failure is << and success is >> 170 | 171 | call vs __call__: it's now designed to be run in a composition where the skills stuff is done only once, and the reslt object is not necessary 172 | 173 | FastTrack optional 174 | Wrapped optional 175 | 176 | * Add `pass` and `fail` as four-character aliases for `success` and `failure`. 177 | * Remove `Uber::Callable` requirement and treat all non-`:symbol` steps as callable objects. 178 | * Remove non-kw options for steps. All steps receive keyword args now: 179 | 180 | ```ruby 181 | def model(options) 182 | ``` 183 | 184 | now must have a minimal signature as follows. 185 | 186 | ```ruby 187 | def model(options, **) 188 | ``` 189 | * Remove `Operation#[]` and `Operation#[]=`. Please only change state in `options`. 190 | * API change for `step Macro()`: the macro's return value is now called with the low-level "Task API" signature `(direction, options, flow_options)`. You need to return `[direction, options, flow_options]`. There's a soft-deprecation warning. 191 | * Remove support for Ruby 1.9.3 for now. This can be re-introduced on demand. 192 | * Remove `pipetree` in favor of [`trailblazer-circuit`](https://github.com/trailblazer/trailblazer-circuit). This allows rich workflows and state machines in an operation. 193 | * Remove `uber` dependency. 194 | 195 | ## 0.0.13 196 | 197 | * Rename `Operation::New` to `:Instantiate` to avoid name clashes with `New` operations in applications. 198 | * Fix Ruby > 2.3.3's `Forwardable` issue. 199 | 200 | ## 0.0.12 201 | 202 | * Allow passing tmp options into `KW::Option` that will be merged with `options` and then transformed into kw args, but only locally for the step scope (or wherever you do `Option.()`). The API: 203 | 204 | ```ruby 205 | Option::KW.(proc).(input, options, some: "more", ...) 206 | ``` 207 | Note that `KW::Option` could be massively sped up with simple optimizations. 208 | 209 | ## 0.0.11 210 | 211 | * Use `Forwardable` instead of `Uber::Delegates`. 212 | 213 | ## 0.0.10 214 | 215 | * `Flow` is now `Railway`. 216 | * Any `Right` subclass will now be interpreted as success. 217 | * Add `fail!`, `fail_fast!`, `pass!`, and `pass_fast!`. 218 | * The only semi-public method to modify the pipe is `Railway#add` 219 | * Removed `&`, `>`, `<` and `%` "operators" in favor of `#add`. 220 | * Extremely simplified the macro API. Macros now return a callable step with the interface `->(input, options)` and their pipe options, e.g. `[ step, name: "my.macro"]`. 221 | 222 | ## 0.0.9 223 | 224 | Removing `Operation::consider`, which is now `step`. 225 | We now have three methods, only. 226 | 227 | * `step` import macro or add step with the & operator, meaning its result is always evaluated and 228 | decides about left or right. 229 | * `success` always adds to right track. 230 | * `failure` always adds to left track. 231 | 232 | This was heavily inspired by a discussion with @dnd, so, thanks! 🍻 233 | 234 | ## 0.0.8 235 | 236 | * Introduce a new keyword signature for steps: 237 | 238 | ```ruby 239 | step ->(options, params:, **) { options["x"] = params[:id] } 240 | ``` 241 | 242 | The same API works for instance methods and `Callable`s. 243 | 244 | Note that the implementation of `Option` and `Skills#to_hash` are improveable, but work just fine for now. 245 | 246 | 247 | ## 0.0.7 248 | 249 | * Simplify inheritance by basically removing it. 250 | 251 | ## 0.0.6 252 | 253 | * Improvements with the pipe DSL. 254 | 255 | ## 0.0.5 256 | 257 | * `_insert` provides better API now. 258 | 259 | ## 0.0.4 260 | 261 | * Don't pass the operation into `Result`, but the `Skill` options hash, only. 262 | 263 | ## 0.0.3 264 | 265 | * Add `#inspect(slices)`. 266 | 267 | ## 0.0.2 268 | 269 | * Works. 270 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in trailblazer.gemspec 4 | gemspec 5 | 6 | # gem "trailblazer-developer", path: "../trailblazer-developer" 7 | # gem "trailblazer-activity", path: "../trailblazer-activity" 8 | # gem "trailblazer-invoke", path: "../trailblazer-invoke" 9 | gem "trailblazer-invoke", github: "trailblazer/trailblazer-invoke" 10 | # gem "trailblazer-core-utils", path: "../trailblazer-core-utils" 11 | # gem "trailblazer-activity-dsl-linear", path: "../trailblazer-activity-dsl-linear" 12 | 13 | # gem "trailblazer-activity", github: "trailblazer/trailblazer-activity" 14 | # gem "trailblazer-developer", github: "trailblazer/trailblazer-developer" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trailblazer-operation 2 | 3 | _Trailblazer's Operation implementation._ 4 | 5 | ## Overview 6 | 7 | An operation is a pattern from the Trailblazer architecture. It implements a public function such as "create user" or "archive blog post". Internally, an operation is simply a generic _activity_ that uses an existing DSL to help you creating the operation's flow. 8 | 9 | An operation is identical to an activity with two additions. 10 | 11 | * A public `call` method with a simplified signature `Create.call(params: params, current_user: @user)` 12 | * It produces a `Result` object with the popular `success?` API. 13 | 14 | An operation can be used exaclty like an activity, including nesting, tracing, etc. 15 | 16 | ## Copyright 17 | 18 | Copyright (c) 2016-2020 Nick Sutterer 19 | 20 | `trailblazer-operation` is released under the [MIT License](http://www.opensource.org/licenses/MIT). 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |test| 5 | test.libs << "test" 6 | test.verbose = true 7 | test.test_files = FileList["test/**/*_test.rb"] - FileList["test/isolated/**"] 8 | end 9 | 10 | task :default => %i[test] 11 | 12 | Rake::TestTask.new(:test_configuration) do |test| 13 | test.libs << "test" 14 | test.verbose = true 15 | test.test_files = FileList["test/isolated/**/*_test.rb"] 16 | end 17 | -------------------------------------------------------------------------------- /lib/trailblazer/operation.rb: -------------------------------------------------------------------------------- 1 | require "trailblazer/operation/version" 2 | require "trailblazer/activity/dsl/linear" 3 | require "trailblazer/invoke" 4 | require "forwardable" 5 | 6 | # 7 | # Developer's docs: https://trailblazer.to/2.1/docs/internals.html#internals-operation 8 | # 9 | module Trailblazer 10 | # As opposed to {Activity::Railway} and {Activity::FastTrack} an operation 11 | # maintains different terminus subclasses. 12 | # DISCUSS: remove this, at some point in time! 13 | class Activity 14 | class Railway 15 | module End 16 | # @private 17 | class Success < Activity::End; end 18 | class Failure < Activity::End; end 19 | 20 | class FailFast < Failure; end 21 | class PassFast < Success; end 22 | end 23 | end 24 | 25 | module Operation 26 | def self.OptionsForState() 27 | { 28 | end_task: Activity::Railway::End::Success.new(semantic: :success), 29 | failure_end: Activity::Railway::End::Failure.new(semantic: :failure), 30 | fail_fast_end: Activity::Railway::End::FailFast.new(semantic: :fail_fast), 31 | pass_fast_end: Activity::Railway::End::PassFast.new(semantic: :pass_fast), 32 | } 33 | end 34 | end 35 | end 36 | 37 | def self.Operation(options) 38 | Class.new(Activity::FastTrack( Activity::Operation.OptionsForState.merge(options) )) do 39 | extend Operation::PublicCall 40 | raise # FIXME: what is the matter with you? 41 | end 42 | end 43 | 44 | # The Trailblazer-style operation. 45 | # Note that you don't have to use our "opinionated" version with result object, etc. 46 | class Operation < Activity::FastTrack(**Activity::Operation.OptionsForState) 47 | class << self 48 | alias_method :strategy_call, :call 49 | end 50 | 51 | def self.configure!(&block) 52 | Trailblazer::Invoke.module!(self.singleton_class, &block) # => Operation.__() as a canonical invoke. 53 | self 54 | end 55 | 56 | require "trailblazer/operation/public_call" 57 | extend PublicCall # Operation.call that exposes a switch for two different interfaces. 58 | 59 | require "trailblazer/operation/wtf" 60 | extend Wtf # Operation.trace 61 | end 62 | end 63 | 64 | require "trailblazer/operation/result" 65 | require "trailblazer/operation/railway" 66 | 67 | Trailblazer::Operation.configure! { {} } # create a default Operation.() with no dynamic args set. 68 | 69 | =begin 70 | Trailblazer::Operation.instance_variable_get(:@state).update!(:fields) do |fields| 71 | # Override Activity's initial taskWrap. 72 | # This way, an OP is never called using `call`, always via `#strategy_call` (even the top in Invoke). 73 | 74 | # Even though this is much cleaner, "problem" with this approach is that 75 | # nested OPs won't have {Operation.call} invoked anymore, which breaks some users' tests 76 | # especially those with expectations on nested OPs being {call}ed. 77 | fields.merge( 78 | task_wrap: Trailblazer::Operation::PublicCall::INITIAL_TASK_WRAP # HERE, we can add other tw steps like dependeny injection. 79 | ) 80 | end 81 | =end 82 | -------------------------------------------------------------------------------- /lib/trailblazer/operation/public_call.rb: -------------------------------------------------------------------------------- 1 | module Trailblazer 2 | module Operation::PublicCall 3 | # TODO: add docs from original {Operation.call}. 4 | def call(options = {}, flow_options = {}, **circuit_options, &block) 5 | return strategy_call(options, **circuit_options) if options.is_a?(Array) # This is kind of a hack that could be well hidden if Ruby had method overloading. Goal is to simplify the call thing as we're fading out Operation::public_call anyway. 6 | 7 | # DISCUSS: move to separate method? 8 | # normalize options: 9 | options = options.merge(circuit_options) # when using Op.call(params:, ...), {circuit_options} will always be ctx variables. 10 | 11 | invoke_with_public_interface(options, &block) 12 | end 13 | 14 | def invoke_with_public_interface(options, **options_for_invoke, &block) 15 | # On the top level, use {#__}. 16 | options_for_invoke = {matcher_context: block.binding.receiver}.merge(options_for_invoke) if block # DISCUSS: do we always want that? 17 | 18 | options_for_invoke = options_for_invoke.merge( 19 | extensions: [ 20 | NORMALIZER_TASK_WRAP_EXTENSIONS_FOR_PUBLIC_CALL_TASK 21 | ] 22 | ) 23 | 24 | signal, (ctx, flow_options) = self.__(self, options, **options_for_invoke, &block) # Operation.__ is defined via {trailblazer-invoke}. It's the "canonical invoke". 25 | 26 | Operation::Railway::Result(signal, ctx, flow_options) 27 | end 28 | 29 | # NOTE: mostly copied from {Activity::TaskWrap.call_task}. 30 | # 31 | # This TaskWrap step replaces the default {call_task} step for this very operation. 32 | # Instead of invoking the operation using {Operation.call}, it does {Operation.call_with_circuit_interface}, 33 | # so we don't invoke {Operation.call} twice. 34 | # 35 | # @private 36 | def self.call_operation_with_circuit_interface(wrap_ctx, original_args) 37 | operation = wrap_ctx[:task] 38 | 39 | original_arguments, original_circuit_options = original_args 40 | 41 | # Call the actual operation, but directly using {#strategy_call} using the circuit-interface. 42 | return_signal, return_args = operation.strategy_call(original_arguments, **original_circuit_options) 43 | 44 | # DISCUSS: do we want original_args here to be passed on, or the "effective" return_args which are different to original_args now? 45 | wrap_ctx = wrap_ctx.merge(return_signal: return_signal, return_args: return_args) 46 | 47 | return wrap_ctx, original_args 48 | end 49 | 50 | # Replace the TaskWrap's {call_task} step with our step that doesn't do {Create.call} but {Create.strategy_call}. 51 | NORMALIZER_TASK_WRAP_EXTENSIONS_FOR_PUBLIC_CALL_TASK = Activity::TaskWrap::Extension( 52 | [ 53 | method(:call_operation_with_circuit_interface), 54 | id: "task_wrap.call_task", 55 | replace: "task_wrap.call_task" 56 | ] 57 | ) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/trailblazer/operation/railway.rb: -------------------------------------------------------------------------------- 1 | module Trailblazer 2 | # Operations is simply a thin API to define, inherit and run circuits by passing the options object. 3 | # It encourages the linear railway style (http://trb.to/gems/workflow/circuit.html#operation) but can 4 | # easily be extend for more complex workflows. 5 | class Operation 6 | # End event: All subclasses of End:::Success are interpreted as "success". 7 | module Railway 8 | def self.fail! ; Activity::Left end 9 | def self.pass! ; Activity::Right end 10 | def self.fail_fast!; Activity::FastTrack::FailFast end 11 | def self.pass_fast!; Activity::FastTrack::PassFast end 12 | # @param options Context 13 | # @param terminus The last emitted signal in a circuit is the end event/terminus. 14 | def self.Result(terminus, options, *) 15 | Result.new(terminus.kind_of?(End::Success), options, terminus) 16 | end 17 | 18 | # The Railway::Result knows about its binary state, the context (data), and 19 | # the reached terminus of the circuit. 20 | class Result < Result # Operation::Result 21 | def initialize(success, data, terminus) 22 | super(success, data) 23 | 24 | @terminus = terminus 25 | end 26 | 27 | attr_reader :terminus 28 | 29 | # TODO: add {#to_h}. 30 | end 31 | 32 | module End 33 | Success = Activity::Railway::End::Success 34 | Failure = Activity::Railway::End::Failure 35 | end 36 | end # Railway 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/trailblazer/operation/result.rb: -------------------------------------------------------------------------------- 1 | class Trailblazer::Operation 2 | class Result 3 | # @param success Boolean validity of the result object 4 | # @param data Context 5 | def initialize(success, data) 6 | @success, @data = success, data 7 | end 8 | 9 | def success? 10 | @success 11 | end 12 | 13 | def failure? 14 | !success? 15 | end 16 | 17 | extend Forwardable 18 | def_delegators :@data, :[], :to_h, :keys # DISCUSS: make it a real delegator? see Nested. 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/trailblazer/operation/testing.rb: -------------------------------------------------------------------------------- 1 | module Trailblazer 2 | class Operation 3 | module Testing 4 | module Assertions 5 | # TODO: test me! 6 | def assert_call(operation, terminus: :success, seq: "[]", expected_ctx_variables: {}, **ctx_variables) 7 | result = operation.(seq: [], **ctx_variables) 8 | 9 | signal = result.terminus 10 | ctx = result.to_h 11 | 12 | assert_call_for(signal, ctx, terminus: terminus, seq: seq, **expected_ctx_variables, **ctx_variables) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/trailblazer/operation/version.rb: -------------------------------------------------------------------------------- 1 | module Trailblazer 2 | module Version 3 | module Operation 4 | VERSION = "0.11.0" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/trailblazer/operation/wtf.rb: -------------------------------------------------------------------------------- 1 | require "trailblazer/developer" 2 | 3 | module Trailblazer 4 | class Operation 5 | module Wtf 6 | def wtf?(options) 7 | invoke_with_public_interface(options, invoke_method: Trailblazer::Developer::Wtf.method(:invoke)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/activity_interface_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ActivityInterfaceTest < Minitest::Spec 4 | require "trailblazer/operation/testing" 5 | include Trailblazer::Operation::Testing::Assertions 6 | 7 | let (:operation) do 8 | Class.new(Trailblazer::Operation) do 9 | step :model, Output(:failure) => End(:not_found) 10 | step :validate 11 | step :save 12 | 13 | include T.def_steps(:model, :validate, :save) 14 | end 15 | end 16 | 17 | it "exposes the step DSL" do 18 | assert_call operation, seq: "[:model, :validate, :save]" 19 | assert_call operation, seq: "[:model]", model: false, terminus: :not_found 20 | assert_call operation, seq: "[:model, :validate]", validate: false, terminus: :failure 21 | end 22 | 23 | it "we can nested operations" do 24 | nested = self.operation 25 | 26 | operation = Class.new(Trailblazer::Operation) do 27 | step Subprocess(nested), Output(:not_found) => End(:fail_fast) 28 | step :persist 29 | 30 | include T.def_steps(:persist) 31 | end 32 | 33 | assert_call operation, seq: "[:model, :validate, :save, :persist]" 34 | assert_call operation, seq: "[:model]", model: false, terminus: :fail_fast 35 | end 36 | 37 | it "exposes the circuit-interface via {Operation.call}" do 38 | signal, (ctx, _) = operation.([{model: false, seq: []}, {}]) 39 | 40 | assert_equal signal.to_h[:semantic], :not_found 41 | assert_equal ctx[:seq].inspect, "[:model]" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/benchmark/skill_resolver_benchmark.rb: -------------------------------------------------------------------------------- 1 | require "trailblazer/operation" 2 | require "benchmark/ips" 3 | 4 | initialize_hash = {} 5 | 10.times do |i| 6 | initialize_hash["bla_#{i}"] = i 7 | end 8 | 9 | normal_container = {} 10 | 50.times do |i| 11 | normal_container["xbla_#{i}"] = i 12 | end 13 | 14 | Benchmark.ips do |x| 15 | x.report(:merge) do 16 | attrs = normal_container.merge(initialize_hash) 17 | 10.times do |_i| 18 | attrs["bla_8"] 19 | end 20 | 10.times do |_i| 21 | attrs["xbla_1"] 22 | end 23 | end 24 | 25 | x.report(:resolver) do 26 | attrs = Trailblazer::Skill::Resolver.new(initialize_hash, normal_container) 27 | 28 | 10.times do |_i| 29 | attrs["bla_8"] 30 | end 31 | 10.times do |_i| 32 | attrs["xbla_1"] 33 | end 34 | end 35 | end 36 | 37 | # Warming up -------------------------------------- 38 | # merge 3.974k i/100ms 39 | # resolver 6.593k i/100ms 40 | # Calculating ------------------------------------- 41 | # merge 39.678k (± 9.1%) i/s - 198.700k in 5.056653s 42 | # resolver 68.928k (± 6.4%) i/s - 342.836k in 5.001610s 43 | -------------------------------------------------------------------------------- /test/docs/autogenerated/activity_basics_test.rb: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM trailblazer-activity-dsl-linear/test/docs/activity_basics_test.rb 2 | require "test_helper" 3 | 4 | module X 5 | class DocsActivityTest < Minitest::Spec 6 | it "basic activity" do 7 | Memo = Struct.new(:options) do 8 | def save 9 | true 10 | end 11 | end 12 | 13 | #:memo-create 14 | module Memo::Operation 15 | class Create < Trailblazer::Operation 16 | step :validate 17 | #~body 18 | step :save 19 | left :handle_errors 20 | step :notify 21 | #~meths 22 | include T.def_steps(:validate, :save, :handle_errors, :notify) 23 | 24 | #:save 25 | def save(ctx, params:, **) 26 | memo = Memo.new(params[:memo]) 27 | memo.save 28 | 29 | ctx[:model] = memo # you can write to the {ctx}. 30 | end 31 | #:save end 32 | 33 | def notify(ctx, **) 34 | true 35 | end 36 | 37 | #~body end 38 | def validate(ctx, params:, **) # home-made validation 39 | params.key?(:memo) && 40 | params[:memo].key?(:text) && 41 | params[:memo][:text].size > 9 42 | # return value matters! 43 | end 44 | #~meths end 45 | end 46 | end 47 | #:memo-create end 48 | 49 | #:memo-call 50 | result = Memo::Operation::Create.( 51 | params: {memo: {text: "Do not forget!"}} 52 | ) 53 | 54 | result.success? # => true 55 | puts result.terminus.to_h[:semantic] #=> :success 56 | #:memo-call end 57 | 58 | #:memo-call-model 59 | result = Memo::Operation::Create.( 60 | params: {memo: {text: "Do not forget!"}} 61 | ) 62 | 63 | #~ctx_to_result 64 | puts result[:model] #=> # 65 | #:memo-call-model end 66 | #~ctx_to_result end 67 | 68 | assert_equal result.terminus.inspect, %(#) 69 | #~ignore end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/docs/autogenerated/composable_variable_mapping_test.rb: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM trailblazer-activity-dsl-linear/test/docs/composable_variable_mapping_test.rb 2 | require "test_helper" 3 | 4 | class ComposableVariableMappingDocTest < Minitest::Spec 5 | class ApplicationPolicy 6 | def self.can?(model, user, mode) 7 | decision = !user.nil? 8 | Struct.new(:allowed?).new(decision) 9 | end 10 | end 11 | 12 | module Steps 13 | def create_model(ctx, **) 14 | ctx[:model] = Object 15 | end 16 | end 17 | 18 | module A 19 | #:policy 20 | module Policy 21 | # Explicit policy, not ideal as it results in a lot of code. 22 | class Create 23 | def self.call(ctx, model:, user:, **) 24 | decision = ApplicationPolicy.can?(model, user, :create) # FIXME: how does pundit/cancan do this exactly? 25 | #~decision 26 | 27 | if decision.allowed? 28 | return true 29 | else 30 | ctx[:status] = 422 # we're not interested in this field. 31 | ctx[:message] = "Command {create} not allowed!" 32 | return false 33 | end 34 | #~decision end 35 | end 36 | end 37 | end 38 | #:policy end 39 | end 40 | end 41 | 42 | #@ 0.1 No In() 43 | class CVNoInTest < Minitest::Spec 44 | Memo = Module.new 45 | Policy = ComposableVariableMappingDocTest::A::Policy 46 | 47 | #:no-in 48 | module Memo::Operation 49 | class Create < Trailblazer::Operation 50 | step :create_model 51 | step Policy::Create # an imaginary policy step. 52 | #~meths 53 | include ComposableVariableMappingDocTest::Steps 54 | #~meths end 55 | end 56 | end 57 | #:no-in end 58 | 59 | it "why do we need In() ? because we get an exception" do 60 | exception = assert_raises ArgumentError do 61 | #:no-in-invoke 62 | result = Memo::Operation::Create.(current_user: Module) 63 | 64 | #=> ArgumentError: missing keyword: :user 65 | #:no-in-invoke end 66 | end 67 | 68 | assert_equal exception.message, "missing keyword: #{Trailblazer::Core.symbol_inspect_for(:user)}" 69 | end 70 | end 71 | 72 | #@ In() 1.1 {:model => :model} 73 | class CVInMappingHashTest < Minitest::Spec 74 | Policy = ComposableVariableMappingDocTest::A::Policy 75 | Memo = Module.new 76 | 77 | module A 78 | #:in-mapping 79 | class Create < Trailblazer::Operation 80 | step :create_model 81 | step Policy::Create, 82 | In() => { 83 | :current_user => :user, # rename {:current_user} to {:user} 84 | :model => :model # add {:model} to the inner ctx. 85 | } 86 | #~meths 87 | include ComposableVariableMappingDocTest::Steps 88 | #~meths end 89 | end 90 | #:in-mapping end 91 | 92 | end # A 93 | 94 | it "why do we need In() ?" do 95 | assert_invoke A::Create, current_user: Module, expected_ctx_variables: {model: Object} 96 | end 97 | 98 | #:in-mapping-keys 99 | module Memo::Operation 100 | class Create < Trailblazer::Operation 101 | step :create_model 102 | step :show_ctx, 103 | In() => { 104 | :current_user => :user, # rename {:current_user} to {:user} 105 | :model => :model # add {:model} to the inner ctx. 106 | } 107 | 108 | def show_ctx(ctx, **) 109 | p ctx.to_h 110 | #=> {:user=>#, :model=>#} 111 | end 112 | #~meths 113 | include ComposableVariableMappingDocTest::Steps 114 | #~meths end 115 | end 116 | end 117 | #:in-mapping-keys end 118 | 119 | it "In() is only locally visible" do 120 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object} 121 | end 122 | end 123 | 124 | # In() 1.2 125 | class CVInLimitTest < Minitest::Spec 126 | Policy = ComposableVariableMappingDocTest::A::Policy 127 | Memo = Module.new 128 | 129 | #:in-limit 130 | module Memo::Operation 131 | class Create < Trailblazer::Operation 132 | step :create_model 133 | step Policy::Create, 134 | In() => {:current_user => :user}, 135 | In() => [:model] 136 | #~meths 137 | include ComposableVariableMappingDocTest::Steps 138 | #~meths end 139 | end 140 | end 141 | #:in-limit end 142 | 143 | it "In() can map and limit" do 144 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object} 145 | end 146 | 147 | it "Policy breach will add {ctx[:message]} and {:status}" do 148 | assert_invoke Memo::Operation::Create, current_user: nil, terminus: :failure, expected_ctx_variables: {model: Object, status: 422, message: "Command {create} not allowed!"} 149 | end 150 | end 151 | 152 | # In() 1.3 (callable) 153 | class CVInCallableTest < Minitest::Spec 154 | Policy = ComposableVariableMappingDocTest::A::Policy 155 | Memo = Module.new 156 | 157 | #:in-callable 158 | module Memo::Operation 159 | class Create < Trailblazer::Operation 160 | step :create_model 161 | step Policy::Create, 162 | In() => ->(ctx, **) do 163 | # only rename {:current_user} if it's there. 164 | ctx[:current_user].nil? ? {} : {user: ctx[:current_user]} 165 | end, 166 | In() => [:model] 167 | #~meths 168 | include ComposableVariableMappingDocTest::Steps 169 | #~meths end 170 | end 171 | end 172 | #:in-callable end 173 | 174 | it "In() can map and limit" do 175 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object} 176 | end 177 | 178 | it "exception because we don't pass {:current_user}" do 179 | exception = assert_raises ArgumentError do 180 | result = Memo::Operation::Create.({}) # no {:current_user} 181 | end 182 | 183 | assert_equal exception.message, "missing keyword: #{Trailblazer::Core.symbol_inspect_for(:user)}" 184 | end 185 | end 186 | 187 | # In() 1.4 (filter method) 188 | class CVInMethodTest < Minitest::Spec 189 | Policy = ComposableVariableMappingDocTest::A::Policy 190 | Memo = Module.new 191 | 192 | #:in-method 193 | module Memo::Operation 194 | class Create < Trailblazer::Operation 195 | step :create_model 196 | step Policy::Create, 197 | In() => :input_for_policy, # You can use an {:instance_method}! 198 | In() => [:model] 199 | 200 | def input_for_policy(ctx, **) 201 | # only rename {:current_user} if it's there. 202 | ctx[:current_user].nil? ? {} : {user: ctx[:current_user]} 203 | end 204 | #~meths 205 | include ComposableVariableMappingDocTest::Steps 206 | #~meths end 207 | end 208 | end 209 | #:in-method end 210 | 211 | it { assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object} } 212 | end 213 | 214 | # In() 1.5 (callable with kwargs) 215 | class CVInKwargsTest < Minitest::Spec 216 | Policy = ComposableVariableMappingDocTest::A::Policy 217 | Memo = Module.new 218 | 219 | #:in-kwargs 220 | module Memo::Operation 221 | class Create < Trailblazer::Operation 222 | step :create_model 223 | step Policy::Create, 224 | # vvvvvvvvvvvv keyword arguments rock! 225 | In() => ->(ctx, current_user: nil, **) do 226 | current_user.nil? ? {} : {user: current_user} 227 | end, 228 | In() => [:model] 229 | #~meths 230 | include ComposableVariableMappingDocTest::Steps 231 | #~meths end 232 | end 233 | end 234 | #:in-kwargs end 235 | 236 | it { assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object} } 237 | end 238 | 239 | # Out() 1.1 240 | class CVOutTest < Minitest::Spec 241 | Policy = ComposableVariableMappingDocTest::A::Policy 242 | Memo = Module.new 243 | 244 | #:out-array 245 | module Memo::Operation 246 | class Create < Trailblazer::Operation 247 | step :create_model 248 | step Policy::Create, 249 | In() => {:current_user => :user}, 250 | In() => [:model], 251 | Out() => [:message] 252 | #~meths 253 | include ComposableVariableMappingDocTest::Steps 254 | #~meths end 255 | end 256 | end 257 | #:out-array end 258 | 259 | it "Out() can limit" do 260 | #= policy didn't set any message 261 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object, message: nil} 262 | #= policy breach, {message_from_policy} set. 263 | assert_invoke Memo::Operation::Create, current_user: nil, terminus: :failure, expected_ctx_variables: {model: Object, message: "Command {create} not allowed!"} 264 | end 265 | 266 | end 267 | 268 | # Out() 1.2 269 | class CVOutHashTest < Minitest::Spec 270 | Policy = ComposableVariableMappingDocTest::A::Policy 271 | Memo = Module.new 272 | 273 | #:out-hash 274 | module Memo::Operation 275 | class Create < Trailblazer::Operation 276 | step :create_model 277 | step Policy::Create, 278 | In() => {:current_user => :user}, 279 | In() => [:model], 280 | Out() => {:message => :message_from_policy} 281 | #~meths 282 | include ComposableVariableMappingDocTest::Steps 283 | #~meths end 284 | end 285 | end 286 | #:out-hash end 287 | 288 | it "Out() can map" do 289 | #= policy didn't set any message 290 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object, message_from_policy: nil} 291 | #= policy breach, {message_from_policy} set. 292 | assert_invoke Memo::Operation::Create, current_user: nil, terminus: :failure, expected_ctx_variables: {model: Object, message_from_policy: "Command {create} not allowed!"} 293 | end 294 | end 295 | 296 | # Out() 1.3 297 | class CVOutCallableTest < Minitest::Spec 298 | Policy = ComposableVariableMappingDocTest::A::Policy 299 | Memo = Module.new 300 | 301 | # Message = Struct.new(:data) 302 | #:out-callable 303 | module Memo::Operation 304 | class Create < Trailblazer::Operation 305 | step :create_model 306 | step Policy::Create, 307 | In() => {:current_user => :user}, 308 | In() => [:model], 309 | Out() => ->(ctx, **) do 310 | return {} unless ctx[:message] 311 | 312 | { # you always have to return a hash from a callable! 313 | :message_from_policy => ctx[:message] 314 | } 315 | end 316 | #~meths 317 | include ComposableVariableMappingDocTest::Steps 318 | #~meths end 319 | end 320 | end 321 | #:out-callable end 322 | 323 | it "Out() can map with callable" do 324 | #= policy didn't set any message 325 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object} 326 | #= policy breach, {message_from_policy} set. 327 | assert_invoke Memo::Operation::Create, current_user: nil, terminus: :failure, expected_ctx_variables: {model: Object, message_from_policy: "Command {create} not allowed!"} 328 | end 329 | end 330 | 331 | # Out() 1.4 332 | class CVOutKwTest < Minitest::Spec 333 | Policy = ComposableVariableMappingDocTest::A::Policy 334 | Memo = Module.new 335 | 336 | #:out-kw 337 | module Memo::Operation 338 | class Create < Trailblazer::Operation 339 | step :create_model 340 | step Policy::Create, 341 | In() => {:current_user => :user}, 342 | In() => [:model], 343 | Out() => ->(ctx, message: nil, **) do 344 | return {} if message.nil? 345 | 346 | { # you always have to return a hash from a callable! 347 | :message_from_policy => message 348 | } 349 | end 350 | #~meths 351 | include ComposableVariableMappingDocTest::Steps 352 | #~meths end 353 | end 354 | end 355 | #:out-kw end 356 | 357 | it "Out() can map with callable" do 358 | #= policy didn't set any message 359 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object} 360 | #= policy breach, {message_from_policy} set. 361 | assert_invoke Memo::Operation::Create, current_user: nil, terminus: :failure, expected_ctx_variables: {model: Object, message_from_policy: "Command {create} not allowed!"} 362 | end 363 | end 364 | 365 | # Out() 1.6 366 | class CVOutOuterTest < Minitest::Spec 367 | Policy = ComposableVariableMappingDocTest::A::Policy 368 | Memo = Module.new 369 | 370 | #:out-outer 371 | module Memo::Operation 372 | class Create < Trailblazer::Operation 373 | step :create_model 374 | step Policy::Create, 375 | In() => {:current_user => :user}, 376 | In() => [:model], 377 | Out() => [:message], 378 | 379 | Out(with_outer_ctx: true) => ->(inner_ctx, outer_ctx:, **) do 380 | { 381 | errors: outer_ctx[:errors].merge(policy_message: inner_ctx[:message]) 382 | } 383 | end 384 | #~meths 385 | include ComposableVariableMappingDocTest::Steps 386 | #~meths end 387 | end 388 | end 389 | #:out-outer end 390 | 391 | it "Out() with {outer_ctx}" do 392 | #= policy didn't set any message 393 | assert_invoke Memo::Operation::Create, current_user: Module, errors: {}, expected_ctx_variables: {:errors=>{:policy_message=>nil}, model: Object, message: nil} 394 | #= policy breach, {message_from_policy} set. 395 | assert_invoke Memo::Operation::Create, current_user: nil, errors: {}, terminus: :failure, expected_ctx_variables: {:errors=>{:policy_message=>"Command {create} not allowed!"}, :model=>Object, :message=>"Command {create} not allowed!"} 396 | end 397 | end 398 | 399 | # Macro 1.0 400 | class CVMacroTest < Minitest::Spec 401 | Policy = ComposableVariableMappingDocTest::A::Policy 402 | Memo = Module.new 403 | 404 | #:macro 405 | module Policy 406 | def self.Create() 407 | { 408 | task: Policy::Create, 409 | wrap_task: true, 410 | Trailblazer::Activity::Railway.In() => {:current_user => :user}, 411 | Trailblazer::Activity::Railway.In() => [:model], 412 | Trailblazer::Activity::Railway.Out() => {:message => :message_from_policy}, 413 | } 414 | end 415 | end 416 | #:macro end 417 | 418 | #:macro-use 419 | module Memo::Operation 420 | class Create < Trailblazer::Operation 421 | step :create_model 422 | step Policy::Create() 423 | #~meths 424 | include ComposableVariableMappingDocTest::Steps 425 | #~meths end 426 | end 427 | end 428 | #:macro-use end 429 | 430 | it "Out() with {outer_ctx}" do 431 | #= policy didn't set any message 432 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object, message_from_policy: nil} 433 | #= policy breach, {message_from_policy} set. 434 | assert_invoke Memo::Operation::Create, current_user: nil, terminus: :failure, expected_ctx_variables: {model: Object, :message_from_policy=>"Command {create} not allowed!"} 435 | end 436 | end 437 | 438 | # Macro 1.1 439 | class CVMacroMergeTest < Minitest::Spec 440 | Policy = CVMacroTest::Policy 441 | Memo = Module.new 442 | 443 | #:macro-merge 444 | module Memo::Operation 445 | class Create < Trailblazer::Operation 446 | step :create_model 447 | step Policy::Create(), 448 | Out() => {:message => :copied_message} # user options! 449 | #~meths 450 | include ComposableVariableMappingDocTest::Steps 451 | #~meths end 452 | end 453 | end 454 | #:macro-merge end 455 | 456 | it do 457 | #= policy didn't set any message 458 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object, message_from_policy: nil, :copied_message=>nil} 459 | #= policy breach, {message_from_policy} set. 460 | assert_invoke Memo::Operation::Create, current_user: nil, terminus: :failure, expected_ctx_variables: {model: Object, :message_from_policy=>"Command {create} not allowed!", :copied_message=>"Command {create} not allowed!"} 461 | end 462 | end 463 | 464 | # Inheritance 1.0 465 | class CVInheritanceTest < Minitest::Spec 466 | Policy = CVMacroTest::Policy 467 | Memo = Module.new 468 | 469 | #:inheritance-base 470 | module Memo::Operation 471 | class Create < Trailblazer::Operation 472 | 473 | step :create_model 474 | step Policy::Create, 475 | In() => {:current_user => :user}, 476 | In() => [:model], 477 | Out() => [:message], 478 | id: :policy 479 | #~meths 480 | include ComposableVariableMappingDocTest::Steps 481 | #~meths end 482 | end 483 | end 484 | #:inheritance-base end 485 | 486 | # puts Trailblazer::Developer::Render::TaskWrap.(Create, id: :policy) 487 | =begin 488 | #:tw-render 489 | puts Trailblazer::Developer::Render::TaskWrap.(Memo::Operation::Create, id: :policy) 490 | #:tw-render end 491 | =end 492 | 493 | =begin 494 | #:tw-render-out 495 | Memo::Operation::Create 496 | `-- policy 497 | |-- task_wrap.input..................Trailblazer::Operation::DSL::Linear::VariableMapping::Pipe::Input 498 | | |-- input.init_hash.............................. ............................................. VariableMapping.initial_aggregate 499 | | |-- input.add_variables.0.994[...]............... {:current_user=>:user}....................... VariableMapping::AddVariables 500 | | |-- input.add_variables.0.592[...]............... [:model]..................................... VariableMapping::AddVariables 501 | | `-- input.scope.................................. ............................................. VariableMapping.scope 502 | |-- task_wrap.call_task..............Method 503 | `-- task_wrap.output.................Trailblazer::Operation::DSL::Linear::VariableMapping::Pipe::Output 504 | |-- output.init_hash............................. ............................................. VariableMapping.initial_aggregate 505 | |-- output.add_variables.0.599[...].............. [:message]................................... VariableMapping::AddVariables::Output 506 | `-- output.merge_with_original................... ............................................. VariableMapping.merge_with_original 507 | #:tw-render-out end 508 | =end 509 | 510 | #:inheritance-sub 511 | module Memo::Operation 512 | class Admin < Create 513 | step Policy::Create, 514 | Out() => {:message => :raw_message_for_admin}, 515 | inherit: [:variable_mapping], 516 | id: :policy, # you need to reference the :id when your step 517 | replace: :policy 518 | end 519 | end 520 | #:inheritance-sub end 521 | 522 | # puts Trailblazer::Developer::Render::TaskWrap.(Admin, id: :policy) 523 | =begin 524 | #:sub-pipe 525 | puts Trailblazer::Developer::Render::TaskWrap.(Memo::Operation::Admin, id: :policy) 526 | 527 | Memo::Operation::Admin 528 | # `-- policy 529 | # |-- task_wrap.input..................Trailblazer::Operation::DSL::Linear::VariableMapping::Pipe::Input 530 | # | |-- input.init_hash.............................. ............................................. VariableMapping.initial_aggregate 531 | # | |-- input.add_variables.0.994[...]............... {:current_user=>:user}....................... VariableMapping::AddVariables 532 | # | |-- input.add_variables.0.592[...]............... [:model]..................................... VariableMapping::AddVariables 533 | # | `-- input.scope.................................. ............................................. VariableMapping.scope 534 | # |-- task_wrap.call_task..............Method 535 | # `-- task_wrap.output.................Trailblazer::Operation::DSL::Linear::VariableMapping::Pipe::Output 536 | # |-- output.init_hash............................. ............................................. VariableMapping.initial_aggregate 537 | # |-- output.add_variables.0.599[...].............. [:message]................................... VariableMapping::AddVariables::Output 538 | # |-- output.add_variables.0.710[...].............. {:message=>:raw_message_for_admin}........... VariableMapping::AddVariables::Output 539 | # `-- output.merge_with_original................... ............................................. VariableMapping.merge_with_original 540 | #:sub-pipe end 541 | =end 542 | 543 | it do 544 | #= policy didn't set any message 545 | assert_invoke Memo::Operation::Admin, current_user: Module, expected_ctx_variables: {model: Object, message: nil, :raw_message_for_admin=>nil} 546 | assert_invoke Memo::Operation::Admin, current_user: nil, terminus: :failure, expected_ctx_variables: {model: Object, :message=>"Command {create} not allowed!", :raw_message_for_admin=>"Command {create} not allowed!"} 547 | end 548 | end 549 | 550 | # Inject() 1.0 551 | class CVInjectTest < Minitest::Spec 552 | Memo = Module.new 553 | 554 | class ApplicationPolicy 555 | def self.can?(model, user, action) 556 | decision = !user.nil? && action == :create 557 | Struct.new(:allowed?).new(decision) 558 | end 559 | end 560 | 561 | #:policy-check 562 | module Policy 563 | class Check 564 | # vvvvvvvvvvvvvvv-- defaulted keyword arguments 565 | def self.call(ctx, model:, user:, action: :create, **) 566 | decision = ApplicationPolicy.can?(model, user, action) # FIXME: how does pundit/cancan do this exactly? 567 | #~decision 568 | 569 | if decision.allowed? 570 | return true 571 | else 572 | ctx[:message] = "Command {#{action}} not allowed!" 573 | return false 574 | end 575 | #~decision end 576 | end 577 | end 578 | end 579 | #:policy-check end 580 | 581 | #:inject 582 | module Memo::Operation 583 | class Create < Trailblazer::Operation 584 | step :create_model 585 | step Policy::Check, 586 | In() => {:current_user => :user}, 587 | In() => [:model], 588 | Inject() => [:action] 589 | #~meths 590 | include ComposableVariableMappingDocTest::Steps 591 | #~meths end 592 | end 593 | end 594 | #:inject end 595 | 596 | it "Inject()" do 597 | #= {:action} defaulted to {:create} 598 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object} 599 | 600 | #= {:action} set explicitely to {:create} 601 | assert_invoke Memo::Operation::Create, current_user: Module, action: :create, expected_ctx_variables: {model: Object} 602 | 603 | #= {:action} set explicitely to {:update}, policy breach 604 | assert_invoke Memo::Operation::Create, current_user: Module, action: :update, expected_ctx_variables: {model: Object, message: "Command {update} not allowed!"}, terminus: :failure 605 | end 606 | end 607 | 608 | class CVNoInjectTest < Minitest::Spec 609 | Policy = CVInjectTest::Policy 610 | Memo = Module.new 611 | 612 | #:no-inject 613 | module Memo::Operation 614 | class Create < Trailblazer::Operation 615 | step :create_model 616 | step Policy::Check, 617 | In() => {:current_user => :user}, 618 | In() => [:model, :action] 619 | #~meths 620 | include ComposableVariableMappingDocTest::Steps 621 | #~meths end 622 | end 623 | end 624 | #:no-inject end 625 | 626 | it "not using Inject()" do 627 | #= {:action} not defaulted as In() passes nil 628 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object, message: "Command {} not allowed!"}, terminus: :failure 629 | 630 | #= {:action} set explicitely to {:create} 631 | assert_invoke Memo::Operation::Create, current_user: Module, action: :create, expected_ctx_variables: {model: Object} 632 | 633 | #= {:action} set explicitely to {:update} 634 | assert_invoke Memo::Operation::Create, current_user: Module, action: :update, expected_ctx_variables: {model: Object, message: "Command {update} not allowed!"}, terminus: :failure 635 | end 636 | end 637 | 638 | class CVInjectDefaultTest < Minitest::Spec 639 | ApplicationPolicy = CVInjectTest::ApplicationPolicy 640 | Memo = Module.new 641 | 642 | #:policy-check-nodef 643 | module Policy 644 | class Check 645 | # vvvvvvv-- no defaulting! 646 | def self.call(ctx, model:, user:, action:, **) 647 | decision = ApplicationPolicy.can?(model, user, action) # FIXME: how does pundit/cancan do this exactly? 648 | #~decision 649 | 650 | if decision.allowed? 651 | return true 652 | else 653 | ctx[:message] = "Command {#{action}} not allowed!" 654 | return false 655 | end 656 | #~decision end 657 | end 658 | end 659 | end 660 | #:policy-check-nodef end 661 | 662 | #:inject-default 663 | module Memo::Operation 664 | class Create < Trailblazer::Operation 665 | step :create_model 666 | step Policy::Check, 667 | In() => {:current_user => :user}, 668 | In() => [:model], 669 | Inject(:action) => ->(ctx, **) { :create } 670 | #~meths 671 | include ComposableVariableMappingDocTest::Steps 672 | #~meths end 673 | end 674 | end 675 | #:inject-default end 676 | 677 | it "Inject() with default" do 678 | #= {:action} defaulted by Inject() 679 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object} 680 | 681 | #= {:action} set explicitely to {:create} 682 | assert_invoke Memo::Operation::Create, current_user: Module, action: :create, expected_ctx_variables: {model: Object} 683 | 684 | #= {:action} set explicitely to {:update} 685 | assert_invoke Memo::Operation::Create, current_user: Module, action: :update, expected_ctx_variables: {model: Object, message: "Command {update} not allowed!"}, terminus: :failure 686 | end 687 | end 688 | 689 | class CVInjectOverrideTest < Minitest::Spec 690 | Policy = CVInjectDefaultTest::Policy 691 | Memo = Module.new 692 | 693 | #:inject-override 694 | module Memo::Operation 695 | class Create < Trailblazer::Operation 696 | step :create_model 697 | step Policy::Check, 698 | In() => {:current_user => :user}, 699 | In() => [:model], 700 | #:inject_override_iso 701 | Inject(:action, override: true) => ->(*) { :create } # always used. 702 | #:inject_override_iso end 703 | #~meths 704 | include ComposableVariableMappingDocTest::Steps 705 | #~meths end 706 | end 707 | end 708 | #:inject-override end 709 | 710 | it "Inject() with default" do 711 | #= {:action} override 712 | assert_invoke Memo::Operation::Create, current_user: Module, expected_ctx_variables: {model: Object} 713 | 714 | #= {:action} still overridden 715 | assert_invoke Memo::Operation::Create, current_user: Module, action: :update, expected_ctx_variables: {model: Object} 716 | 717 | current_user = Module 718 | 719 | #:inject-override-call 720 | result = Memo::Operation::Create.( 721 | current_user: current_user, 722 | action: :update # this is always overridden. 723 | ) 724 | #~ctx_to_result 725 | puts result[:model] #=> # 726 | #~ctx_to_result end 727 | #:inject-override-call end 728 | 729 | assert_equal result[:model], Object 730 | end 731 | end 732 | 733 | # def operation_for(&block) 734 | # namespace = Module.new 735 | # # namespace::Policy = ComposableVariableMappingDocTest::A::Policy 736 | # namespace.const_set :Policy, A::Policy 737 | 738 | # namespace.module_eval do 739 | # operation = yield 740 | # operation.class_eval do 741 | # include ComposableVariableMappingDocTest::Steps 742 | # end 743 | # end 744 | # end # operation_for 745 | 746 | class DefaultInjectOnlyTest < Minitest::Spec 747 | it "Inject(), only, without In()" do 748 | class Create < Trailblazer::Operation 749 | step :write, 750 | Inject() => { name: ->(ctx, field:, **) { field } } 751 | 752 | def write(ctx, name: nil, **) 753 | ctx[:write] = %{ 754 | name: #{name.inspect} 755 | } 756 | end 757 | end 758 | 759 | assert_invoke Create, field: Module, expected_ctx_variables: {write: %{ 760 | name: Module 761 | }} 762 | end 763 | end 764 | 765 | class PassthroughInjectOnlyTest < Minitest::Spec 766 | it "Inject() => [...], only, without In()" do 767 | class Create < Trailblazer::Operation 768 | step :write, 769 | Inject() => [:name] 770 | 771 | def write(ctx, name: nil, **) 772 | ctx[:write] = %{ 773 | name: #{name.inspect} 774 | } 775 | end 776 | end 777 | 778 | assert_invoke Create, name: Module, expected_ctx_variables: {write: %{ 779 | name: Module 780 | }} 781 | end 782 | end 783 | 784 | #@ Out() 1.5 785 | #@ First, blacklist all, then add whitelisted. 786 | class OutMultipleTimes < Minitest::Spec 787 | Policy = ComposableVariableMappingDocTest::A::Policy 788 | 789 | class Create < Trailblazer::Operation 790 | step :model 791 | step Policy::Create, 792 | In() => {:current_user => :user}, 793 | In() => [:model], 794 | Out() => [], 795 | Out() => [:message] 796 | 797 | #~meths 798 | def model(ctx, **) 799 | ctx[:model] = Object 800 | end 801 | #~meths end 802 | end 803 | 804 | it "Out() can be used sequentially" do 805 | #= policy didn't set any message 806 | assert_invoke Create, current_user: Module, expected_ctx_variables: {model: Object, message: nil} 807 | #= policy breach, {message_from_policy} set. 808 | assert_invoke Create, current_user: nil, terminus: :failure, expected_ctx_variables: {model: Object, message: "Command {create} not allowed!"} 809 | end 810 | end 811 | 812 | 813 | class IoOutDeleteTest < Minitest::Spec 814 | #@ Delete a key in the outgoing ctx. 815 | it "Out() DSL: {delete: true} forces deletion in aggregate." do 816 | class Create < Trailblazer::Operation 817 | step :create_model, 818 | Out() => [:model], 819 | Out() => ->(ctx, **) { 820 | {errors: {}, # this is deleted. 821 | status: 200} # this sticks around. 822 | }, 823 | Out(delete: true) => [:errors] # always deletes from aggregate. 824 | 825 | def create_model(ctx, current_user:, **) 826 | ctx[:private] = "hi!" 827 | ctx[:model] = [current_user, ctx.keys] 828 | end 829 | end 830 | 831 | assert_invoke Create, current_user: Object, expected_ctx_variables: { 832 | model: [Object, [:seq, :current_user, :private]], 833 | :status=>200, 834 | } 835 | end 836 | end 837 | 838 | # {:read_from_aggregate} for the moment is only supposed to be used with SetVariable filters. 839 | class IoOutDeleteReadFromAggregateTest < Minitest::Spec 840 | #@ Rename a key *in the aggregate* and delete the original in {aggregate}. 841 | # NOTE: this is currently experimental. 842 | it "Out() DSL: {delete: true} forces deletion in outgoing ctx. Renaming can be applied on {:input_hash}" do 843 | class Create < Trailblazer::Operation 844 | step :create_model, 845 | Out() => [:model], 846 | Out() => ->(ctx, **) { {errors: {}} }, 847 | Out(read_from_aggregate: true) => {:errors => :create_model_errors}, 848 | Out(delete: true) => [:errors] # always on aggregate. 849 | 850 | def create_model(ctx, current_user:, **) 851 | ctx[:private] = "hi!" 852 | ctx[:model] = [current_user, ctx.keys] # we want only this on the outside, as {:song} and {:hit}! 853 | end 854 | end 855 | 856 | #@ we basically rename {:errors} to {:create_model_errors} in the {:aggregate} itself. 857 | assert_invoke Create, current_user: Object, expected_ctx_variables: { 858 | model: [Object, [:seq, :current_user, :private]], 859 | create_model_errors: {}, 860 | } 861 | end 862 | end 863 | 864 | #@ In() can override Inject() if it was added last. 865 | class InInjectSortingTest < Minitest::Spec 866 | it do 867 | activity = Class.new(Trailblazer::Activity::Railway) do 868 | step :params, 869 | Inject() => [:params], 870 | In() => ->(ctx, **) { {params: {id: 1}} } 871 | 872 | def params(ctx, params:, **) 873 | ctx[:captured_params] = params.inspect 874 | end 875 | end 876 | 877 | assert_invoke activity, expected_ctx_variables: {captured_params: "#{{:id=>1}}"} 878 | assert_invoke activity, params: {id: nil}, expected_ctx_variables: {params: {id: nil}, captured_params: "#{{:id=>1}}"} 879 | end 880 | end 881 | -------------------------------------------------------------------------------- /test/docs/autogenerated/fast_track_layout_test.rb: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM trailblazer-activity-dsl-linear/test/docs/fast_track_layout_test.rb 2 | require "test_helper" 3 | 4 | class FastTrack_Layout_Passfast_DocTest < Minitest::Spec 5 | Memo = Class.new 6 | it do 7 | #:ft-passfast 8 | module Memo::Operation 9 | class Create < Trailblazer::Operation 10 | step :validate, pass_fast: true 11 | step :save 12 | fail :handle_errors 13 | #~mod 14 | include T.def_steps(:validate, :handle_errors, :save) 15 | #~mod end 16 | end 17 | end 18 | #:ft-passfast end 19 | 20 | assert_invoke Memo::Operation::Create, terminus: :pass_fast, seq: "[:validate]" 21 | assert_invoke Memo::Operation::Create, validate: false, seq: "[:validate, :handle_errors]", terminus: :failure 22 | end 23 | end 24 | 25 | class FastTrack_Layout_Failfast_DocTest < Minitest::Spec 26 | Memo = Class.new 27 | it do 28 | #:ft-failfast 29 | module Memo::Operation 30 | class Create < Trailblazer::Operation 31 | step :validate, fail_fast: true 32 | step :save 33 | fail :handle_errors 34 | #~mod 35 | include T.def_steps(:validate, :handle_errors, :save) 36 | #~mod end 37 | end 38 | end 39 | #:ft-failfast end 40 | 41 | assert_invoke Memo::Operation::Create, terminus: :fail_fast, seq: "[:validate]", validate: false 42 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save]" 43 | assert_invoke Memo::Operation::Create, save: false, seq: "[:validate, :save, :handle_errors]", terminus: :failure 44 | end 45 | end 46 | 47 | class FastTrack_Layout_FastTrack_DocTest < Minitest::Spec 48 | Memo = Class.new 49 | it do 50 | #:ft-fasttrack 51 | class Create < Trailblazer::Operation 52 | module Memo::Operation 53 | class Create < Trailblazer::Operation 54 | step :validate, fast_track: true 55 | step :save 56 | fail :handle_errors 57 | 58 | def validate(ctx, params:, **) 59 | return Railway.fail_fast! if params.nil? 60 | 61 | params.key?(:memo) 62 | end 63 | #~mod 64 | include T.def_steps(:validate, :handle_errors, :save) 65 | #~mod end 66 | end 67 | end 68 | end 69 | #:ft-fasttrack end 70 | 71 | 72 | assert_invoke Memo::Operation::Create, terminus: :fail_fast, seq: "[]", params: nil 73 | assert_invoke Memo::Operation::Create, seq: "[:save]", params: {memo: nil} 74 | assert_invoke Memo::Operation::Create, seq: "[:handle_errors]", terminus: :failure, params: {} 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/docs/autogenerated/mechanics_test.rb: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM trailblazer-activity-dsl-linear/test/docs/mechanics_test.rb 2 | require "test_helper" 3 | 4 | module Y 5 | class DocsMechanicsTest < Minitest::Spec 6 | Memo = Module.new 7 | it "what" do 8 | #:instance-method 9 | module Memo::Operation 10 | class Create < Trailblazer::Operation 11 | step :validate 12 | 13 | #~meths 14 | def validate(ctx, params:, **) 15 | params.key?(:memo) ? true : false # return value matters! 16 | end 17 | #~meths end 18 | end 19 | end 20 | #:instance-method end 21 | 22 | #:instance-method-call 23 | result = Memo::Operation::Create.call(params: {memo: nil}) 24 | #:instance-method-call end 25 | assert_equal result.success?, true 26 | 27 | #:instance-method-implicit-call 28 | result = Memo::Operation::Create.(params: {memo: nil}) 29 | # #:instance-method-implicit-call end 30 | assert_equal result.success?, true 31 | end 32 | end 33 | end 34 | 35 | class ReadfromCtx_DocsMechanicsTest < Minitest::Spec 36 | Memo = Module.new 37 | it "what" do 38 | #:ctx-read 39 | module Memo::Operation 40 | class Create < Trailblazer::Operation 41 | step :validate 42 | #~meths 43 | step :save 44 | 45 | def save(*); true; end 46 | #~meths end 47 | def validate(ctx, **) 48 | p ctx[:params] #=> {:memo=>nil} 49 | end 50 | end 51 | end 52 | #:ctx-read end 53 | 54 | #:ctx-read-call 55 | result = Memo::Operation::Create.(params: {memo: nil}) 56 | #:ctx-read-call end 57 | assert_equal result.success?, true 58 | end 59 | end 60 | 61 | class ReadfromCtxKwargs_DocsMechanicsTest < Minitest::Spec 62 | Memo = Module.new 63 | it "what" do 64 | module Memo::Operation 65 | class Create < Trailblazer::Operation 66 | step :validate 67 | #~meths 68 | step :save 69 | 70 | def save(*); true; end 71 | #~meths end 72 | #:ctx-read-kwargs 73 | def validate(ctx, params:, **) 74 | p params #=> {:memo=>nil} 75 | end 76 | #:ctx-read-kwargs end 77 | end 78 | end 79 | 80 | result = Memo::Operation::Create.(params: {memo: nil}) 81 | assert_equal result.success?, true 82 | 83 | user = Object 84 | assert_raises ArgumentError do 85 | #:kwargs-error 86 | result = Memo::Operation::Create.(current_user: user) 87 | #=> ArgumentError: missing keyword: :params 88 | # memo/operation/create.rb:9:in `validate' 89 | #:kwargs-error end 90 | end 91 | end 92 | end 93 | 94 | class WriteToCtx_DocsMechanicsTest < Minitest::Spec 95 | class Memo 96 | def initialize(*); end 97 | end 98 | it "what" do 99 | #:ctx-write-read 100 | module Memo::Operation 101 | class Create < Trailblazer::Operation 102 | step :validate 103 | step :save # sets ctx[:model] 104 | step :notify 105 | #~body 106 | #~meths 107 | def validate(ctx, params:, **) 108 | true 109 | end 110 | 111 | def send_email(*) 112 | true 113 | end 114 | #~meths end 115 | #:ctx-write 116 | def save(ctx, params:, **) 117 | ctx[:model] = Memo.new(params[:memo]) 118 | end 119 | #~body end 120 | #:ctx-write end 121 | def notify(ctx, model:, **) 122 | send_email(model) 123 | end 124 | end 125 | end 126 | #:ctx-write-read end 127 | 128 | #:result-read 129 | result = Memo::Operation::Create.(params: {memo: {content: "remember that"}}) 130 | 131 | result[:model] #=> # 132 | #:result-read end 133 | 134 | #:result-success 135 | puts result.success? #=> true 136 | #:result-success end 137 | 138 | assert_equal result[:model].class, Memo 139 | assert_equal result.success?, true 140 | 141 | user = Object 142 | assert_raises ArgumentError do 143 | #:kwargs-error 144 | result = Memo::Operation::Create.(current_user: user) 145 | #=> ArgumentError: missing keyword: :params 146 | # memo/operation/create.rb:9:in `validate' 147 | #:kwargs-error end 148 | end 149 | end 150 | end 151 | 152 | class ReturnValueSuccess_DocsMechanicsTest < Minitest::Spec 153 | Memo = Module.new 154 | it "what" do 155 | module Memo::Operation 156 | class Create < Trailblazer::Operation 157 | step :validate 158 | #~meths 159 | step :save 160 | 161 | def save(*); true; end 162 | #~meths end 163 | #:return-success 164 | def validate(ctx, params:, **) 165 | params.key?(:memo) # => true/false 166 | end 167 | #:return-success end 168 | end 169 | end 170 | 171 | result = Memo::Operation::Create.(params: {memo: nil}) 172 | assert_equal result.success?, true 173 | end 174 | end 175 | 176 | class ReturnValueFailure_DocsMechanicsTest < Minitest::Spec 177 | Memo = Module.new 178 | it "what" do 179 | module Memo::Operation 180 | class Create < Trailblazer::Operation 181 | step :validate 182 | #~meths 183 | step :save 184 | 185 | def save(*); true; end 186 | #~meths end 187 | #:return-failure 188 | def validate(ctx, params:, **) 189 | nil 190 | end 191 | #:return-failure end 192 | end 193 | end 194 | 195 | result = Memo::Operation::Create.(params: {memo: nil}) 196 | assert_equal result.success?, false 197 | end 198 | end 199 | 200 | class ReturnSignal_DocsMechanicsTest < Minitest::Spec 201 | Memo = Module.new 202 | it "what" do 203 | #:signal-operation 204 | module Memo::Operation 205 | class Create < Trailblazer::Operation 206 | class NetworkError < Trailblazer::Activity::Signal 207 | end 208 | #~meths 209 | #:signal-steps 210 | step :validate 211 | step :save 212 | left :handle_errors 213 | step :notify, 214 | Output(NetworkError, :network_error) => End(:network_error) 215 | #:signal-steps end 216 | def save(ctx, **) 217 | ctx[:model] = Object 218 | end 219 | def validate(ctx, params:, **) 220 | true 221 | end 222 | def send_email(model) 223 | true 224 | end 225 | def check_network(params) 226 | ! params[:network_broken] 227 | end 228 | 229 | #:return-signal 230 | def notify(ctx, model:, params:, **) 231 | return NetworkError unless check_network(params) 232 | 233 | send_email(model) 234 | end 235 | #:return-signal end 236 | #~meths end 237 | end 238 | end 239 | #:signal-operation end 240 | 241 | result = Memo::Operation::Create.(params: {memo: nil, network_broken: false}) 242 | assert_equal result.success?, true 243 | 244 | #:terminus 245 | result = Memo::Operation::Create.(params: {memo: nil, network_broken: true}) 246 | 247 | result.terminus.to_h[:semantic] #=> :network_error 248 | #:terminus end 249 | assert_equal result.success?, false 250 | assert_equal result.terminus.to_h[:semantic], :network_error 251 | 252 | #:terminus-subprocess 253 | module Endpoint 254 | class API < Trailblazer::Operation 255 | step Subprocess(Memo::Operation::Create), 256 | Output(:network_error) => Track(:failure) 257 | # ... 258 | end 259 | end 260 | #:terminus-subprocess end 261 | end 262 | end 263 | 264 | class Classmethod_DocsMechanicsTest < Minitest::Spec 265 | Memo = Module.new 266 | it "what" do 267 | #:class-method 268 | module Memo::Operation 269 | class Create < Trailblazer::Operation 270 | #~meths 271 | # Define {Memo::Operation::Create.validate} 272 | def self.validate(ctx, params:, **) 273 | params.key?(:memo) ? true : false # return value matters! 274 | end 275 | #~meths end 276 | 277 | step method(:validate) 278 | end 279 | end 280 | #:class-method end 281 | end 282 | end 283 | 284 | class Module_Classmethod_DocsMechanicsTest < Minitest::Spec 285 | Memo = Module.new 286 | it "what" do 287 | #:module-step 288 | # Reusable steps in a module. 289 | module Steps 290 | def self.validate(ctx, params:, **) 291 | params.key?(:memo) ? true : false # return value matters! 292 | end 293 | end 294 | #:module-step end 295 | 296 | #:module-method 297 | module Memo::Operation 298 | class Create < Trailblazer::Operation 299 | step Steps.method(:validate) 300 | end 301 | end 302 | #:module-method end 303 | end 304 | end 305 | 306 | class Callable_DocsMechanicsTest < Minitest::Spec 307 | Memo = Module.new 308 | it "what" do 309 | #:callable-step 310 | module Validate 311 | def self.call(ctx, params:, **) 312 | valid?(params) ? true : false # return value matters! 313 | end 314 | 315 | def valid?(params) 316 | params.key?(:memo) 317 | end 318 | end 319 | #:callable-step end 320 | 321 | #:callable-method 322 | module Memo::Operation 323 | class Create < Trailblazer::Operation 324 | step Validate 325 | end 326 | end 327 | #:callable-method end 328 | end 329 | end 330 | 331 | class Lambda_DocsMechanicsTest < Minitest::Spec 332 | Memo = Module.new 333 | it "what" do 334 | #:lambda-step 335 | module Memo::Operation 336 | class Create < Trailblazer::Operation 337 | step ->(ctx, params:, **) { p params.inspect } 338 | end 339 | end 340 | #:lambda-step end 341 | end 342 | end 343 | 344 | class Inheritance_DocsMechanicsTest < Minitest::Spec 345 | Memo = Module.new 346 | it "what" do 347 | #:inherit-create 348 | module Memo::Operation 349 | class Create < Trailblazer::Operation 350 | step :create_model 351 | step :validate 352 | step :save 353 | #~meths 354 | include T.def_steps(:create_model, :validate, :save) 355 | #~meths end 356 | end 357 | end 358 | #:inherit-create end 359 | 360 | #:inherit-update-empty 361 | module Memo::Operation 362 | class Update < Create 363 | end 364 | end 365 | #:inherit-update-empty end 366 | 367 | #:inherit-update 368 | module Memo::Operation 369 | class Update < Create 370 | step :find_model, replace: :create_model 371 | #~meths 372 | include T.def_steps(:find_model) 373 | #~meths end 374 | end 375 | end 376 | #:inherit-update end 377 | 378 | assert_invoke Memo::Operation::Create, seq: "[:create_model, :validate, :save]" 379 | assert_invoke Memo::Operation::Update, seq: "[:find_model, :validate, :save]" 380 | end 381 | end 382 | 383 | -------------------------------------------------------------------------------- /test/docs/autogenerated/sequence_options_test.rb: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM trailblazer-activity-dsl-linear/test/docs/sequence_options_test.rb 2 | require "test_helper" 3 | # require "trailblazer/developer" 4 | 5 | module A 6 | class Id_DocSeqOptionsTest < Minitest::Spec 7 | Memo = Struct.new(:text) 8 | 9 | #:id 10 | module Memo::Operation 11 | class Create < Trailblazer::Operation 12 | step :validate 13 | step :save, id: :save_the_world 14 | step :notify 15 | #~meths 16 | include T.def_steps(:validate, :save, :notify) 17 | #~meths end 18 | end 19 | end 20 | #:id end 21 | 22 | it ":id shows up in introspect" do 23 | =begin 24 | output = 25 | #:id-inspect 26 | Trailblazer::Developer.railway(Memo::Operation::Create) 27 | #=> [>validate,>save_the_world,>notify] 28 | #:id-inspect end 29 | =end 30 | 31 | #~ignore end 32 | 33 | assert Trailblazer::Activity::Introspect.Nodes(Memo::Operation::Create, id: :save_the_world) 34 | #:id-introspect 35 | puts Trailblazer::Activity::Introspect.Nodes(Memo::Operation::Create, id: :save_the_world) 36 | #=> # 37 | #:id-introspect end 38 | end 39 | end 40 | end 41 | 42 | module B 43 | class Delete_DocsSequenceOptionsTest < Minitest::Spec 44 | Memo = A::Id_DocSeqOptionsTest::Memo 45 | 46 | it ":delete removes step" do 47 | #:delete 48 | module Memo::Operation 49 | class Admin < Create 50 | step nil, delete: :validate 51 | end 52 | end 53 | #:delete end 54 | 55 | =begin 56 | output = 57 | #:delete-inspect 58 | Trailblazer::Developer.railway(Memo::Operation::Admin) 59 | #=> [>save_the_world,>notify] 60 | #:delete-inspect end 61 | =end 62 | 63 | #~ignore end 64 | end 65 | end 66 | end 67 | 68 | module C 69 | class Before_DocsSequenceOptionsTest < Minitest::Spec 70 | it ":before" do 71 | Memo = A::Id_DocSeqOptionsTest::Memo 72 | 73 | #:before 74 | module Memo::Operation 75 | class Authorized < Memo::Operation::Create 76 | step :policy, before: :validate 77 | #~meths 78 | include T.def_steps(:policy) 79 | #~meths end 80 | end 81 | end 82 | #:before end 83 | 84 | =begin 85 | #:before-inspect 86 | Trailblazer::Developer.railway(Memo::Operation::Authorized) 87 | #=> [>policy,>validate,>save_the_world,>notify] 88 | #:before-inspect end 89 | =end 90 | 91 | #~ignore end 92 | end 93 | 94 | end 95 | end 96 | 97 | module D 98 | class After_DocsSequenceOptionsTest < Minitest::Spec 99 | it ":after" do 100 | Memo = Class.new(A::Id_DocSeqOptionsTest::Memo) 101 | Memo::Operation = Module.new 102 | Memo::Operation::Create = Class.new(A::Id_DocSeqOptionsTest::Memo::Operation::Create) 103 | 104 | #:after 105 | module Memo::Operation 106 | class Authorized < Memo::Operation::Create 107 | step :policy, after: :validate 108 | #~meths 109 | include T.def_steps(:policy) 110 | #~meths end 111 | end 112 | end 113 | #:after end 114 | 115 | =begin 116 | #:after-inspect 117 | Trailblazer::Developer.railway(Memo::Operation::Authorized) 118 | #=> [>validate,>policy,>save_the_world,>notify] 119 | #:after-inspect end 120 | =end 121 | 122 | #~ignore end 123 | end 124 | end 125 | end 126 | 127 | module E 128 | class Replace_DocsSequenceOptionsTest < Minitest::Spec 129 | Memo = Class.new 130 | module Memo::Operation 131 | class Create < Trailblazer::Operation 132 | step :validate 133 | step :save 134 | step :notify 135 | #~meths 136 | include T.def_steps(:validate, :save, :notify) 137 | #~meths end 138 | end 139 | end 140 | 141 | it "{:replace} automatically assigns ID" do 142 | #:replace 143 | module Memo::Operation 144 | class Update < Create 145 | step :update, replace: :save 146 | #~meths 147 | include T.def_steps(:update) 148 | #~meths end 149 | end 150 | 151 | end 152 | #:replace end 153 | 154 | =begin 155 | #:replace-inspect 156 | Trailblazer::Developer.railway(Memo::Operation::Update) 157 | #=> [>validate,>update,>notify] 158 | #:replace-inspect end 159 | =end 160 | assert Trailblazer::Activity::Introspect.Nodes(Memo::Operation::Update, id: :update) 161 | 162 | #~ignore end 163 | end 164 | end 165 | end 166 | 167 | #@ {#replace} with {:id} 168 | module E_2 169 | class Replace_With_ID_DocsSequenceOptionsTest < Minitest::Spec 170 | Memo = Class.new 171 | module Memo::Operation 172 | class Create < Trailblazer::Operation 173 | step :validate 174 | step :save 175 | step :notify 176 | #~meths 177 | include T.def_steps(:validate, :save, :notify) 178 | #~meths end 179 | end 180 | end 181 | 182 | it "{:replace} allows explicit ID" do 183 | #:replace-id 184 | module Memo::Operation 185 | class Update < Create 186 | step :update, replace: :save, id: :update_memo 187 | #~meths 188 | include T.def_steps(:update) 189 | #~meths end 190 | end 191 | 192 | end 193 | #:replace-id end 194 | 195 | # assert_equal Trailblazer::Developer.railway(Memo::Operation::Update), %([>validate,>update_memo,>notify]) 196 | 197 | Trailblazer::Activity::Introspect.Nodes(Memo::Operation::Update, id: :update_memo) 198 | 199 | #~ignore end 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /test/docs/autogenerated/subprocess_test.rb: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM trailblazer-activity-dsl-linear/test/docs/subprocess_test.rb 2 | require "test_helper" 3 | 4 | class SubprocessDocsTest < Minitest::Spec 5 | Memo = Class.new 6 | #:nested 7 | module Memo::Operation 8 | class Validate < Trailblazer::Operation 9 | step :check_params 10 | step :text_present? 11 | #~meths 12 | include T.def_steps(:check_params, :text_present?) 13 | #~meths end 14 | end 15 | end 16 | #:nested end 17 | 18 | #:container 19 | module Memo::Operation 20 | class Create < Trailblazer::Operation 21 | step Subprocess(Validate) 22 | step :save 23 | left :handle_errors 24 | step :notify 25 | #~meths 26 | include T.def_steps(:validate, :save, :handle_errors, :notify) 27 | #~meths end 28 | end 29 | end 30 | #:container end 31 | 32 | it "what" do 33 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :text_present?, :save, :notify]" 34 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :text_present?, :save, :handle_errors]", save: false, terminus: :failure 35 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :text_present?, :handle_errors]", text_present?: false, terminus: :failure 36 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :handle_errors]", check_params: false, terminus: :failure 37 | end 38 | end 39 | 40 | class Output_SubprocessDocsTest < Minitest::Spec 41 | Memo = Class.new 42 | 43 | module Memo::Operation 44 | class Validate < Trailblazer::Operation 45 | step :check_params 46 | step :text_present? 47 | #~meths 48 | include T.def_steps(:check_params, :text_present?) 49 | #~meths end 50 | end 51 | end 52 | 53 | #:container-output 54 | module Memo::Operation 55 | class Create < Trailblazer::Operation 56 | step Subprocess(Validate), 57 | Output(:failure) => Id(:notify) 58 | step :save 59 | left :handle_errors 60 | step :notify 61 | #~meths 62 | include T.def_steps(:validate, :save, :handle_errors, :notify) 63 | #~meths end 64 | end 65 | end 66 | #:container-output end 67 | 68 | it "what" do 69 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :text_present?, :save, :notify]" 70 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :text_present?, :save, :handle_errors]", save: false, terminus: :failure 71 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :text_present?, :notify]", text_present?: false 72 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :notify]", check_params: false 73 | end 74 | end 75 | 76 | class SubprocessDocsTest < Minitest::Spec 77 | Memo = Class.new 78 | #:nested-terminus 79 | module Memo::Operation 80 | class Validate < Trailblazer::Operation 81 | step :check_params, 82 | Output(:failure) => End(:invalid) 83 | step :text_present? 84 | #~meths 85 | include T.def_steps(:check_params, :text_present?) 86 | #~meths end 87 | end 88 | end 89 | #:nested-terminus end 90 | 91 | #:container-terminus 92 | module Memo::Operation 93 | class Create < Trailblazer::Operation 94 | step Subprocess(Validate), 95 | Output(:invalid) => Track(:failure) 96 | step :save 97 | left :handle_errors 98 | step :notify 99 | #~meths 100 | include T.def_steps(:validate, :save, :handle_errors, :notify) 101 | #~meths end 102 | end 103 | end 104 | #:container-terminus end 105 | 106 | it "what" do 107 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :text_present?, :save, :notify]" 108 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :text_present?, :save, :handle_errors]", save: false, terminus: :failure 109 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :text_present?, :handle_errors]", text_present?: false, terminus: :failure 110 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :handle_errors]", check_params: false, terminus: :failure 111 | end 112 | end 113 | 114 | #~ignore end 115 | 116 | class Strict_SubprocessDocsTest < Minitest::Spec 117 | Memo = Class.new 118 | 119 | module Memo::Operation 120 | class Validate < Trailblazer::Operation 121 | step :check_params, 122 | Output(:failure) => End(:invalid) 123 | step :text_present? 124 | #~meths 125 | include T.def_steps(:check_params, :text_present?) 126 | #~meths end 127 | end 128 | end 129 | 130 | module Memo::Operation 131 | class Create < Trailblazer::Operation 132 | step Subprocess(Validate, strict: true) # no wiring of {:invalid} terminus. 133 | step :save 134 | left :handle_errors 135 | step :notify 136 | #~meths 137 | include T.def_steps(:validate, :save, :handle_errors, :notify) 138 | #~meths end 139 | end 140 | end 141 | 142 | it "raises {IllegalSignalError} at runtime when not connected" do 143 | skip "see https://github.com/trailblazer/trailblazer-activity-dsl-linear/issues/59" 144 | 145 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :text_present?, :save, :notify]" 146 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :text_present?, :save, :handle_errors]", save: false, terminus: :failure 147 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :text_present?, :handle_errors]", text_present?: false, terminus: :failure 148 | # exception = assert_raises Trailblazer::Operation::Circuit::IllegalSignalError do 149 | assert_invoke Memo::Operation::Create, seq: "[:check_params, :handle_errors]", check_params: false, terminus: :failure 150 | # end 151 | 152 | # assert_equal exception.message.split("\n")[1][0..82], %(\e[31mUnrecognized Signal `#` returned) 153 | end 154 | end 155 | 156 | class FixmeSubprocess_FailFast_DocsTest < Minitest::Spec 157 | Memo = Class.new 158 | module Memo::Operation 159 | class Create < Trailblazer::Operation 160 | step :validate, 161 | fail_fast: true 162 | step :save 163 | #~meths 164 | include T.def_steps(:validate, :save) 165 | #~meths end 166 | end 167 | end 168 | 169 | it do 170 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save]" 171 | assert_invoke Memo::Operation::Create, seq: "[:validate]", terminus: :fail_fast, validate: false 172 | end 173 | end 174 | 175 | class Subprocess_FailFast_DocsTest < Minitest::Spec 176 | Memo = Struct.new(:id) 177 | 178 | module Memo::Operation 179 | class Validate < Trailblazer::Operation # Validate is a {Railway} 180 | step :validate 181 | include T.def_steps(:validate) 182 | end 183 | end 184 | 185 | module Memo::Operation 186 | class Create < Trailblazer::Operation 187 | step Subprocess(Validate), fail_fast: true 188 | step :save 189 | #~meths 190 | include T.def_steps(:save) 191 | #~meths end 192 | end 193 | end 194 | 195 | it do 196 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save]" 197 | assert_invoke Memo::Operation::Create, seq: "[:validate]", terminus: :fail_fast, validate: false 198 | end 199 | end 200 | 201 | class Subprocess_PassFast_DocsTest < Minitest::Spec 202 | Memo = Struct.new(:id) 203 | 204 | module Memo::Operation 205 | class Validate < Trailblazer::Operation 206 | step :validate 207 | include T.def_steps(:validate) 208 | end 209 | end 210 | 211 | module Memo::Operation 212 | class Create < Trailblazer::Operation 213 | step Subprocess(Validate), pass_fast: true 214 | step :save 215 | #~meths 216 | include T.def_steps(:save) 217 | #~meths end 218 | end 219 | end 220 | 221 | it do 222 | assert_invoke Memo::Operation::Create, seq: "[:validate]", terminus: :pass_fast 223 | assert_invoke Memo::Operation::Create, seq: "[:validate]", terminus: :failure, validate: false 224 | end 225 | end 226 | 227 | class Subprocess_FastTrack_DocsTest < Minitest::Spec 228 | Memo = Struct.new(:id) 229 | 230 | module Memo::Operation 231 | class Validate < Trailblazer::Operation 232 | step :validate, fast_track: true 233 | include T.def_steps(:validate) 234 | end 235 | end 236 | 237 | #:subprocess-fast-track 238 | module Memo::Operation 239 | class Create < Trailblazer::Operation 240 | step Subprocess(Validate), fast_track: true 241 | step :save 242 | left :handle_errors 243 | step :notify 244 | #~meths 245 | include T.def_steps(:save, :handle_errors, :notify) 246 | #~meths end 247 | end 248 | end 249 | #:subprocess-fast-track end 250 | 251 | it do 252 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :notify]" # validate returns {true}. 253 | assert_invoke Memo::Operation::Create, seq: "[:validate, :handle_errors]", validate: false, terminus: :failure # validate returns {false}. 254 | assert_invoke Memo::Operation::Create, seq: "[:validate]", validate: Trailblazer::Activity::FastTrack::PassFast, terminus: :pass_fast # validate returns {pass_fast!}. 255 | assert_invoke Memo::Operation::Create, seq: "[:validate]", validate: Trailblazer::Activity::FastTrack::FailFast, terminus: :fail_fast # validate returns {fail_fast!}. 256 | end 257 | end 258 | -------------------------------------------------------------------------------- /test/docs/autogenerated/wiring_api_test.rb: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM trailblazer-activity-dsl-linear/test/docs/wiring_api_test.rb 2 | require "test_helper" 3 | 4 | #@ original Memo::Operation::Create 5 | class Vanilla_WiringApiDocsTest < Minitest::Spec 6 | Memo = Class.new 7 | module Memo::Operation 8 | class Create < Trailblazer::Operation 9 | step :validate 10 | step :save 11 | left :handle_errors 12 | step :notify 13 | #~meths 14 | include T.def_steps(:validate, :save, :handle_errors, :notify) 15 | end 16 | end 17 | 18 | it "what" do 19 | =begin 20 | #:render 21 | puts Trailblazer::Developer.render(Memo::Operation::Create) 22 | 23 | # 24 | {Trailblazer::Operation::Right} => # 25 | # 26 | {Trailblazer::Operation::Left} => # 27 | {Trailblazer::Operation::Right} => # 28 | # 29 | {Trailblazer::Operation::Left} => # 30 | {Trailblazer::Operation::Right} => # 31 | # 32 | {Trailblazer::Operation::Left} => # 33 | {Trailblazer::Operation::Right} => # 34 | # 35 | {Trailblazer::Operation::Left} => # 36 | {Trailblazer::Operation::Right} => # 37 | # 38 | 39 | # 40 | #:render end 41 | =end 42 | end 43 | end 44 | 45 | #@ Output => End 46 | class Output_WiringApiDocsTest < Minitest::Spec 47 | Memo = Class.new 48 | module Memo::Operation 49 | class Create < Trailblazer::Operation 50 | step :validate 51 | step :save, 52 | Output(:failure) => End(:db_error) 53 | left :handle_errors 54 | step :notify 55 | #~meths 56 | include T.def_steps(:validate, :save, :handle_errors, :notify) 57 | end 58 | end 59 | 60 | it "what" do 61 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :notify]" 62 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save]", save: false, terminus: :db_error 63 | end 64 | end 65 | 66 | #@ failure/Output => End 67 | class OutputOnLeft_WiringApiDocsTest < Minitest::Spec 68 | Memo = Class.new 69 | #:left 70 | module Memo::Operation 71 | class Create < Trailblazer::Operation 72 | step :validate 73 | step :save 74 | left :handle_errors, 75 | Output(:success) => Track(:success) 76 | step :notify 77 | #~meths 78 | include T.def_steps(:validate, :save, :handle_errors, :notify) 79 | #~meths end 80 | end 81 | end 82 | #:left end 83 | 84 | it "what" do 85 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :notify]" 86 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :handle_errors, :notify]", save: false 87 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :handle_errors]", save: false, handle_errors: false, terminus: :failure 88 | end 89 | end 90 | 91 | #@ Output => Track 92 | class OutputToSuccess_WiringApiDocsTest < Minitest::Spec 93 | Memo = Class.new 94 | #:output-track 95 | module Memo::Operation 96 | class Create < Trailblazer::Operation 97 | step :validate 98 | step :save, 99 | Output(:failure) => Track(:success) 100 | left :handle_errors 101 | step :notify 102 | #~meths 103 | include T.def_steps(:validate, :save, :handle_errors, :notify) 104 | #~meths end 105 | end 106 | end 107 | #:output-track end 108 | 109 | it "what" do 110 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :notify]" 111 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :notify]", save: false 112 | end 113 | end 114 | 115 | #@ Output => Id 116 | class OutputToId_WiringApiDocsTest < Minitest::Spec 117 | Memo = Class.new 118 | #:output-id 119 | module Memo::Operation 120 | class Create < Trailblazer::Operation 121 | step :validate, 122 | Output(:failure) => Id(:notify) 123 | step :save 124 | left :handle_errors 125 | step :notify 126 | #~meths 127 | include T.def_steps(:validate, :save, :handle_errors, :notify) 128 | #~meths end 129 | end 130 | end 131 | #:output-id end 132 | 133 | it "what" do 134 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :notify]" 135 | assert_invoke Memo::Operation::Create, seq: "[:validate, :notify]", validate: false 136 | end 137 | end 138 | 139 | #@ Output(semantic, Signal) => Track 140 | class ExplicitOutput_WiringApiDocsTest < Minitest::Spec 141 | Memo = Struct.new(:save_result) do 142 | def save; self.save_result; end 143 | end 144 | 145 | #:output-explicit 146 | module Memo::Operation 147 | class Create < Trailblazer::Operation 148 | class DbError < Trailblazer::Activity::Signal; end 149 | 150 | step :validate 151 | step :save, 152 | Output(DbError, :database_error) => Track(:failure) 153 | left :handle_errors 154 | step :notify 155 | #~meths 156 | include T.def_steps(:validate, :handle_errors, :notify) 157 | def save(ctx, model:, **) 158 | #~code 159 | database_broken = ctx[:database_broken] 160 | #~code end 161 | return DbError if database_broken 162 | 163 | model.save 164 | end 165 | #~meths end 166 | end 167 | end 168 | #:output-explicit end 169 | 170 | it "what" do 171 | assert_invoke Memo::Operation::Create, seq: "[:validate, :handle_errors]", validate: false, terminus: :failure 172 | assert_invoke Memo::Operation::Create, seq: "[:validate, :notify]", model: Memo.new(true) 173 | assert_invoke Memo::Operation::Create, seq: "[:validate, :handle_errors]", model: Memo.new(false), terminus: :failure 174 | assert_invoke Memo::Operation::Create, seq: "[:validate, :handle_errors]", model: Memo.new(true), database_broken: true, terminus: :failure 175 | end 176 | end 177 | 178 | #@ Output => End 179 | class OutputToEnd_WiringApiDocsTest < Minitest::Spec 180 | Memo = Class.new 181 | #:output-end 182 | module Memo::Operation 183 | class Create < Trailblazer::Operation 184 | step :validate 185 | step :save, 186 | Output(:failure) => End(:db_error) 187 | left :handle_errors 188 | step :notify 189 | #~meths 190 | include T.def_steps(:validate, :save, :handle_errors, :notify) 191 | #~meths end 192 | end 193 | end 194 | #:output-end end 195 | 196 | it "what" do 197 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :notify]" 198 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save]", save: false, terminus: :db_error 199 | end 200 | end 201 | 202 | #@ #terminus 203 | class Terminus_WiringApiDocsTest < Minitest::Spec 204 | Memo = Class.new 205 | #:terminus 206 | module Memo::Operation 207 | class CRUD < Trailblazer::Operation 208 | step :validate 209 | step :save 210 | terminus :db_error 211 | #~meths 212 | include T.def_steps(:validate, :save, :handle_errors, :notify) 213 | #~meths end 214 | end 215 | end 216 | #:terminus end 217 | 218 | #:terminus-sub 219 | module Memo::Operation 220 | class Create < CRUD 221 | step :notify, 222 | Output(:failure) => End(:db_error) 223 | #~meths 224 | include T.def_steps(:validate, :save, :handle_errors, :notify) 225 | #~meths end 226 | end 227 | end 228 | #:terminus-sub end 229 | 230 | it "what" do 231 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :notify]" 232 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save]", save: false, terminus: :failure 233 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :notify]", notify: false, terminus: :db_error 234 | end 235 | end 236 | 237 | #@ Track() 238 | class Track_WiringApiDocsTest < Minitest::Spec 239 | Memo = Class.new 240 | #:custom-track 241 | module Memo::Operation 242 | class Charge < Trailblazer::Operation 243 | terminus :paypal # add a custom terminus (if you need it) 244 | step :validate 245 | step :find_provider, 246 | Output(:failure) => Track(:paypal) 247 | step :charge_paypal, 248 | magnetic_to: :paypal, Output(:success) => Track(:paypal) 249 | step :charge_default 250 | #~meths 251 | include T.def_steps(:validate, :find_provider, :charge_paypal, :charge_default) 252 | #~meths end 253 | end 254 | end 255 | #:custom-track end 256 | 257 | it "what" do 258 | assert_invoke Memo::Operation::Charge, seq: "[:validate, :find_provider, :charge_default]" 259 | assert_invoke Memo::Operation::Charge, seq: "[:validate, :find_provider, :charge_paypal]", find_provider: false, terminus: :paypal 260 | assert_invoke Memo::Operation::Charge, seq: "[:validate, :find_provider, :charge_paypal]", find_provider: false, charge_paypal: false, terminus: :failure 261 | end 262 | end 263 | 264 | #@ Path() 265 | class Path_WiringApiDocsTest < Minitest::Spec 266 | Memo = Class.new 267 | #:path-helper 268 | module Memo::Operation 269 | class Charge < Trailblazer::Operation 270 | step :validate 271 | step :find_provider, 272 | Output(:failure) => Path(terminus: :paypal) do 273 | # step :authorize # you can have multiple steps on a path. 274 | step :charge_paypal 275 | end 276 | step :charge_default 277 | #~meths 278 | include T.def_steps(:validate, :find_provider, :charge_paypal, :charge_default) 279 | #~meths end 280 | end 281 | end 282 | #:path-helper end 283 | 284 | it "what" do 285 | assert_invoke Memo::Operation::Charge, seq: "[:validate, :find_provider, :charge_default]" 286 | assert_invoke Memo::Operation::Charge, seq: "[:validate, :find_provider, :charge_paypal]", find_provider: false, terminus: :paypal 287 | # TODO: this doesn't add a {failure} output. 288 | # assert_invoke Memo::Operation::Charge, seq: "[:validate, :find_provider, :charge_paypal]", find_provider: false, charge_paypal: false, terminus: :failure 289 | end 290 | end 291 | 292 | #@ Path() with error handling: Output() 293 | # class Path_WiringApiDocsTest < Minitest::Spec 294 | # Memo = Class.new 295 | # #:path-helper-failure 296 | # module Memo::Operation 297 | # class Charge < Trailblazer::Operation 298 | # step :validate 299 | # step :find_provider, 300 | # Output(:failure) => Path(terminus: :paypal) do 301 | # step :charge_paypal, Output(:failure) => Track(:failure) # route to the "global" failure track. 302 | # end 303 | # step :charge_default 304 | 305 | # #~meths 306 | # include T.def_steps(:validate, :find_provider, :charge_paypal, :charge_default) 307 | # #~meths end 308 | # end 309 | # end 310 | # #:path-helper-failure end 311 | 312 | # it "what" do 313 | # assert_invoke Memo::Operation::Charge, seq: "[:validate, :find_provider, :charge_default]" 314 | # assert_invoke Memo::Operation::Charge, seq: "[:validate, :find_provider, :charge_paypal]", find_provider: false, terminus: :paypal 315 | # assert_invoke Memo::Operation::Charge, seq: "[:validate, :find_provider, :charge_paypal]", find_provider: false, charge_paypal: false, terminus: :failure 316 | # end 317 | # end 318 | 319 | #@ Path(:connect_to) 320 | class PathConnectTo_WiringApiDocsTest < Minitest::Spec 321 | Memo = Class.new 322 | #:path-helper-connect-to 323 | module Memo::Operation 324 | class Charge < Trailblazer::Operation 325 | step :validate 326 | step :find_provider, 327 | Output(:failure) => Path(connect_to: Id(:finalize)) do 328 | # step :authorize # you can have multiple steps on a path. 329 | step :charge_paypal 330 | end 331 | step :charge_default 332 | step :finalize 333 | #~meths 334 | include T.def_steps(:validate, :find_provider, :charge_paypal, :charge_default, :finalize) 335 | #~meths end 336 | end 337 | end 338 | #:path-helper-connect-to end 339 | 340 | it "what" do 341 | assert_invoke Memo::Operation::Charge, seq: "[:validate, :find_provider, :charge_default, :finalize]" 342 | assert_invoke Memo::Operation::Charge, seq: "[:validate, :find_provider, :charge_paypal, :finalize]", find_provider: false 343 | # TODO: this doesn't add a {failure} output. 344 | # assert_invoke Memo::Operation::Charge, seq: "[:validate, :find_provider, :charge_paypal]", find_provider: false, charge_paypal: false, terminus: :failure 345 | end 346 | end 347 | 348 | class WiringApiDocsTest < Minitest::Spec 349 | # {#terminus} 1.0 350 | module A 351 | class Payment 352 | module Operation 353 | end 354 | end 355 | 356 | #:terminus- 357 | module Payment::Operation 358 | class Create < Trailblazer::Operation 359 | step :find_provider 360 | 361 | terminus :provider_invalid # , id: "End.provider_invalid", magnetic_to: :provider_invalid 362 | #~meths 363 | include T.def_steps(:find_provider) 364 | #~meths end 365 | end 366 | end 367 | #:terminus- end 368 | # @diagram wiring-terminus 369 | end 370 | 371 | #@ we cannot route to {End.provider_invalid} 372 | it { assert_invoke A::Payment::Operation::Create, seq: "[:find_provider]" } 373 | it { assert_invoke A::Payment::Operation::Create, find_provider: false, seq: "[:find_provider]", terminus: :failure } 374 | 375 | # {#terminus} 1.1 376 | module B 377 | class Payment 378 | module Operation 379 | end 380 | end 381 | 382 | #:terminus-track 383 | module Payment::Operation 384 | class Create < Trailblazer::Operation 385 | step :find_provider, 386 | # connect {failure} to the next element that is magnetic_to {:provider_invalid}. 387 | Output(:failure) => Track(:provider_invalid) 388 | 389 | terminus :provider_invalid 390 | #~meths 391 | include T.def_steps(:find_provider) 392 | #~meths end 393 | end 394 | end 395 | #:terminus-track end 396 | end 397 | 398 | #@ failure routes to {End.provider_invalid} 399 | it { assert_invoke B::Payment::Operation::Create, seq: "[:find_provider]" } 400 | it { assert_invoke B::Payment::Operation::Create, find_provider: false, seq: "[:find_provider]", terminus: :provider_invalid } 401 | 402 | it do 403 | result = B::Payment::Operation::Create.(find_provider: false, seq: []) 404 | assert_equal result.terminus.to_h[:semantic], :provider_invalid 405 | =begin 406 | #:terminus-invalid 407 | result = Payment::Operation::Create.(provider: "bla-unknown") 408 | puts signal.to_h[:semantic] #=> :provider_invalid 409 | #:terminus-invalid end 410 | =end 411 | 412 | end 413 | end 414 | 415 | #@ :magnetic_to 416 | module A 417 | class MagneticTo_DocsTest < Minitest::Spec 418 | Memo = Class.new 419 | #:magnetic_to 420 | module Memo::Operation 421 | class Create < Trailblazer::Operation 422 | step :validate 423 | step :payment_provider, Output(:failure) => Track(:paypal) 424 | step :charge_paypal, magnetic_to: :paypal 425 | step :save 426 | end 427 | end 428 | #:magnetic_to end 429 | 430 | it "what" do 431 | #~ignore end 432 | end 433 | 434 | end 435 | end 436 | -------------------------------------------------------------------------------- /test/docs/developer_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Wtf_DeveloperDocsTest < Minitest::Spec 4 | Memo = Class.new 5 | module Memo::Operation 6 | class Create < Trailblazer::Operation 7 | step :validate 8 | step :save 9 | left :handle_errors 10 | step :notify 11 | #~meths 12 | include T.def_steps(:validate, :save, :handle_errors, :notify) 13 | #~meths end 14 | end 15 | end 16 | 17 | it "what" do 18 | #:wtf 19 | result = Memo::Operation::Create.wtf?( 20 | #~meths 21 | seq: [], 22 | #~meths end 23 | params: {memo: "remember me!"} 24 | ) 25 | #:wtf end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/docs/operation_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class DocsActivityTest < Minitest::Spec 4 | Memo = Struct.new(:text) 5 | 6 | #:memo 7 | module Memo::Operation 8 | class Create < Trailblazer::Operation 9 | step :create_model 10 | 11 | def create_model(ctx, params:, **) 12 | ctx[:model] = Memo.new(params[:text]) 13 | end 14 | end 15 | end 16 | #:memo end 17 | 18 | describe Memo::Operation::Create do 19 | it "allows indifferent access for ctx keys" do 20 | #:ctx-indifferent-access 21 | result = Memo::Operation::Create.(params: { text: "Enjoy an IPA" }) 22 | 23 | result[:params] # => { text: "Enjoy an IPA" } 24 | result['params'] # => { text: "Enjoy an IPA" } 25 | #:ctx-indifferent-access end 26 | 27 | assert_equal result.success?, true 28 | assert_equal result[:params],({ text: "Enjoy an IPA" }) 29 | assert_equal result["params"], ({ text: "Enjoy an IPA" }) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/docs/step_dsl_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module A 4 | class DocsStepTest < Minitest::Spec 5 | Memo = Module.new 6 | 7 | #:railway 8 | module Memo::Operation 9 | class Create < Trailblazer::Operation 10 | step :validate 11 | step :save 12 | #~step 13 | left :handle_errors 14 | #~left 15 | step :notify 16 | #~meths 17 | # fail :log_error 18 | # step :save 19 | def validate(ctx, params:, **) 20 | ctx[:input] = Form.validate(params) # true/false 21 | end 22 | 23 | # def create(ctx, input:, create:, **) 24 | # create 25 | # end 26 | 27 | # def log_error(ctx, logger:, params:, **) 28 | # logger.error("wrong params: #{params.inspect}") 29 | # end 30 | #~meths end 31 | end 32 | end 33 | #~step end 34 | #~left end 35 | #:railway end 36 | 37 | # it "what" do 38 | # ctx = {params: {text: "Hydrate!"}, create: true} 39 | # signal, (ctx, _flow_options) = D::Create.([ctx, {}]) 40 | # end 41 | end 42 | end 43 | 44 | 45 | module B 46 | class Fail_DocsStepTest < Minitest::Spec 47 | Memo = Module.new 48 | 49 | #:fail 50 | module Memo::Operation 51 | class Create < Trailblazer::Operation 52 | step :validate 53 | step :save 54 | fail :handle_errors # just like {#left} 55 | #~meths 56 | step :notify 57 | include T.def_steps(:validate, :save, :handle_errors, :notify) 58 | #~meths end 59 | end 60 | end 61 | #:fail end 62 | 63 | it "what" do 64 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :notify]" 65 | assert_invoke Memo::Operation::Create, validate: false, terminus: :failure, seq: "[:validate, :handle_errors]" 66 | end 67 | end 68 | end 69 | 70 | module A 71 | class Pass_DocsStepTest < Minitest::Spec 72 | Memo = Module.new 73 | 74 | #:pass 75 | module Memo::Operation 76 | class Create < Trailblazer::Operation 77 | step :validate 78 | pass :save # no output goes to the failure track here. 79 | left :handle_errors 80 | #~meths 81 | step :notify 82 | include T.def_steps(:validate, :save, :handle_errors, :notify) 83 | #~meths end 84 | end 85 | end 86 | #:pass end 87 | 88 | it "what" do 89 | assert_invoke Memo::Operation::Create, seq: "[:validate, :save, :notify]" 90 | assert_invoke Memo::Operation::Create, validate: false, terminus: :failure, seq: "[:validate, :handle_errors]" 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/operation_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | # Tests around {Operation.call}. 4 | class OperationTest < Minitest::Spec 5 | require "trailblazer/operation/testing" 6 | include Trailblazer::Operation::Testing::Assertions 7 | 8 | def assert_aliasing(result) 9 | assert_equal result.success?, true 10 | assert_equal result[:params], {id: 1} 11 | assert_equal result[:parameters], {id: 1} 12 | end 13 | 14 | it "canonical invoke using {Operation.__}" do 15 | # Trailblazer::Operation.()# calls { }. 16 | operation_class = Trailblazer::Operation 17 | 18 | result = operation_class.(params: {id: 1}) 19 | 20 | # {Operation.__} returns circuit-interface return set. 21 | signal, (result, _) = operation_class.__(operation_class, {params: {id: 1}}) 22 | 23 | assert_equal signal.to_h[:semantic], :success 24 | 25 | stdout, _ = capture_io do 26 | signal, (result, _) = operation_class.__?(operation_class, {params: {id: 1}}) 27 | end 28 | 29 | assert_equal CU.strip(stdout), %(Trailblazer::Operation 30 | |-- \e[32mStart.default\e[0m 31 | `-- End.success\n) 32 | end 33 | 34 | it "canonical invoke #__ allows a second argument and accepts the {:invoke_method} option" do 35 | operation_class = Trailblazer::Operation 36 | signal, result = nil 37 | 38 | stdout, _ = capture_io do 39 | signal, (result, _) = operation_class.__(operation_class, {params: {id: 1}}, invoke_method: Trailblazer::Developer::Wtf.method(:invoke)) 40 | end 41 | 42 | assert_equal signal.to_h[:semantic], :success 43 | assert_equal CU.strip(stdout), %(Trailblazer::Operation 44 | |-- \e[32mStart.default\e[0m 45 | `-- End.success\n) 46 | end 47 | 48 | it "we can use public call" do 49 | result = Trailblazer::Operation.({seq: []}) 50 | 51 | assert_equal result.success?, true 52 | assert_equal result.instance_variable_get(:@data).class, Trailblazer::Context::Container#::WithAliases 53 | end 54 | 55 | it "we can use the circuit-interface and inject options like {:runner}" do 56 | # Internally, TaskWrap::Runner.call_task invokes the circuit-interface. 57 | signal, (ctx, _) = Trailblazer::Activity::TaskWrap.invoke(Trailblazer::Operation, [{id: 1}, {}]) 58 | 59 | assert_equal signal.to_h[:semantic], :success 60 | assert_equal ctx.class, Hash # because canonical invoke is not called. 61 | end 62 | 63 | # test that circuit-interface doesn't use dynamic args / (e.g. aliasing) 64 | it "circuit-interface doesn't use dynamic args from {configure!}" do 65 | operation_class = Class.new(Trailblazer::Operation) 66 | operation_class.configure! do 67 | { 68 | flow_options: { 69 | context_options: { 70 | aliases: { "seq" => :sequence }, 71 | container_class: Trailblazer::Context::Container::WithAliases, 72 | } 73 | } 74 | } 75 | end 76 | 77 | result = operation_class.({seq: []}) 78 | assert_equal result[:sequence], [] # with public_call, we use {configure!} and can see the alias. 79 | 80 | signal, (ctx, _) = Trailblazer::Activity::TaskWrap.invoke(Trailblazer::Operation, [{seq: []}, {}]) 81 | 82 | assert_equal ctx.class, Hash 83 | assert_equal ctx[:seq], [] 84 | assert_nil ctx[:sequence] 85 | end 86 | 87 | def self.flow_options_with_aliasing 88 | { 89 | context_options: { 90 | aliases: {"seq" => :sequence}, 91 | container_class: Trailblazer::Context::Container::WithAliases, 92 | } 93 | } 94 | end 95 | 96 | # test Op.wtf? 97 | it "{Operation.wtf?}" do 98 | operation_class = Class.new(Trailblazer::Operation) 99 | operation_class.configure! do 100 | { 101 | flow_options: OperationTest.flow_options_with_aliasing 102 | } 103 | end 104 | signal, result = nil 105 | 106 | stdout, _ = capture_io do 107 | result = operation_class.wtf?({seq: []}) 108 | end 109 | 110 | assert_equal CU.strip(stdout), %(# 111 | |-- \e[32mStart.default\e[0m 112 | `-- End.success\n) 113 | assert_equal result[:sequence], [] # aliasing works. 114 | assert_equal result.instance_variable_get(:@data).class, Trailblazer::Context::Container::WithAliases 115 | end 116 | # test matcher block interface 117 | 118 | # TODO: test overriding configure! options etc in subclasses 119 | it "inheritance: configure! can be overridden per class" do 120 | operation_class_1 = Class.new(Trailblazer::Operation) 121 | operation_class_1.configure! { {flow_options: OperationTest.flow_options_with_aliasing} } 122 | 123 | # override configure 124 | operation_class_2 = Class.new(operation_class_1) 125 | operation_class_2.configure! { {} } 126 | 127 | # inherit configure 128 | operation_class_3 = Class.new(operation_class_1) 129 | 130 | 131 | result = Trailblazer::Operation.(seq: {id: 1}) 132 | result_1 = operation_class_1.(seq: {id: 1}) 133 | result_2 = operation_class_2.(seq: {id: 1}) 134 | result_3 = operation_class_3.(seq: {id: 1}) 135 | 136 | assert_equal result.success?, true 137 | assert_equal result.keys.inspect, "[:seq]" 138 | assert_equal result_1.success?, true 139 | assert_equal result_1.keys.inspect, "[:seq, :sequence]" 140 | assert_equal result_2.success?, true 141 | assert_equal result_2.keys.inspect, "[:seq]" 142 | assert_equal result_3.success?, true 143 | assert_equal result_3.keys.inspect, "[:seq, :sequence]" 144 | end 145 | 146 | it "Operation.call accepts block matcher interface" do 147 | my_operation = Class.new(Trailblazer::Operation) do 148 | step :model 149 | include T.def_steps(:model) 150 | end 151 | 152 | @render = nil 153 | 154 | result = my_operation.(seq: []) do 155 | success { |ctx, seq:, **| @render = "success! #{seq}" } 156 | failure { |ctx, seq:, **| @render = "failure! #{seq}" } 157 | end 158 | 159 | assert_equal @render, %(success! [:model]) 160 | end 161 | 162 | 163 | class Noop < Trailblazer::Operation 164 | def self.capture_circuit_options((ctx, flow_options), **circuit_options) 165 | ctx[:capture_circuit_options] = circuit_options.keys.inspect 166 | 167 | return Trailblazer::Activity::Right, [ctx, flow_options] 168 | end 169 | 170 | step task: method(:capture_circuit_options) 171 | end 172 | 173 | # Mixing keywords and string keys in {Operation.call}. 174 | # Test that {.(params: {}, "current_user" => user)} is processed properly 175 | 176 | it "doesn't mistake circuit options as ctx variables when using circuit-interface" do 177 | signal, (ctx, _) = Noop.call( 178 | [{params: {}}, {}], 179 | # real circuit_options 180 | variable_for_circuit_options: true 181 | ) # call_with_public_interface 182 | #@ {:variable_for_circuit_options} is not supposed to be in {ctx}. 183 | assert_equal CU.inspect(ctx), %({:params=>{}, :capture_circuit_options=>"[:variable_for_circuit_options, :exec_context, :activity, :runner]"}) 184 | end 185 | 186 | it "doesn't mistake circuit options as ctx variables when using the call interface" do 187 | result = Noop.call( 188 | params: {}, 189 | model: true, 190 | "current_user" => Object 191 | ) # call with public interface. 192 | #@ {:variable_for_circuit_options} is not supposed to be in {ctx}. 193 | 194 | assert_equal result.to_h, {params: {}, model: true, current_user: Object, capture_circuit_options: "[:exec_context, :wrap_runtime, :activity, :runner]"} 195 | end 196 | 197 | describe "{Operation.call} is not called twice" do 198 | let(:operation) do 199 | Class.new(Trailblazer::Operation) do 200 | class << self 201 | def global; @GLOBAL; end 202 | def global=(v); @GLOBAL = v; end 203 | end 204 | self.global= [] 205 | 206 | def self.call(*args) 207 | global << :call 208 | super 209 | end 210 | 211 | pass :model 212 | 213 | def model(ctx, **) 214 | self.class.global << :model 215 | end 216 | end 217 | end 218 | 219 | it "doesn't invoke {Operation.call} twice when using public interface" do 220 | operation.({}) 221 | assert_equal operation.global.inspect, %{[:call, :model]} 222 | end 223 | 224 | it "{Operation.call} is obviously invoked when using canonical invoke #__()" do 225 | kernel = Class.new { Trailblazer::Invoke.module!(self) }.new 226 | 227 | # don't invoke call twice when going through canonical invoke. 228 | result = Trailblazer::Operation.__(operation, {}) 229 | 230 | assert_equal operation.global.inspect, %{[:call, :model]} 231 | end 232 | 233 | it "isn't invoked twice on nested Operation, either" do 234 | operation = self.operation 235 | 236 | parent = Class.new(operation) do 237 | step Subprocess(operation)#, 238 | # without this option {:initial_task_wrap} set, we use TaskWrap::INITIAL_TASK_WRAP with call_task.*call* 239 | # change it to *call_with_circuit_interface* 240 | # initial_task_wrap: Trailblazer::Activity::TaskWrap::Pipeline.new(operation.to_h[:fields][:task_wrap]) 241 | end 242 | parent.global= [] 243 | 244 | parent.({}) 245 | 246 | assert_equal operation.global.inspect, %{[:call, :model]} 247 | assert_equal parent.global.inspect, %{[:call, :model]} 248 | end 249 | end 250 | 251 | it "{Operation.call} invokes with the taskWrap" do 252 | def add_1(wrap_ctx, original_args) 253 | ctx, = original_args[0] 254 | ctx[:seq] << 1 255 | 256 | return wrap_ctx, original_args # yay to mutable state. not. 257 | end 258 | 259 | add_1_method = method(:add_1) 260 | 261 | operation = Class.new(Trailblazer::Operation) do 262 | include T.def_steps(:model) 263 | 264 | step :model, 265 | Extension() => Trailblazer::Activity::TaskWrap::Extension.WrapStatic( 266 | [add_1_method, prepend: "task_wrap.call_task", id: "user.add_1"] 267 | ) 268 | end 269 | 270 | # normal operation invocation 271 | assert_call operation, seq: "[1, :model]" 272 | 273 | result = nil 274 | # with tracing 275 | stdout, _ = capture_io do 276 | result = operation.wtf?(seq: []) 277 | end 278 | 279 | assert_equal CU.strip(stdout), %(# 280 | |-- \e[32mStart.default\e[0m 281 | |-- \e[32mmodel\e[0m 282 | `-- End.success 283 | ) 284 | assert_equal CU.inspect(result.to_h), %({:seq=>[1, :model]}) 285 | assert_equal result.terminus.to_h[:semantic], :success 286 | 287 | # with circuit-interface and {:wrap_runtime}. 288 | my_runtime_extension = Trailblazer::Activity::TaskWrap::Extension( 289 | [add_1_method, id: "my.add_1", append: "task_wrap.call_task"] 290 | ) 291 | 292 | # circuit interface invocation using call 293 | signal, (ctx, _) = operation.( 294 | [{seq: []}, {}], 295 | wrap_runtime: Hash.new(my_runtime_extension), 296 | runner: Trailblazer::Activity::TaskWrap::Runner 297 | ) 298 | 299 | assert_equal signal.to_h[:semantic], :success 300 | assert_equal CU.inspect(ctx), %({:seq=>[1, 1, :model, 1, 1]}) # {Start.default} is a step, too :D 301 | end 302 | 303 | it "{Operation.call} works with operations that expose public {:normalizer_task_wrap_extensions}" do 304 | operation = Class.new(Trailblazer::Operation) do 305 | # This usually happens in extensions such as {trailblazer-dependency}. 306 | def self.adds_instruction(task_wrap, id: nil, **) 307 | Trailblazer::Activity::TaskWrap::Extension( 308 | # Return an ADDS instruction. 309 | [ 310 | ->(wrap_ctx, original_args) { original_args[0][0][:tw] = "hello from taskWrap #{id.inspect}"; return wrap_ctx, original_args }, 311 | id: "xxx", 312 | prepend: nil 313 | ] 314 | ).(task_wrap) 315 | end 316 | 317 | ext = method(:adds_instruction) 318 | 319 | @state.update!(:fields) do |fields| 320 | exts = fields[:task_wrap_extensions] # [call_task] 321 | exts = exts + [ext] 322 | fields.merge(task_wrap_extensions: exts) 323 | end 324 | end 325 | 326 | # We can inject options when using canonical invoke. 327 | signal, (ctx, flow_options) = Trailblazer::Operation.__(operation, {}, id: "tw ID xxx") 328 | assert_equal CU.inspect(ctx.to_h), %({:tw=>\"hello from taskWrap \\\"tw ID xxx\\\"\"}) 329 | 330 | # ...with public interface, that's not possible. 331 | result = operation.() 332 | assert_equal CU.inspect(result.to_h), %({:tw=>\"hello from taskWrap nil\"}) 333 | end 334 | end 335 | -------------------------------------------------------------------------------- /test/result_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class RailwayResultTest < Minitest::Spec 4 | Result = Trailblazer::Operation::Railway::Result 5 | Success = Trailblazer::Operation::Railway::End::Success 6 | 7 | let(:terminus) { Success.new(semantic: nil) } 8 | let(:success) { Result.new(true, {"x" => String}, terminus) } 9 | 10 | it { assert_equal success.success?, true } 11 | it { assert_equal success.failure?, false } 12 | it { assert_equal success.terminus, terminus } 13 | 14 | it { assert_equal success["x"], String } 15 | it { assert_nil success["not-existant"] } 16 | it { assert_equal success.to_h, {"x"=>String} } 17 | it { assert_equal success.keys, ["x"] } 18 | end 19 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | 3 | require "pp" 4 | require "trailblazer/operation" 5 | 6 | require "trailblazer/activity/testing" 7 | require "trailblazer/developer/render/linear" 8 | require "trailblazer/core" 9 | 10 | Minitest::Spec.class_eval do 11 | T = Trailblazer::Activity::Testing 12 | include Trailblazer::Activity::Testing::Assertions 13 | CU = Trailblazer::Core::Utils 14 | 15 | def assert_equal(asserted, expected, *args) 16 | super(expected, asserted, *args) 17 | end 18 | end 19 | 20 | # TODO: replace all this with {Activity::Testing.def_steps} 21 | module Test 22 | # Create a step method in `klass` with the following body. 23 | # 24 | # def a(options, a_return:, data:, **) 25 | # data << :a 26 | # 27 | # a_return 28 | # end 29 | def self.step(klass, *names) 30 | names.each do |name| 31 | method_def = 32 | %{def #{name}(options, #{name}_return:, data:, **) 33 | data << :#{name} 34 | #{name}_return 35 | end} 36 | 37 | klass.class_eval(method_def) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/trace_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class TraceTest < Minitest::Spec 4 | class B < Trailblazer::Operation 5 | step ->(options, **) { options[:b] = true }, id: "B.task.b" 6 | step ->(options, **) { options[:e] = true }, id: "B.task.e" 7 | end 8 | 9 | class Create < Trailblazer::Operation 10 | step ->(options, a_return:, **) { options[:a] = a_return }, id: "Create.task.a" 11 | step Subprocess(B), id: "MyNested" 12 | step ->(options, **) { options[:c] = true }, id: "Create.task.c" 13 | step ->(_options, params:, **) { params.any? }, id: "Create.task.params" 14 | end 15 | 16 | it "allows using low-level Operation::Trace" do 17 | stack, result = Trailblazer::Developer::Trace.( 18 | Create, 19 | { a_return: true, params: {} }, 20 | ) 21 | 22 | output = Trailblazer::Developer::Trace::Present.(stack) 23 | 24 | assert_equal output.gsub(/0x\w+/, "").gsub(/@.+_test/, ""), %{TraceTest::Create 25 | |-- Start.default 26 | |-- Create.task.a 27 | |-- MyNested 28 | | |-- Start.default 29 | | |-- B.task.b 30 | | |-- B.task.e 31 | | `-- End.success 32 | |-- Create.task.c 33 | |-- Create.task.params 34 | `-- End.failure} 35 | end 36 | 37 | it "Operation.wtf?" do 38 | result = nil 39 | output, = capture_io do 40 | result = Create.wtf?(params: {x: 1}, a_return: true) 41 | end 42 | 43 | assert_equal output.gsub(/0x\w+/, "").gsub(/@.+_test/, ""), %{TraceTest::Create 44 | |-- \e[32mStart.default\e[0m 45 | |-- \e[32mCreate.task.a\e[0m 46 | |-- MyNested 47 | | |-- \e[32mStart.default\e[0m 48 | | |-- \e[32mB.task.b\e[0m 49 | | |-- \e[32mB.task.e\e[0m 50 | | `-- End.success 51 | |-- \e[32mCreate.task.c\e[0m 52 | |-- \e[32mCreate.task.params\e[0m 53 | `-- End.success 54 | } 55 | 56 | result.success?.must_equal true 57 | result[:a_return].must_equal true 58 | assert_equal CU.inspect(result[:params]), %({:x=>1}) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /trailblazer-operation.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'trailblazer/operation/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "trailblazer-operation" 7 | spec.version = Trailblazer::Version::Operation::VERSION 8 | spec.authors = ["Nick Sutterer"] 9 | spec.email = ["apotonick@gmail.com"] 10 | spec.description = %q(Trailblazer's operation object.) 11 | spec.summary = %q(Trailblazer's operation object with railway flow and integrated error handling.) 12 | spec.homepage = "https://trailblazer.to/2.1/docs/operation.html" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files`.split($/) 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test)/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_dependency "trailblazer-activity-dsl-linear", ">= 1.2.3", "< 1.4.0" 21 | spec.add_dependency "trailblazer-developer", ">= 0.1.0", "< 0.2.0" 22 | spec.add_dependency "trailblazer-invoke" 23 | 24 | spec.add_development_dependency "bundler" 25 | spec.add_development_dependency "minitest" 26 | spec.add_development_dependency "minitest-line" 27 | spec.add_development_dependency "rake" 28 | spec.add_development_dependency "trailblazer-core-utils", ">= 0.0.6" 29 | 30 | spec.required_ruby_version = ">= 2.5.0" 31 | end 32 | --------------------------------------------------------------------------------