Skip to content

Commit 2e3194f

Browse files
committed
Porting to new Literal::Properties
1 parent 7c20ba1 commit 2e3194f

File tree

10 files changed

+60
-95
lines changed

10 files changed

+60
-95
lines changed

README.md

+9-41
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,16 @@ class ShelveBookOperation < ::TypedOperation::Base
4444
# Or if you prefer:
4545
# `param :description, String`
4646

47-
named_param :author_id, Integer, &:to_i
48-
named_param :isbn, String
47+
# `param` creates named parameters by default
48+
param :author_id, Integer, &:to_i
49+
param :isbn, String
4950

5051
# Optional parameters are specified by wrapping the type constraint in the `optional` method, or using the `optional:` option
51-
named_param :shelf_code, optional(Integer)
52+
param :shelf_code, optional(Integer)
5253
# Or if you prefer:
5354
# `named_param :shelf_code, Integer, optional: true`
5455

55-
named_param :category, String, default: "unknown".freeze
56+
param :category, String, default: "unknown".freeze
5657

5758
# optional hook called when the operation is initialized, and after the parameters have been set
5859
def prepare
@@ -521,7 +522,8 @@ This is an example of a `ApplicationOperation` in a Rails app that uses `Dry::Mo
521522

522523
class ApplicationOperation < ::TypedOperation::Base
523524
# We choose to use dry-monads for our operations, so include the required modules
524-
include Dry::Monads[:result, :do]
525+
include Dry::Monads[:result]
526+
include Dry::Monads::Do.for(:perform)
525527

526528
class << self
527529
# Setup our own preferred names for the DSL methods
@@ -711,55 +713,21 @@ end
711713

712714
Note you are provided the ActionPolicy error object, but you cannot stop the error from being re-raised.
713715

714-
### Using with `literal` monads
715-
716-
You can use the `literal` gem to provide a `Result` type for your operations.
717-
718-
```ruby
719-
class MyOperation < ::TypedOperation::Base
720-
param :account_name, String
721-
param :owner, String
722-
723-
def perform
724-
create_account.bind do |account|
725-
associate_owner(account).map { account }
726-
end
727-
end
728-
729-
private
730-
731-
def create_account
732-
# ...
733-
# Literal::Failure.new(:cant_create_account)
734-
Literal::Success.new(account_name)
735-
end
736-
737-
def associate_owner(account)
738-
# ...
739-
Literal::Failure.new(:cant_associate_owner)
740-
# Literal::Success.new("ok")
741-
end
742-
end
743-
744-
MyOperation.new(account_name: "foo", owner: "bar").call
745-
# => Literal::Failure(:cant_associate_owner)
746-
```
747-
748716
### Using with `Dry::Monads`
749717

750718
As per the example in [`Dry::Monads` documentation](https://dry-rb.org/gems/dry-monads/1.0/do-notation/)
751719

752720
```ruby
753721
class MyOperation < ::TypedOperation::Base
754722
include Dry::Monads[:result]
755-
include Dry::Monads::Do.for(:call)
723+
include Dry::Monads::Do.for(:perform, :create_account)
756724

757725
param :account_name, String
758726
param :owner, ::Owner
759727

760728
def perform
761729
account = yield create_account(account_name)
762-
yield associate_owner(account, owner)
730+
yield AnotherOperation.call(account, owner)
763731

764732
Success(account)
765733
end

lib/typed_operation.rb

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
require "typed_operation/operations/partial_application"
1212
require "typed_operation/operations/callable"
1313
require "typed_operation/operations/lifecycle"
14-
require "typed_operation/operations/deconstruct"
15-
require "typed_operation/operations/attribute_builder"
14+
require "typed_operation/operations/property_builder"
1615
require "typed_operation/operations/executable"
1716
require "typed_operation/curried"
1817
require "typed_operation/immutable_base"

lib/typed_operation/base.rb

