Module: Trailblazer::Macro

Defined in:
lib/trailblazer/macro.rb,
lib/trailblazer/macro/each.rb,
lib/trailblazer/macro/wrap.rb,
lib/trailblazer/macro/guard.rb,
lib/trailblazer/macro/model.rb,
lib/trailblazer/macro/nested.rb,
lib/trailblazer/macro/policy.rb,
lib/trailblazer/macro/pundit.rb,
lib/trailblazer/macro/rescue.rb,
lib/trailblazer/macro/strategy.rb,
lib/trailblazer/macro/model/find.rb

Defined Under Namespace

Modules: IdFor, Policy, Rescue Classes: AssignVariable, Each, Model, Nested, Strategy, Wrap

Constant Summary collapse

NoopHandler =
lambda { |*| }

Class Method Summary collapse

Class Method Details

.block_activity_for(block_activity, &block) ⇒ Object



46
47
48
49
50
51
52
53
# File 'lib/trailblazer/macro.rb', line 46

def self.block_activity_for(block_activity, &block)
  return block_activity, block_activity.to_h[:outputs] unless block_given?

  block_activity = Class.new(Activity::FastTrack, &block) # TODO: use Wrap() logic!
  block_activity.extend Each::Transitive

  return block_activity, block_activity.to_h[:outputs]
end

.Each(block_activity = nil, dataset_from: nil, item_key: :item, id: Macro.id_for(block_activity, macro: :Each, hint: dataset_from), collect: false, **dsl_options_for_iterated, &block) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/trailblazer/macro/each.rb', line 85

def self.Each(block_activity=nil, dataset_from: nil, item_key: :item, id: Macro.id_for(block_activity, macro: :Each, hint: dataset_from), collect: false, **dsl_options_for_iterated, &block)
  dsl_options_for_iterated = block_activity if block_activity.is_a?(Hash) # Ruby 2.5 and 2.6

  block_activity, outputs_from_block_activity = Macro.block_activity_for(block_activity, &block)

  collect_options       = options_for_collect(collect: collect)
  dataset_from_options  = options_for_dataset_from(dataset_from: dataset_from)

  wrap_static_for_block_activity = task_wrap_for_iterated(
    {Activity::Railway.Out() => []}. # per default, don't let anything out.
    merge(collect_options).
    merge(dsl_options_for_iterated)
  )

  # This activity is passed into the {Runner} for each iteration of {block_activity}.
  container_activity = Activity::TaskWrap.container_activity_for(
    block_activity,
    id:        "invoke_block_activity",
    # merged into {:config}:
      each:        true, # mark this activity for {compute_runtime_id}.
  ).merge(
    outputs: outputs_from_block_activity,
  )

  # FIXME: we can't pass {wrap_static: wrap_static_for_block_activity} into {#container_activity_for}
  #        because when patching, the container_activity is not recompiled, so we need the Hash here
  #        with defaulting.
  # FIXME: this "hack" is only here to satify patching.
  config = container_activity[:config].merge(wrap_static: Hash.new(wrap_static_for_block_activity))
  container_activity.merge!(config: config)

  # DISCUSS: move to Wrap.
  termini_from_block_activity =
    outputs_from_block_activity.
      # DISCUSS: End.success needs to be the last here, so it's directly behind {Start.default}.
      sort { |a,b| a.semantic == :success ? 1 : -1 }.
      collect { |output|
        [output.signal, id: "End.#{output.semantic}", magnetic_to: output.semantic, append_to: "Start.default"]
      }

  state = Declarative::State(
    block_activity:   [block_activity, {copy: Trailblazer::Declarative::State.method(:subclass)}], # DISCUSS: move to Macro::Strategy.
    item_key:         [item_key, {}], # DISCUSS: we could even allow the wrap_handler to be patchable.
    failing_semantic: [[:failure, :fail_fast], {}],
    activity:         [container_activity, {}],
    success_signal:   [termini_from_block_activity[-1][0], {}] # FIXME: when subclassing (e.g. patching) this must be recomputed.
  )

  # |-- Each/composers_for_each
  # |   |-- Start.default
  # |   |-- Each.iterate.block            This is Class.new(Each), outputs_from_block_activity
  # |   |   |-- invoke_block_activity.0      step :invoke_block_activity.0
  # |   |   |   |-- Start.default
  # |   |   |   |-- notify_composers
  # |   |   |   `-- End.success
  # |   |   `-- invoke_block_activity.1      step "invoke_block_activity.1"
  # |   |       |-- Start.default
  # |   |       |-- notify_composers
  iterate_strategy = Class.new(Each) do
    extend Macro::Strategy::State # now, the Wrap subclass can inherit its state and copy the {block_activity}.
    initialize!(state)
  end

  each_activity = Activity::FastTrack(termini: termini_from_block_activity) # DISCUSS: what base class should we be using?
  each_activity.extend Each::Transitive

  # {Subprocess} with {strict: true} will automatically wire all {block_activity}'s termini to the corresponding termini
  # of {each_activity} as they have the same semantics (both termini sets are identical).
  each_activity.step Activity::Railway.Subprocess(iterate_strategy, strict: true),
    id: "Each.iterate.#{block ? :block : block_activity}" # FIXME: test :id.

  Activity::Railway.Subprocess(each_activity).
    merge(id: id).
    merge(dataset_from_options) # FIXME: provide that service via Subprocess.
