trailblazer-macro-contract 2.1.0 → 2.1.3.beta1

Sign up to get free protection for your applications and to get access to all the features.
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