-12
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,6 @@ class Base < Literal::Struct
88

99
include Operations::Lifecycle
1010
include Operations::Callable
11-
include Operations::Deconstruct
1211
include Operations::Executable
13-
14-
class << self
15-
def attribute(name, type, special = nil, reader: :public, writer: :public, positional: false, default: nil)
16-
super(name, type, special, reader:, writer: false, positional:, default:)
17-
end
18-
end
19-
20-
def with(...)
21-
# copy to new operation with new attrs
22-
self.class.new(**attributes.merge(...))
23-
end
2412
end
2513
end

lib/typed_operation/immutable_base.rb

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ class ImmutableBase < Literal::Data
88

99
include Operations::Lifecycle
1010
include Operations::Callable
11-
include Operations::Deconstruct
1211
include Operations::Executable
1312
end
1413
end

lib/typed_operation/operations/deconstruct.rb

-16
This file was deleted.

lib/typed_operation/operations/introspection.rb

+5-7
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,23 @@ module Operations
55
# Introspection methods
66
module Introspection
77
def positional_parameters
8-
literal_attributes.filter_map { |name, attribute| name if attribute.positional? }
8+
literal_properties.filter_map { |property| property.name if property.positional? }
99
end
1010

1111
def keyword_parameters
12-
literal_attributes.filter_map { |name, attribute| name unless attribute.positional? }
12+
literal_properties.filter_map { |property| property.name if property.keyword? }
1313
end
1414

1515
def required_parameters
16-
literal_attributes.filter do |name, attribute|
17-
attribute.default.nil? # Any optional parameters will have a default value/proc in their Literal::Attribute
18-
end
16+
literal_properties.filter { |property| property.required? }
1917
end
2018

2119
def required_positional_parameters
22-
required_parameters.filter_map { |name, attribute| name if attribute.positional? }
20+
required_parameters.filter_map { |property| property.name if property.positional? }
2321
end
2422

2523
def required_keyword_parameters
26-
required_parameters.filter_map { |name, attribute| name unless attribute.positional? }
24+
required_parameters.filter_map { |property| property.name if property.keyword? }
2725
end
2826

2927
def optional_positional_parameters

lib/typed_operation/operations/lifecycle.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module TypedOperation
44
module Operations
55
module Lifecycle
66
# This is called by Literal on initialization of underlying Struct/Data
7-
def after_initialization
7+
def after_initialize
88
prepare if respond_to?(:prepare)
99
end
1010
end

lib/typed_operation/operations/parameters.rb

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ module TypedOperation
44
module Operations
55
# Method to define parameters for your operation.
66
module Parameters
7+
# Override literal `prop` to prevent creating writers (Literal::Data does this by default)
8+
def self.prop(name, type, kind = :keyword, reader: :public, writer: :public, default: nil)
9+
super(name, type, kind, reader:, writer: false, default:)
10+
end
11+
712
# Parameter for keyword argument, or a positional argument if you use positional: true
813
# Required, but you can set a default or use optional: true if you want optional
914
def param(name, signature = :any, **options, &converter)
10-
AttributeBuilder.new(self, name, signature, options).define(&converter)
15+
PropertyBuilder.new(self, name, signature, options).define(&converter)
1116
end
1217

1318
# Alternative DSL

lib/typed_operation/operations/property_builder.rb

+4-6
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
module TypedOperation
44
module Operations
5-
class AttributeBuilder
5+
class PropertyBuilder
66
def initialize(typed_operation, parameter_name, type_signature, options)
77
@typed_operation = typed_operation
88
@name = parameter_name
99
@signature = type_signature
10-
@optional = options[:optional]
11-
@positional = options[:positional]
10+
@optional = options[:optional] # Wraps signature in NilableType
11+
@positional = options[:positional] # Changes kind to positional
1212
@reader = options[:reader] || :public
1313
@default_key = options.key?(:default)
1414
@default = options[:default]
@@ -51,7 +51,7 @@ def type_nilable?
5151
end
5252