end

.id_for(user_proc, **options) ⇒ Object



73
74
75
# File 'lib/trailblazer/macro.rb', line 73

def self.id_for(user_proc, **options)
  IdFor.(user_proc, **options)
end

.Model(model_class = nil, action = :new, find_by_key = :id, id: 'model.build', not_found_terminus: false) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/trailblazer/macro/model.rb', line 4

def self.Model(model_class = nil, action = :new, find_by_key = :id, id: 'model.build', not_found_terminus: false)
  task = Activity::Circuit::TaskAdapter.for_step(Model.new)

  injections = {
    Activity::Railway.Inject() => [:params], # pass-through {:params} if it's in ctx.

    # defaulting as per Inject() API.
    Activity::Railway.Inject() => {
      :"model.class"          => ->(*) { model_class },
      :"model.action"         => ->(*) { action },
      :"model.find_by_key"    => ->(*) { find_by_key },
    }
  }

  options = {task: task, id: id}.merge(injections)

  options = options.merge(Activity::Railway.Output(:failure) => Activity::Railway.End(:not_found)) if not_found_terminus

  options
end

.Nested(callable, id: Macro.id_for(callable, macro: :Nested, hint: callable), auto_wire: []) ⇒ Object

Nested macro. DISCUSS: rename auto_wire => static



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/trailblazer/macro/nested.rb', line 5

def self.Nested(callable, id: Macro.id_for(callable, macro: :Nested, hint: callable), auto_wire: [])
  # Warn developers when they confuse Nested with Subprocess (for simple nesting, without a dynamic decider).
  if callable.is_a?(Class) && callable < Nested.operation_class
    caller_locations = caller_locations(1, 2)
    caller_location = caller_locations[0].to_s =~ /forwardable/ ? caller_locations[1] : caller_locations[0]

    Activity::Deprecate.warn caller_location,
      "Using the `Nested()` macro without a dynamic decider is deprecated.\n" \
      "To simply nest an activity or operation, replace `Nested(#{callable})` with `Subprocess(#{callable})`.\n" \
      "Check the Subprocess API docs to learn more about nesting: https://trailblazer.to/2.1/docs/activity.html#activity-wiring-api-subprocess"

    return Activity::Railway.Subprocess(callable)
  end

  task =
    if auto_wire.any?
      Nested.Static(callable, id: id, auto_wire: auto_wire)
    else # no {auto_wire}
      Nested.Dynamic(callable, id: id)
    end

  # |-- Nested.compute_nested_activity...Trailblazer::Macro::Nested::Decider
  # `-- task_wrap.call_task..............Method
  merge = [
    [Nested::Decider.new(callable), id: "Nested.compute_nested_activity", prepend: "task_wrap.call_task"],
  ]

  task_wrap_extension = Activity::TaskWrap::Extension::WrapStatic.new(extension: Activity::TaskWrap::Extension(*merge))

  Activity::Railway.Subprocess(task).merge( # FIXME: allow this directly in Subprocess
    id:         id,
    extensions: [task_wrap_extension],
  )
end

.options_for_collect(collect:) ⇒ Object

DSL options added to block_activity to implement true.



171
172
173
174
175
176
177
178
# File 'lib/trailblazer/macro/each.rb', line 171

def self.options_for_collect(collect:)
  return {} unless collect

  {
    Activity::Railway.Inject(:collected_from_each) => ->(ctx, **) { [] }, # this is called only once.
    Activity::Railway.Out() => ->(ctx, collected_from_each:, **) { {collected_from_each: collected_from_each += [ctx[:value]] } }
  }
