trailblazer-macro-contract 2.1.0 → 2.1.3.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3603305c22954cb6b53e94057b4b25c4ae98d1286d5a32b0b0a2292325156086
4
- data.tar.gz: 952c4060256225918cfbd41939f2b6fb479cd153b4416967f8ef03682033cb42
3
+ metadata.gz: c9c3c0092a4bd1f8b71d4506ed24e77af326b28d2991a88bdb45d67586dad746
4
+ data.tar.gz: 6de99c14b4bc553a0e32e568a9c31a74abf839f0d82f36dac33884300d979777
5
5
  SHA512:
6
- metadata.gz: 2f1118cf2e91af6c8a95c9c7050ae6bf43a82ebe7fca82b15255b53f7ace0c3a46e76a02efcaaa94195d6003fdba8c4acaeeb8fb30580c7eb09a5c765b44c690
7
- data.tar.gz: 55ee53d0e937672aee52552167ad987f4c36f99f669966dd5f2a417c9bfc047c897b6981b4eebceb4dc1e9e1136c0adfa6efb5fea962e1a860abbc2edd01cd51
6
+ metadata.gz: 15de139140c545de9ab6d1b6d19a83053815b78b7e4ae990a9b5a57dea89e77566524d2eb84326694f5399f41d3d7f367a761572badf465fdaa58cd32c2e1a79
7
+ data.tar.gz: 4b9fc7eb3a4f30a7f57f2ec95c6b0459ad5ccff8e4adbfd3ddc324570d8647b1046277e61f7f1b402bbbb663e28fbe3b1161d649db5036e74c91da66b9e6b8c1
@@ -0,0 +1,17 @@
1
+ name: CI
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ strategy:
6
+ fail-fast: false
7
+ matrix:
8
+ # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
9
+ ruby: [2.6, 2.7, '3.0', jruby]
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v2
13
+ - uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: ${{ matrix.ruby }}
16
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
17
+ - run: bundle exec rake
data/CHANGES.md CHANGED
@@ -1,3 +1,18 @@
1
+ # 2.1.3
2
+
3
+ * Use `trailblazer-activity-dsl-linear` >= 1.0.0.
4
+ * Use Inject() API instead of `:inject`, `:input` etc in macros.
5
+
6
+ # 2.1.2
7
+
8
+ * Refactor `Contract::Build` to use TRB mechanics:
9
+ * `:input` and `:inject` to allow injection of the contract class.
10
+ * an `Option()` to wrap the builder code.
11
+
12
+ # 2.1.1
13
+
14
+ * Support for Ruby 3.0.
15
+
1
16
  # 2.1.0
2
17
 
3
18
  * Finally.
data/Gemfile CHANGED
@@ -9,6 +9,8 @@ gem "dry-matcher"
9
9
  # gem "trailblazer-macro", path: "../trailblazer-macro"
10
10
  # gem "trailblazer-activity", path: "../trailblazer-activity"
11
11
  # gem "trailblazer-activity-dsl-linear", path: "../trailblazer-activity-dsl-linear"
12
+ # gem "trailblazer-errors", path: "../trailblazer-errors"
13
+ # gem "trailblazer-developer", path: "../trailblazer-developer"
12
14
 
13
15
  gem "minitest-line"
14
16
 
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
3
  require "rubocop/rake_task"
4
4
 
5
- task :default => %i[test rubocop]
5
+ task :default => %i[test]
6
6
 
7
7
  Rake::TestTask.new(:test) do |test|
8
8
  test.libs << 'test'
@@ -0,0 +1,87 @@
1
+ require "reform"
2
+
3
+ module Trailblazer
4
+ module Macro
5
+ # This Circuit-task calls the {task} Option, then allows
6
+ # to run an arbitary block to process the option's result.
7
+ # @private
8
+ class CircuitTaskWithResultProcessing < Activity::TaskBuilder::Task # DISCUSS: extract to public?
9
+ def initialize(task, user_proc, block)
10
+ @block = block
11
+ super(task, user_proc)
12
+ end
13
+
14
+ def call_option(task_with_option_interface, (ctx, flow_options), **circuit_options)
15
+ result = super
16
+
17
+ @block.call(result, ctx)
18
+ end
19
+ end
20
+
21
+ module Contract
22
+ def self.Build(name: "default", constant: nil, builder: nil)
23
+ contract_path = :"contract.#{name}"
24
+
25
+ injections = {
26
+ Activity::Railway.Inject() => {
27
+ "#{contract_path}.class": ->(*) { constant }, # default to {constant} if not injected.
28
+ }
29
+ }
30
+
31
+ # DISCUSS: can we force-default this via Inject()?
32
+ input = {
33
+ Activity::Railway.In() => ->(ctx, **) do
34
+ ctx.to_hash.merge(
35
+ constant: constant,
36
+ name: contract_path
37
+ )
38
+ end
39
+ }
40
+
41
+ output = {
42
+ Activity::Railway.Out() => [contract_path]
43
+ }
44
+
45
+ default_contract_builder = ->(ctx, model: nil, **) { ctx[:"#{contract_path}.class"].new(model) }
46
+
47
+ # proc is called via {Option()}.
48
+ task_option_proc = builder ? builder : default_contract_builder
49
+
50
+ # after the builder proc is run, assign its result to {:"contract.default"}.
51
+ ctx_assign_block = ->(result, ctx) { ctx[contract_path] = result }
52
+
53
+ task = CircuitTaskWithResultProcessing.new(Trailblazer::Option(task_option_proc), task_option_proc, ctx_assign_block)
54
+
55
+ {
56
+ task: task, id: "contract.build",
57
+ }.
58
+ merge(injections).
59
+ merge(input).
60
+ merge(output)
61
+ end
62
+
63
+ module DSL
64
+ def self.extended(extender)
65
+ extender.extend(ClassDependencies)
66
+ warn "[Trailblazer] Using `contract do...end` is deprecated. Please use a form class and the Builder( constant: <Form> ) option."
67
+ end
68
+
69
+ # This is the class level DSL method.
70
+ # Op.contract #=> returns contract class
71
+ # Op.contract do .. end # defines contract
72
+ # Op.contract CommentForm # copies (and subclasses) external contract.
73
+ # Op.contract CommentForm do .. end # copies and extends contract.
74
+ def contract(name = :default, constant = nil, base: Reform::Form, &block)
75
+ heritage.record(:contract, name, constant, &block)
76
+
77
+ path, form_class = Trailblazer::DSL::Build.new.(
78
+ {prefix: :contract, class: base, container: self},
79
+ name, constant, block
80
+ )
81
+
82
+ self[path] = form_class
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,5 +1,5 @@
1
1
  module Trailblazer