5353
def union_with_nil_to_support_nil_default
54-
@signature = Literal::Union.new(@signature, NilClass) if has_default_value_nil?
54+
@signature = Literal::Types::UnionType.new(@signature, NilClass) if has_default_value_nil?
5555
end
5656

5757
def has_default_value_nil?
@@ -61,8 +61,6 @@ def has_default_value_nil?
6161
def validate_positional_order_params!
6262
# Optional ones can always be added after required ones, or before any others, but required ones must be first
6363
unless type_nilable? || @typed_operation.optional_positional_parameters.empty?
64-
puts type_nilable?
65-
puts @typed_operation.optional_positional_parameters
6664
raise ParameterError, "Cannot define required positional parameter '#{@name}' after optional positional parameters"
6765
end
6866
end

test/typed_operation/base_test.rb

+34-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "test_helper"
4+
require "dry-monads"
45

56
module TypedOperation
67
class BaseTest < Minitest::Test
@@ -165,11 +166,12 @@ def test_operation_raises_on_invalid_positional_params
165166
end
166167

167168
def test_operation_raises_on_invalid_positional_params_using_optional
168-
assert_raises do
169+
assert_raises(::TypedOperation::ParameterError) do
169170
Class.new(::TypedOperation::Base) do
170171
# This is invalid, because positional params can't be optional before required ones
171172
positional_param :first, optional(String)
172-
positional_param :second, String
173+
positional_param :second, Literal::Types::NilableType.new(String)
174+
positional_param :third, String # required after optional is not possible
173175
end
174176
end
175177
end
@@ -422,12 +424,10 @@ def test_operation_can_be_partially_applied_then_curry
422424
assert_equal "a/b//d/e/f", curried_operation.call("b").call("d")
423425
end
424426

425-
def test_operation_instance_can_be_copied_using_with
426-
operation = TestOperation.new(foo: "1", bar: "2", baz: "3")
427-
operation2 = operation.with(foo: "a")
428-
assert_equal "a", operation2.foo
429-
assert_equal "2", operation2.bar
430-
assert_equal "3", operation2.baz
427+
def test_operation_instance_can_be_copied_using_dup
428+
operation = TestKeywordAndPositionalOperation.new("1", "2", kw1: "1", kw2: "2")
429+
operation2 = operation.dup
430+
assert_equal "1/2/1/2", operation2.call
431431
end
432432

433433
def test_operation_should_not_freeze_arguments
@@ -436,5 +436,31 @@ def test_operation_should_not_freeze_arguments
436436
operation.my_hash[:b] = 2
437437
assert_equal({a: 1, b: 2}, operation.my_hash)
438438
end
439+
440+
def test_with_dry_maybe_monad_partially_apply_then_curry
441+
operation_class = Class.new(::TypedOperation::Base) do
442+
positional_param :v1, Integer
443+
positional_param :v2, Integer
444+
445+
def perform
446+
::Dry::Monads::Maybe(v1 + v2)
447+
end
448+
end
449+
450+
operation = operation_class.with(1)
451+
operation2 = operation_class.with(3)
452+
m = ::Dry::Monads::Maybe::Some.new(2)
453+
assert_equal m.bind(&operation.curry).bind(&operation2.curry), Dry::Monads::Maybe::Some.new(6)
454+
m = Dry::Monads::Maybe::None.instance
455+
assert_equal m.bind(&operation.curry).bind(&operation2.curry), Dry::Monads::Maybe::None.instance
456+
end
457+
458+
def test_can_dup
459+
operation = TestOperation.new(foo: "1", bar: "2", baz: "3")
460+
operation2 = operation.dup
461+
assert_equal "1", operation2.foo
462+
assert_equal "2", operation2.bar
463+
assert_equal "3", operation2.baz
464+
end
439465
end
440466
end

0 commit comments

Comments
 (0)