end

.options_for_dataset_from(dataset_from:) ⇒ Object



180
181
182
183
184
185
186
# File 'lib/trailblazer/macro/each.rb', line 180

def self.options_for_dataset_from(dataset_from:)
  return {} unless dataset_from

  {
    Activity::Railway.Inject(:dataset, override: true) => dataset_from, # {ctx[:dataset]} is private to {each_activity}.
  }
end

.Rescue(*exceptions, handler: NoopHandler, id: Rescue.random_id, &block) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/trailblazer/macro/rescue.rb', line 5

def self.Rescue(*exceptions, handler: NoopHandler, id: Rescue.random_id, &block)
  exceptions = [StandardError] unless exceptions.any?

  handler    = Rescue.deprecate_positional_handler_signature(handler)
  handler    = Trailblazer::Option(handler)

  # This block is evaluated by {Wrap}.
  rescue_block = ->((ctx, flow_options), **circuit_options, &nested_activity) do
    begin
      nested_activity.call
    rescue *exceptions => exception
      # DISCUSS: should we deprecate this signature and rather apply the Task API here?
      handler.call(exception, ctx, **circuit_options) # FIXME: when there's an error here, it shows the wrong exception!

      [Operation::Railway.fail!, [ctx, flow_options]]
    end
  end

  Wrap(rescue_block, id: id, &block)
end

.task_adapter_for_decider(decider_with_step_interface, variable_name:) ⇒ Object



38
39
40
41
42
43
44
# File 'lib/trailblazer/macro.rb', line 38

def self.task_adapter_for_decider(decider_with_step_interface, variable_name:)
  return_value_circuit_step = Activity::Circuit.Step(decider_with_step_interface, option: true)

  assign_task = AssignVariable.new(return_value_circuit_step, variable_name: variable_name)

  Activity::Circuit::TaskAdapter.new(assign_task) # call {assign_task} with circuit-interface, interpret result.
end

.task_wrap_for_iterated(dsl_options) ⇒ Object



161
162
163
164
165
166
167
168
# File 'lib/trailblazer/macro/each.rb', line 161

def self.task_wrap_for_iterated(dsl_options)
  # TODO: maybe the DSL API could be more "open" here? I bet it is, but I'm too lazy.
  activity = Class.new(Activity::Railway) do
    step({task: "iterated"}.merge(dsl_options))
  end

  activity.to_h[:config][:wrap_static]["iterated"]
end

.Wrap(user_wrap, id: Macro.id_for(user_wrap, macro: :Wrap), &block) ⇒ Object

TODO: user_wrap: rename to wrap_handler.



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/trailblazer/macro/wrap.rb', line 4

def self.Wrap(user_wrap, id: Macro.id_for(user_wrap, macro: :Wrap), &block)
  user_wrap = Wrap.deprecate_positional_wrap_signature(user_wrap)

  block_activity, outputs = Macro.block_activity_for(nil, &block)

  outputs   = Hash[outputs.collect { |output| [output.semantic, output] }] # FIXME: redundant to Subprocess().

  # Since in the user block, you can return Railway.pass! etc, we need to map
  # those to the actual wrapped block_activity's end.
  signal_to_output = {
    Activity::Right               => outputs[:success].signal,
    Activity::Left                => outputs[:failure].signal,
    Activity::FastTrack::PassFast => outputs[:pass_fast].signal,
    Activity::FastTrack::FailFast => outputs[:fail_fast].signal,
    true               => outputs[:success].signal,
    false              => outputs[:failure].signal,
    nil                => outputs[:failure].signal,
  }

  state = Declarative::State(
    # this is important, so we subclass the actually wrapped activity when {Wrap} is subclassed.
    block_activity:   [block_activity, {copy: Trailblazer::Declarative::State.method(:subclass)}],
    user_wrap:        [user_wrap, {}], # DISCUSS: we could even allow the wrap_handler to be patchable.
    signal_to_output: [signal_to_output, {}],
  )

  task = Class.new(Wrap) do
    extend Macro::Strategy::State # now, the Wrap subclass can inherit its state and copy the {block_activity}.
    initialize!(state)
  end
  # DISCUSS: unfortunately, Ruby doesn't allow to set this during {Class.new}.

  {
    task:     task,
    id:       id,
    outputs:  outputs,
  }
end