2
- class Operation
2
+ module Macro
3
3
  module Contract
4
4
  def self.Persist(method: :save, name: "default")
5
5
  path = :"contract.#{name}"
@@ -0,0 +1,85 @@
1
+ module Trailblazer
2
+ module Macro
3
+ module Contract
4
+ # result.contract = {..}
5
+ # result.contract.errors = {..}
6
+ # Deviate to left track if optional key is not found in params.
7
+ # Deviate to left if validation result falsey.
8
+ def self.Validate(skip_extract: false, name: "default", representer: false, key: nil, constant: nil, invalid_data_terminus: false) # DISCUSS: should we introduce something like Validate::Deserializer?
9
+ contract_path = :"contract.#{name}" # the contract instance
10
+ params_path = :"contract.#{name}.params" # extract_params! save extracted params here.
11
+ key_path = :"contract.#{name}.extract_key"
12
+
13
+ extract = Validate::Extract.new(key_path: key_path, params_path: params_path)
14
+ validate = Validate.new(name: name, representer: representer, params_path: params_path, contract_path: contract_path)
15
+
16
+ # These are defaulting dependency injection, more here
17
+ # https://trailblazer.to/2.1/docs/activity.html#activity-dependency-injection-inject-defaulting
18
+ extract_injections = {key_path => ->(*) { key }} # default to {key} if not injected.
19
+ validate_injections = {contract_path => ->(*) { constant }} # default the contract instance to {constant}, if not injected (or passed down from {Build()})
20
+
21
+ # Build a simple Railway {Activity} for the internal flow.
22
+ activity = Class.new(Activity::Railway(name: "Contract::Validate")) do
23
+ step extract, id: "#{params_path}_extract", Output(:failure) => End(:extract_failure), Activity::Railway.Inject() => extract_injections unless skip_extract# || representer
24
+ step validate, id: "contract.#{name}.call", Activity::Railway.Inject() => validate_injections
25
+ end
26
+
27
+ options = activity.Subprocess(activity)
28
+ options = options.merge(id: "contract.#{name}.validate")
29
+
30
+ # Deviate End.extract_failure to the standard failure track as a default. This can be changed from the user side.
31
+ options = options.merge(activity.Output(:extract_failure) => activity.Track(:failure)) unless skip_extract
32
+
33
+ # Halt failure track to End with {contract.name.invalid}.
34
+ options = options.merge(activity.Output(:failure) => activity.End(:invalid_data)) if invalid_data_terminus
35
+
36
+ options
37
+ end
38
+
39
+ class Validate
40
+ # Task: extract the contract's input from params by reading `:key`.
41
+ class Extract
42
+ def initialize(key_path: nil, params_path: nil)
43
+ @key_path, @params_path = key_path, params_path
44
+ end
45
+
46
+ def call(ctx, params: {}, **)
47
+ key = ctx[@key_path] # e.g. {:song}.
48
+ ctx[@params_path] = key ? params[key] : params
49
+ end
50
+ end
51
+
52
+ def initialize(name: "default", representer: false, params_path: nil, contract_path: )
53
+ @name, @representer, @params_path, @contract_path = name, representer, params_path, contract_path
54
+ end
55
+
56
+ # Task: Validates contract `:name`.
57
+ def call(ctx, **)
58
+ validate!(
59
+ ctx,
60
+ representer: ctx[:"representer.#{@name}.class"] ||= @representer, # FIXME: maybe @representer should use DI.
61
+ params_path: @params_path
62
+ )
63
+ end
64
+
65
+ def validate!(ctx, representer: false, from: :document, params_path: nil)
66
+ contract = ctx[@contract_path] # grab contract instance from "contract.default" (usually set in {Contract::Build()})
67
+
68
+ # this is for 1.1-style compatibility and should be removed once we have Deserializer in place:
69
+ ctx[:"result.#{@contract_path}"] = result =
70
+ if representer
71
+ # use :document as the body and let the representer deserialize to the contract.
72
+ # this will be simplified once we have Deserializer.
73
+ # translates to contract.("{document: bla}") { MyRepresenter.new(contract).from_json .. }
74
+ contract.(ctx[from]) { |document| representer.new(contract).parse(document) }
75
+ else
76
+ # let Reform handle the deserialization.
77
+ contract.(ctx[params_path])
78
+ end
79
+
80
+ result.success?
81
+ end
82
+ end
83
+ end
84
+ end # Macro
85
+ end
@@ -2,7 +2,7 @@ module Trailblazer
2
2
  module Version
3
3
  module Macro
4
4
  module Contract
5
- VERSION = "2.1.0"
5
+ VERSION = "2.1.3.beta1"
6
6
  end
7
7
  end
8
8
  end
@@ -1,5 +1,17 @@
1
- require "reform"
2
- require "trailblazer/operation"
3
- require "trailblazer/operation/contract"
4
- require "trailblazer/operation/validate"
5
- require "trailblazer/operation/persist"
1
+ require "trailblazer/activity"
2
+ require "trailblazer/activity/dsl/linear"
3
+
4
+ require "trailblazer/macro/contract/build"
5
+ require "trailblazer/macro/contract/validate"
6
+ require "trailblazer/macro/contract/persist"
7
+
8
+ module Trailblazer
9
+ module Macro
10
+ module Contract
11
+ end
12
+ end
13
+
14
+ # All macros sit in the {Trailblazer::Macro::Contract} namespace, where we forward calls from
15
+ # operations and activities to.
16
+ Activity::DSL::Linear::Helper::Constants::Contract = Macro::Contract
17
+ end
@@ -57,17 +57,21 @@ class DocsContractOverviewTest < Minitest::Spec
57
57
  it "shows 2-level tracing" do
58
58
  result = Create.trace( params: { length: "A" } )
59
59
  result.wtf.gsub(/0x\w+/, "").must_equal %{`-- DocsContractOverviewTest::Create
60
- |-- Start.default
61
- |-- model.build
62
- |-- contract.build
63
- |-- contract.default.validate
64
- | |-- Start.default
65
- | |-- contract.default.params_extract
66
- | |-- contract.default.call
67
- | `-- End.failure
68
- `-- End.failure}
69
- end
60
+ |-- Start.default
61
+ |-- model.build
62
+ |-- contract.build
63
+ |-- contract.default.validate
64
+ | |-- Start.default
65
+ | |-- contract.default.params_extract
66
+ | |-- contract.default.call
67
+ | `-- End.failure
68
+ `-- End.failure}
69
+ end
70
+
71
+ # internal variables from {:builder} are excluded in public ctx.
72
+ it { Create.(params: {}).keys.inspect.must_equal %{[:params, :model, :\"result.model\", :\"contract.default\", :\"contract.default.params\", :\"representer.default.class\", :\"result.contract.default\"]} }
70
73
  end
74
+
71
75
  #---
72
76
  # contract MyContract
73
77
  class DocsContractExplicitTest < Minitest::Spec
@@ -257,6 +261,42 @@ class DocsContractKeyTest < Minitest::Spec
257
261
  end
258
262
  end
259
263
 
264
+ #---
265
+ #- Validate() with injected {:"contract.default.extract_key"}
266
+ class DocsContractInjectedKeyTest < Minitest::Spec
267
+ Song = Class.new(ContractConstantTest::Song)
268
+
269
+ module Song::Contract
270
+ Create = ContractConstantTest::Song::Contract::Create
271
+ end
272
+
273
+ #:inject-key-op
274
+ class Song::Create < Trailblazer::Operation
275
+ #~meths
276
+ step Model(Song, :new)
277
+ step Contract::Build(constant: Song::Contract::Create)
278
+ #~meths end
279
+ step Contract::Validate() # we don't define a key here! E.g. {key: "song"}
280
+ step Contract::Persist()
281
+ end
282
+ #:inject-key-op end
283
+
284
+ # empty {:params}, validation fails
285
+ it { Song::Create.(params: {}).inspect(:model, "result.contract.default.extract").must_equal %{<Result:false [#<struct DocsContractInjectedKeyTest::Song title=nil, length=nil>, nil] >} }
286
+ # no {:key} injected/defined, we don't find the data in {params}.
287
+ it { Song::Create.(params: {"song" => { title: "SVG", length: 13 }}).inspect(:model, "result.contract.default.extract").must_equal %{<Result:false [#<struct DocsContractInjectedKeyTest::Song title=nil, length=nil>, nil] >} }
288
+ # {:key} defined and everything works smoothly
289
+ it {
290
+ params = {"song" => { title: "SVG", length: 13 }}
291
+ #:inject-key-call
292
+ res = Song::Create.(
293
+ params: params,
294
+ "contract.default.extract_key": "song"
295
+ )
296
+ #:inject-key-call end
297
+ res.inspect(:model).must_equal %{<Result:true [#<struct DocsContractInjectedKeyTest::Song title=\"SVG\", length=13>] >} }
298
+ end
299
+
260
300
  #---
261
301
  #- Validate( key: :song ), Output(:extract_failure) => End(:my_new_end)
262
302
  class DocsContractKeyWithOutputTest < Minitest::Spec
@@ -297,6 +337,77 @@ class DocsContractKeyWithOutputTest < Minitest::Spec
297
337
  end
298
338
  end
299
339
 
340
+ #---
341
+ #- Validate() with injected {:"contract.default"} and no `Build()`.
342
+ class DocsContractInjectedContractTest < Minitest::Spec
343
+ Song = Class.new(ContractConstantTest::Song)
344
+
345
+ module Song::Contract
346
+ Create = ContractConstantTest::Song::Contract::Create
347
+ end
348
+
349
+ #:inject-contract-op
350
+ class Song::Create < Trailblazer::Operation
351
+ # we omit the {Model()} call as the run-time contract contains the model.
352
+ # we don't have a {Contract::Build()} step here.
353
+ step Contract::Validate(key: "song") # you could use an injection here, too!
354
+ step Contract::Persist()
355
+ end
356
+ #:inject-contract-op end
357
+
358
+ it {
359
+ params = {"song" => { title: "SVG", length: 13 }}
360
+ #:inject-contract-call
361
+ res = Song::Create.(
362
+ params: params,
363
+ "contract.default": Song::Contract::Create.new(Song.new) # we build the contract ourselves!
364
+ )
365
+ #:inject-contract-call end
366
+ res.inspect(:model).must_equal %{<Result:true [nil] >}
367
+ res[:"contract.default"].model.inspect.must_equal %{#<struct DocsContractInjectedContractTest::Song title=\"SVG\", length=13>}
368
+ }
369
+ end
370
+
371
+ #---
372
+ #- Validate( name: "default", invalid_data_terminus: true )
373
+ class DocsContractInvalidEndTest < Minitest::Spec
374
+ Song = Class.new(ContractConstantTest::Song)
375
+
376
+ module Song::Contract
377
+ Create = ContractConstantTest::Song::Contract::Create
378
+ end
379
+
380
+ #:invalid-end
381
+ class Song::Create < Trailblazer::Operation
382
+ step Model( Song, :new )
383
+ step Contract::Build( constant: Song::Contract::Create )
384
+ step Contract::Validate( key: :song, invalid_data_terminus: true )
385
+ step Contract::Persist( )
386
+ end
387
+ #:invalid-end end
388
+
389
+ it do
390
+ result = Song::Create.(params: {song: { title: nil, length: nil }})
391
+ result.event.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:invalid_data>}
392
+ end
393
+
394
+ it { Song::Create.(params: {song: { title: "SVG", length: 13 }}).inspect(:model).must_equal %{<Result:true [#<struct DocsContractInvalidEndTest::Song title=\"SVG\", length=13>] >} }
395
+
396
+ it do
397
+ #:invalid-end-res
398
+ result = Song::Create.(params: { title: "Rising Force", length: 13 })
399
+ result.success? #=> false
400
+ result.event #=> #<Trailblazer::Activity::End semantic=:"contract.default.invalid">
401
+ #:invalid-end-res end
402
+
403
+ #:invalid-end-res-false
404
+ result = Song::Create.(params: { "song" => { title: "Rising Force", length: 13 } })
405
+ result.success? #=> true
406
+ #:invalid-end-res-false end
407
+ end
408
+ end
409
+
410
+
300
411
  #- Contract::Build[ constant: XXX, name: AAA ]
301
412
  class ContractNamedConstantTest < Minitest::Spec
302
413
  Song = Class.new(ContractConstantTest::Song)
@@ -337,24 +448,24 @@ class ContractInjectConstantTest < Minitest::Spec
337
448
  end
338
449
  #:di-constant-contract end
339
450
  #:di-constant
340
- class Create < Trailblazer::Operation
341
- step Model( Song, :new )
342
- step Contract::Build()
451
+ class Song::Create < Trailblazer::Operation
452
+ step Model(Song, :new)
453
+ step Contract::Build() # no constant provided here!
343
454
  step Contract::Validate()
344
- step Contract::Persist( method: :sync )
455
+ step Contract::Persist(method: :sync)
345
456
  end
346
457
  #:di-constant end
347
458
 
348
459
  it do
349
460
  #:di-contract-call
350
- Create.(
351
- params: { title: "Anthony's Song" },
352
- :"contract.default.class" => MyContract
461
+ Song::Create.(
462
+ params: { title: "Anthony's Song" },
463
+ "contract.default.class": MyContract # dependency injection!
353
464
  )
354
465
  #:di-contract-call end
355
466
  end
356
- it { Create.(params: { title: "A" }, :"contract.default.class" => MyContract).inspect(:model).must_equal %{<Result:false [#<struct ContractInjectConstantTest::Song id=nil, title=nil>] >} }
357
- it { Create.(params: { title: "Anthony's Song" }, :"contract.default.class" => MyContract).inspect(:model).must_equal %{<Result:true [#<struct ContractInjectConstantTest::Song id=nil, title="Anthony's Song">] >} }
467
+ it { Song::Create.(params: { title: "A" }, :"contract.default.class" => MyContract).inspect(:model).must_equal %{<Result:false [#<struct ContractInjectConstantTest::Song id=nil, title=nil>] >} }
468
+ it { Song::Create.(params: { title: "Anthony's Song" }, :"contract.default.class" => MyContract).inspect(:model).must_equal %{<Result:true [#<struct ContractInjectConstantTest::Song id=nil, title="Anthony's Song">] >} }
358
469
  end
359
470
 
360
471
  class DryValidationContractTest < Minitest::Spec
@@ -367,16 +478,20 @@ class DryValidationContractTest < Minitest::Spec
367
478
  class Create < Trailblazer::Operation
368
479
  # contract to verify params formally.
369
480
  class MyContract < Reform::Form
370
- feature Reform::Form::Dry
481
+ feature Dry
371
482
  property :id
372
483
  property :title
373
484
 
374
485
  validation name: :default do
375
- required(:id).filled
486
+ params do
487
+ required(:id).filled
488
+ end
376
489
  end
377
490
 
378
491
  validation name: :extra, if: :default do
379
- required(:title).filled(min_size?: 2)
492
+ params do
493
+ required(:title).filled(min_size?: 2)
494
+ end
380
495
  end
381
496
  end
382
497
  #~form end
@@ -402,28 +517,47 @@ class DryValidationContractTest < Minitest::Spec
402
517
  it { Create.(params: { id: 1, title: "Y" }).inspect(:model).must_equal %{<Result:false [#<struct DryValidationContractTest::Song id=nil, title=nil>] >} }
403
518
  it { Create.(params: { id: 1, title: "Yo" }).inspect(:model).must_equal %{<Result:true [#<struct DryValidationContractTest::Song id=1, title="Yo">] >} }
404
519
 
405
- #---
520
+ ##---
406
521
  # Contract::Validate(constant: DrySchema)
407
- class OpWithSchema < Trailblazer::Operation
408
- Schema = Dry::Validation.Schema do
409
- required(:title).filled
410
- end
411
522
 
412
- step Model( Song, :new ) # FIXME.
413
- step Contract::Validate( constant: Schema, key: :song) # generic validate call for you.
523
+ #:dry-schema-contract
524
+ module Song::Operation
525
+ class Archive < Trailblazer::Operation
526
+ Schema = Dry::Validation.Contract do
527
+ params do
528
+ required(:id).filled
529
+ end
530
+ end
531
+
532
+ # step Model(Song, :new) # You don't need {ctx[:model]}.
533
+ step Contract::Validate(constant: Schema, key: :song) # Your validation.
534
+ #~methods
535
+ # step Contract::Persist() # this is not possible!
536
+ #~methods end
537
+ end
414
538
  end
539
+ #:dry-schema-contract end
415
540
 
416
541
  # success
417
- it { OpWithSchema.(params: {song: { title: "SVG" }}).success?.must_equal true }
542
+ it { _(Song::Operation::Archive.(params: {song: {id: "SVG"}}).success?).must_equal true }
418
543
  # failure
419
- it { OpWithSchema.(params: {song: { title: nil }}).success?.must_equal false }
544
+ it { _(Song::Operation::Archive.(params: {song: {id: nil}}).success?).must_equal false }
545
+ # shows error messages
420
546
  it "shows error messages" do
421
- result = OpWithSchema.(params: {song: { title: nil }})
547
+ #:dry-contract-call
548
+ result = Song::Operation::Archive.(params: {song: {id: nil}})
549
+ #:dry-contract-call end
550
+
551
+ _(result[:"result.contract.default"].errors.inspect).must_equal %{#<Dry::Validation::MessageSet messages=[#<Dry::Schema::Message text=\"must be filled\" path=[:id] predicate=:filled? input=nil>] options={:source=>[#<Dry::Schema::Message text=\"must be filled\" path=[:id] predicate=:filled? input=nil>], :hints=>false}>}
422
552
 
423
- result[:"result.contract.default"].errors.must_equal(title: ["must be filled"])
553
+ # raise result[:"result.contract.default"].errors.messages[0].to_s.inspect
554
+ assert_equal result[:"result.contract.default"].errors[:id].inspect, %{["must be filled"]}
555
+ #:dry-contract-result
556
+ result[:"result.contract.default"].errors[:id] #=> ["must be filled"]
557
+ #:dry-contract-result end
424
558
  end
425
559
  # key not found
426
- it { OpWithSchema.(params: {}).success?.must_equal false }
560
+ it { _(Song::Operation::Archive.(params: {}).success?).must_equal false }
427
561
  end
428
562
 
429
563
  class DocContractBuilderTest < Minitest::Spec
@@ -459,6 +593,8 @@ class DocContractBuilderTest < Minitest::Spec
459
593
 
460
594
  it { Create.(params: {}).inspect(:model).must_equal %{<Result:false [#<struct DocContractBuilderTest::Song id=nil, title=nil>] >} }
461
595
  it { Create.(params: { title: "title"}, current_user: Module).inspect(:model).must_equal %{<Result:true [#<struct DocContractBuilderTest::Song id=nil, title="title">] >} }
596
+ # internal variables from {:builder} are excluded in public ctx.
597
+ it { Create.(params: {}).keys.inspect.must_equal %{[:params, :model, :\"result.model\", :\"contract.default\", :\"contract.default.params\", :\"representer.default.class\", :\"result.contract.default\"]} }
462
598
  end
463
599
 
464
600
  class DocContractTest < Minitest::Spec
@@ -501,3 +637,20 @@ class DocContractTest < Minitest::Spec
501
637
 
502
638
  it { Break.(params: { id:1, title: "Fame" }).inspect(:model).must_equal %{<Result:true [#<struct DocContractTest::Song id=1, title=nil>] >} }
503
639
  end
640
+
641
+ class ModelMissingTest < Minitest::Spec
642
+ class Create < Trailblazer::Operation
643
+ class MyContract < Reform::Form
644
+ property :duration, virtual: true
645
+ end
646
+
647
+ step Contract::Build(constant: MyContract)
648
+ step Contract::Validate()
649
+ end
650
+
651
+ it do
652
+ result = Create.(params: {duration: 18})
653
+ assert_equal true, result.success?
654
+ assert_equal 18, result[:"contract.default"].duration
655
+ end
656
+ end
@@ -1,33 +1,33 @@
1
- require "test_helper"
2
- require "dry/container"
1
+ # require "test_helper"
2
+ # require "dry/container"
3
3
 
4
- class DryContainerTest < Minitest::Spec
5
- Song = Struct.new(:id, :title)
4
+ # class DryContainerTest < Minitest::Spec
5
+ # Song = Struct.new(:id, :title)
6
6
 
7
- class MyContract < Reform::Form
8
- property :title
9
- validates :title, length: 2..33
10
- end
7
+ # class MyContract < Reform::Form
8
+ # property :title
9
+ # validates :title, length: 2..33
10
+ # end
11
11
 
12
- my_container = Dry::Container.new
13
- my_container.register("contract.default.class", MyContract)
14
- # my_container.namespace("contract") do
15
- # register("create") { Array }
16
- # end
12
+ # my_container = Dry::Container.new
13
+ # my_container.register("contract.default.class", MyContract)
14
+ # # my_container.namespace("contract") do
15
+ # # register("create") { Array }
16
+ # # end
17
17
 
18
- #---
19
- #- dependency injection
20
- #- with Dry-container
21
- class Create < Trailblazer::Operation
22
- extend Trailblazer::Operation::Container
18
+ # #---
19
+ # #- dependency injection
20
+ # #- with Dry-container
21
+ # class Create < Trailblazer::Operation
22
+ # extend Trailblazer::Operation::Container
23
23
 
24
- step Model(Song, :new)
25
- step Contract::Build()
26
- step Contract::Validate()
27
- step Contract::Persist(method: :sync)
28
- end
29
- #:key end
24
+ # step Model(Song, :new)
25
+ # step Contract::Build()
26
+ # step Contract::Validate()
27
+ # step Contract::Persist(method: :sync)
28
+ # end
29
+ # #:key end
30
30
 
31
- it { Create.({params: {title: "A" } }, my_container).inspect(:model).must_equal %{<Result:false [#<struct DryContainerTest::Song id=nil, title=nil>] >} }
32
- it { Create.({params: {title: "Anthony's Song" } }, my_container).inspect(:model).must_equal %{<Result:true [#<struct DryContainerTest::Song id=nil, title="Anthony's Song">] >} }
33
- end
31
+ # it { Create.({params: {title: "A" } }, my_container).inspect(:model).must_equal %{<Result:false [#<struct DryContainerTest::Song id=nil, title=nil>] >} }
32
+ # it { Create.({params: {title: "Anthony's Song" } }, my_container).inspect(:model).must_equal %{<Result:true [#<struct DryContainerTest::Song id=nil, title="Anthony's Song">] >} }
33
+ # end
@@ -59,6 +59,8 @@ class ContractTest < Minitest::Spec
59
59
  it { Upsert.(params: {song: { title: nil }}).success?.must_equal false }
60
60
  # key not found
61
61
  it { Upsert.(params: {}).success?.must_equal false }
62
+ # no params passed
63
+ it { Upsert.().success?.must_equal false }
62
64
 
63
65
  #---
64
66
  # contract.default.params gets set (TODO: change in 2.1)
data/test/test_helper.rb CHANGED
@@ -1,9 +1,11 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require "trailblazer-macro-contract"
3
+
1
4
  require "pp"
2
5
  require 'delegate'
6
+ require "trailblazer/operation"
3
7
  require "trailblazer/developer"
4
8
  require "trailblazer/macro"
5
- require "trailblazer-macro-contract"
6
- require "trailblazer/developer/render/linear"
7
9
  require "minitest/autorun"
8
10
 
9
11
  # TODO: convert tests to non-rails.
@@ -7,8 +7,8 @@ Gem::Specification.new do |spec|
7
7
  spec.version = Trailblazer::Version::Macro::Contract::VERSION
8
8
  spec.authors = ["Nick Sutterer"]
9
9
  spec.email = ["apotonick@gmail.com"]
10
- spec.description = 'Trailblazer operation form object specific macros'
11
- spec.summary = 'Macros for form-objects: Build, Validate, Persist'
10
+ spec.description = 'Operation macros for form objects'
11
+ spec.summary = 'Macros for form objects: Build, Validate, Persist'
12
12
  spec.homepage = "http://trailblazer.to"
13
13
  spec.license = "LGPL-3.0"
14
14
 
@@ -21,13 +21,16 @@ Gem::Specification.new do |spec|
21
21
  spec.add_dependency "reform", ">= 2.2.0", "< 3.0.0"
22
22
 
23
23
  spec.add_development_dependency "bundler"
24
- spec.add_development_dependency "dry-validation", "0.11.1" # FIXME: upgrade example code
24
+ spec.add_development_dependency "dry-validation"
25
25
  spec.add_development_dependency "reform-rails", "~> 0.2.0.rc2"
26
- spec.add_development_dependency "trailblazer-macro", ">= 2.1.0", "< 2.2.0"
26
+ spec.add_development_dependency "trailblazer-macro", ">= 2.1.9"
27
27
  spec.add_development_dependency "trailblazer-developer"
28
+ spec.add_development_dependency "activemodel", "~> 6.0.0" # FIXME: we still don't support the Rails 6.1 errors object.
28
29
 
29
30
  spec.add_development_dependency "minitest"
30
31
  spec.add_development_dependency "rake"
31
32
 
33
+ spec.add_dependency "trailblazer-activity-dsl-linear", ">= 1.0.0.beta1", "< 1.1.0"
34
+
32
35
  spec.required_ruby_version = ">= 2.0.0"
33
36
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trailblazer-macro-contract
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.1.3.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Sutterer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-23 00:00:00.000000000 Z
11
+ date: 2022-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: reform
@@ -48,16 +48,16 @@ dependencies:
48
48
  name: dry-validation
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - '='
51
+ - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: 0.11.1
53
+ version: '0'
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
- - - '='
58
+ - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: 0.11.1
60
+ version: '0'
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: reform-rails
63
63
  requirement: !ruby/object:Gem::Requirement
@@ -78,20 +78,14 @@ dependencies:
78
78
  requirements:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
- version: 2.1.0
82
- - - "<"
83
- - !ruby/object:Gem::Version
84
- version: 2.2.0
81
+ version: 2.1.9
85
82
  type: :development
86
83
  prerelease: false
87
84
  version_requirements: !ruby/object:Gem::Requirement
88
85
  requirements:
89
86
  - - ">="
90
87
  - !ruby/object:Gem::Version
91
- version: 2.1.0
92
- - - "<"
93
- - !ruby/object:Gem::Version
94
- version: 2.2.0
88
+ version: 2.1.9
95
89
  - !ruby/object:Gem::Dependency
96
90
  name: trailblazer-developer
97
91
  requirement: !ruby/object:Gem::Requirement
@@ -106,6 +100,20 @@ dependencies:
106
100
  - - ">="
107
101
  - !ruby/object:Gem::Version
108
102
  version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: activemodel
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 6.0.0
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 6.0.0
109
117
  - !ruby/object:Gem::Dependency
110
118
  name: minitest
111
119
  requirement: !ruby/object:Gem::Requirement
@@ -134,18 +142,38 @@ dependencies:
134
142
  - - ">="
135
143
  - !ruby/object:Gem::Version
136
144
  version: '0'
137
- description: Trailblazer operation form object specific macros
145
+ - !ruby/object:Gem::Dependency
146
+ name: trailblazer-activity-dsl-linear
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: 1.0.0.beta1
152
+ - - "<"
153
+ - !ruby/object:Gem::Version
154
+ version: 1.1.0
155
+ type: :runtime
156
+ prerelease: false
157
+ version_requirements: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: 1.0.0.beta1
162
+ - - "<"
163
+ - !ruby/object:Gem::Version
164
+ version: 1.1.0
165
+ description: Operation macros for form objects
138
166
  email:
139
167
  - apotonick@gmail.com
140
168
  executables: []
141
169
  extensions: []
142
170
  extra_rdoc_files: []
143
171
  files:
172
+ - ".github/workflows/ci.yml"
144
173
  - ".gitignore"
145
174
  - ".rubocop-https---raw-githubusercontent-com-trailblazer-meta-master-rubocop-yml"
146
175
  - ".rubocop.yml"
147
176
  - ".rubocop_todo.yml"
148
- - ".travis.yml"
149
177
  - CHANGES.md
150
178
  - COMM-LICENSE
151
179
  - Gemfile
@@ -154,10 +182,10 @@ files:
154
182
  - Rakefile
155
183
  - lib/trailblazer-macro-contract.rb
156
184
  - lib/trailblazer/macro/contract.rb
185
+ - lib/trailblazer/macro/contract/build.rb
186
+ - lib/trailblazer/macro/contract/persist.rb
187
+ - lib/trailblazer/macro/contract/validate.rb
157
188
  - lib/trailblazer/macro/contract/version.rb
158
- - lib/trailblazer/operation/contract.rb
159
- - lib/trailblazer/operation/persist.rb
160
- - lib/trailblazer/operation/validate.rb
161
189
  - test/docs/contract_test.rb
162
190
  - test/docs/dry_test.rb
163
191
  - test/operation/contract_test.rb
@@ -179,15 +207,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
179
207
  version: 2.0.0
180
208
  required_rubygems_version: !ruby/object:Gem::Requirement
181
209
  requirements:
182
- - - ">="
210
+ - - ">"
183
211
  - !ruby/object:Gem::Version
184
- version: '0'
212
+ version: 1.3.1
185
213
  requirements: []
186
- rubyforge_project:
187
- rubygems_version: 2.7.6
214
+ rubygems_version: 3.2.3
188
215
  signing_key:
189
216
  specification_version: 4
190
- summary: 'Macros for form-objects: Build, Validate, Persist'
217
+ summary: 'Macros for form objects: Build, Validate, Persist'
191
218
  test_files:
192
219
  - test/docs/contract_test.rb
193
220
  - test/docs/dry_test.rb
data/.travis.yml DELETED
@@ -1,8 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- before_install: gem install bundler
4
- rvm:
5
- - 2.5.1
6
- - 2.4.4
7
- - 2.3.7
8
- # - 2.2.10
@@ -1,63 +0,0 @@
1
- module Trailblazer
2
- class Operation
3
- module Contract
4
- def self.Build(name: "default", constant: nil, builder: nil)
5
- task = lambda do |(options, flow_options), **circuit_options|
6
- result = Build.(options, circuit_options, name: name, constant: constant, builder: builder)
7
-
8
- return Activity::TaskBuilder.binary_signal_for(result, Activity::Right, Activity::Left),
9
- [options, flow_options]
10
- end
11
-
12
- {task: task, id: "contract.build"}
13
- end
14
-
15
- module Build
16
- # Build contract at runtime.
17
- def self.call(options, circuit_options, name: "default", constant: nil, builder: nil)
18
- # TODO: we could probably clean this up a bit at some point.
19
- contract_class = constant || options[:"contract.#{name}.class"] # DISCUSS: Injection possible here?
20
- model = options[:model]
21
- name = :"contract.#{name}"
22
-
23
- options[name] = if builder
24
- call_builder(options, circuit_options, builder: builder, constant: contract_class, name: name)
25
- else
26
- contract_class.new(model)
27
- end
28
- end
29
-
30
- def self.call_builder(options, circuit_options, builder: raise, constant: raise, name: raise)
31
- tmp_options = options.to_hash.merge(
32
- constant: constant,
33
- name: name
34
- )
35
- Trailblazer::Option(builder).(options, tmp_options, circuit_options)
36
- end
37
- end
38
-
39
- module DSL
40
- def self.extended(extender)
41
- extender.extend(ClassDependencies)
42
- warn "[Trailblazer] Using `contract do...end` is deprecated. Please use a form class and the Builder( constant: <Form> ) option."
43
- end
44
-
45
- # This is the class level DSL method.
46
- # Op.contract #=> returns contract class
47
- # Op.contract do .. end # defines contract
48
- # Op.contract CommentForm # copies (and subclasses) external contract.
49
- # Op.contract CommentForm do .. end # copies and extends contract.
50
- def contract(name = :default, constant = nil, base: Reform::Form, &block)
51
- heritage.record(:contract, name, constant, &block)
52
-
53
- path, form_class = Trailblazer::DSL::Build.new.(
54
- {prefix: :contract, class: base, container: self},
55
- name, constant, block
56
- )
57
-
58
- self[path] = form_class
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,75 +0,0 @@
1
- module Trailblazer
2
- class Operation
3
- module Contract
4
- # result.contract = {..}
5
- # result.contract.errors = {..}
6
- # Deviate to left track if optional key is not found in params.
7
- # Deviate to left if validation result falsey.
8
- def self.Validate(skip_extract: false, name: "default", representer: false, key: nil, constant: nil) # DISCUSS: should we introduce something like Validate::Deserializer?
9
- params_path = :"contract.#{name}.params" # extract_params! save extracted params here.
10
-
11
- extract = Validate::Extract.new(key: key, params_path: params_path).freeze
12
- validate = Validate.new(name: name, representer: representer, params_path: params_path, constant: constant).freeze
13
-
14
- # Build a simple Railway {Activity} for the internal flow.
15
- activity = Class.new(Activity::Railway(name: "Contract::Validate")) do
16
- step extract, id: "#{params_path}_extract", Output(:failure) => End(:extract_failure) unless skip_extract# || representer
17
- step validate, id: "contract.#{name}.call"
18
- end
19
-
20
- options = activity.Subprocess(activity)
21
- options = options.merge(id: "contract.#{name}.validate")
22
-
23
- # Deviate End.extract_failure to the standard failure track as a default. This can be changed from the user side.
24
- options = options.merge(activity.Output(:extract_failure) => activity.Track(:failure)) unless skip_extract
25
-
26
- options
27
- end
28
-
29
- class Validate
30
- # Task: extract the contract's input from params by reading `:key`.
31
- class Extract
32
- def initialize(key: nil, params_path: nil)
33
- @key, @params_path = key, params_path
34
- end
35
-
36
- def call(ctx, params:, **)
37
- ctx[@params_path] = @key ? params[@key] : params
38
- end
39
- end
40
-
41
- def initialize(name: "default", representer: false, params_path: nil, constant: nil)
42
- @name, @representer, @params_path, @constant = name, representer, params_path, constant
43
- end
44
-
45
- # Task: Validates contract `:name`.
46
- def call(ctx, **)
47
- validate!(
48
- ctx,
49
- representer: ctx["representer.#{@name}.class"] ||= @representer, # FIXME: maybe @representer should use DI.
50
- params_path: @params_path
51
- )
52
- end
53
-
54
- def validate!(options, representer: false, from: :document, params_path: nil)
55
- path = :"contract.#{@name}"
56
- contract = @constant || options[path]
57
-
58
- # this is for 1.1-style compatibility and should be removed once we have Deserializer in place:
59
- options[:"result.#{path}"] = result =
60
- if representer
61
- # use :document as the body and let the representer deserialize to the contract.
62
- # this will be simplified once we have Deserializer.
63
- # translates to contract.("{document: bla}") { MyRepresenter.new(contract).from_json .. }
64
- contract.(options[from]) { |document| representer.new(contract).parse(document) }
65
- else
66
- # let Reform handle the deserialization.
67
- contract.(options[params_path])
68
- end
69
-
70
- result.success?
71
- end
72
- end
73
- end
74
- end # Operation
75
- end