typed_operation 1.0.0.pre1 → 1.0.0.pre2

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: ff925e2cc5ce03a696a209e0ca61130bff94ddb6ce28f2670ebaaae18f1968ba
4
- data.tar.gz: b14a0952694653f918135a57180c8c7746aeffcb14385d17aeef7a1283726e99
3
+ metadata.gz: 1337560f2bffbfd45574c1df5f007c951dba43293324c309c3409aab18e57f0c
4
+ data.tar.gz: 63deedbca3cde1fadb88668af46ec21652e17f9600d23c0e13c8b22429c05e0c
5
5
  SHA512:
6
- metadata.gz: 0cb23aed491058bdefba04d75661ec4cb5110a2491ba9d4b3afc929d41e1863a09b9ac09b41bdbd6306856af6d5552f2c86c3057b53df3076d0674a9c1feafda
7
- data.tar.gz: 143f6ba5a00d5cabadec3f7f326dc7a724fdd2666ca0dffa326cacf9f9af2f4dce797f77a727468cd919eb42a4b6de530105d6866e49797997c10ff5a36db9fe
6
+ metadata.gz: 0a8fdc89c772282de6c654365e18d7397532b84aebd738285499998c04c32906dc1e63961f5b47d139dfff532a458461dbe07a29bdfcda9f29d8132d645599df
7
+ data.tar.gz: cee03eb2c1e2d9100e657d98a1b70f6889b0e276251dd839f343586ff89f17287853224a487357343fc421ff6723686a2bf672c7ad53a058246668bb1d80a926
data/README.md CHANGED
@@ -92,6 +92,8 @@ shelve.call(author_id: "1", isbn: false)
92
92
 
93
93
  ### Partially applying parameters
94
94
 
95
+ Operations can also be partially applied and curried:
96
+
95
97
  ```ruby
96
98
  class TestOperation < ::TypedOperation::Base
97
99
  param :foo, String, positional: true
@@ -151,9 +153,14 @@ TestOperation.with("1").with(bar: "2").with(baz: 3).operation
151
153
 
152
154
  ## Documentation
153
155
 
154
- ### Create an operation (subclass `TypedOperation::Base`)
156
+ ### Create an operation (subclass `TypedOperation::Base` or `TypedOperation::ImmutableBase`)
157
+
158
+ Create an operation by subclassing `TypedOperation::Base` or `TypedOperation::ImmutableBase` and specifying the parameters the operation requires.
155
159
 
156
- Create an operation by subclassing `TypedOperation::Base` and specifying the parameters the operation requires.
160
+ - `TypedOperation::Base` (uses `Literal::Struct`) is the parent class for an operation where the arguments are potentially mutable (ie not frozen).
161
+ No attribute writer methods are defined, so the arguments can not be changed after initialization, but the values passed in are not guaranteed to be frozen.
162
+ - `TypedOperation::ImmutableBase` (uses `Literal::Data`) is the parent class for an operation where the arguments are immutable (frozen on initialization),
163
+ thus giving a somewhat stronger immutability guarantee (ie that the operation does not mutate its arguments).
157
164
 
158
165
  The subclass must implement the `#call` method which is where the operations main work is done.
159
166
 
@@ -161,11 +168,41 @@ The operation can also implement:
161
168
 
162
169
  - `#prepare` - called when the operation is initialized, and after the parameters have been set
163
170
 
171
+
164
172
  ### Specifying parameters (using `.param`)
165
173
 
166
174
  Parameters are specified using the provided class methods (`.positional_param` and `.named_param`),
167
175
  or using the underlying `.param` method.
168
176
 
177
+ Types are specified using the `literal` gem. In many cases this simply means providing the class of the
178
+ expected type, but there are also some other useful types provided by `literal` (eg `Union`).
179
+
180
+ These can be either accessed via the `Literal` module, eg `Literal::Types::BooleanType`:
181
+
182
+ ```ruby
183
+ class MyOperation < ::TypedOperation::Base
184
+ param :name, String
185
+ param :age, Integer, optional: true
186
+ param :choices, Literal::Types::ArrayType.new(String)
187
+ param :chose, Literal::Types::BooleanType
188
+ end
189
+
190
+ MyOperation.new(name: "bob", choices: ["st"], chose: true)
191
+ ```
192
+
193
+ or by including the `Literal::Types` module into your operation class, and using the aliases provided:
194
+
195
+ ```ruby
196
+ class MyOperation < ::TypedOperation::Base
197
+ include Literal::Types
198
+
199
+ param :name, String
200
+ param :age, _Nilable(Integer) # optional can also be specifed using `.optional`
201
+ param :choices, _Array(String)
202
+ param :chose, _Boolean
203
+ end
204
+ ```
205
+
169
206
  Type constraints can be modified to make the parameter optional using `.optional`.
170
207
 
171
208
  #### Your own aliases
@@ -319,7 +356,7 @@ You can specify a block after a parameter definition to coerce the argument valu
319
356
 
320
357
  ```ruby
321
358
  param :name, String, &:to_s
322
- param :choice, Union(FalseClass, TrueClass) do |v|
359
+ param :choice, Literal::Types::BooleanType do |v|
323
360
  v == "y"
324
361
  end
325
362
  ```
@@ -498,28 +535,27 @@ class MyOperation < ::TypedOperation::Base
498
535
 
499
536
  def call
500
537
  create_account.bind do |account|
501
- associate_owner(account).bind do
502
- account
503
- end
538
+ associate_owner(account).map { account }
504
539
  end
505
540
  end
506
541
 
507
542
  private
508
543
 
509
544
  def create_account
510
- # returns Literal::Success(account) or Literal::Failure(:cant_create)
545
+ # ...
546
+ # Literal::Failure.new(:cant_create_account)
511
547
  Literal::Success.new(account_name)
512
548
  end
513
549
 
514
550
  def associate_owner(account)
515
551
  # ...
516
552
  Literal::Failure.new(:cant_associate_owner)
553
+ # Literal::Success.new("ok")
517
554
  end
518
555
  end
519
556
 
520
557
  MyOperation.new(account_name: "foo", owner: "bar").call
521
558
  # => Literal::Failure(:cant_associate_owner)
522
-
523
559
  ```
524
560
 
525
561
  ### Using with `Dry::Monads`
data/Rakefile CHANGED
@@ -1,3 +1,17 @@
1
- require "bundler/setup"
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
+ require "bundler/setup"
5
+ require "rake/testtask"
6
+
7
+ ENV["NO_RAILS"] = "true"
8
+
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << "test"
11
+ t.libs << "lib"
12
+ t.test_files = FileList["test/typed_operation/**/*_test.rb"]
13
+ end
14
+
15
+ require "standard/rake"
16
+
17
+ task default: %i[test standard]
@@ -1,106 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "literal"
4
-
5
3
  module TypedOperation
6
- class Base < Literal::Data
7
- class << self
8
- def call(...)
9
- new(...).call
10
- end
4
+ class Base < Literal::Struct
5
+ extend Operations::Introspection
6
+ extend Operations::Parameters
7
+ extend Operations::PartialApplication
11
8
 
12
- def with(...)
13
- PartiallyApplied.new(self, ...).with
14
- end
15
- alias_method :[], :with
16
-
17
- def curry
18
- Curried.new(self)
19
- end
20
-
21
- def to_proc
22
- method(:call).to_proc
23
- end
24
-
25
- # Method to define parameters for your operation.
26
-
27
- # Parameter for keyword argument, or a positional argument if you use positional: true
28
- # Required, but you can set a default or use optional: true if you want optional
29
- def param(name, signature = :any, **options, &converter)
30
- AttributeBuilder.new(self, name, signature, options).define(&converter)
31
- end
32
-
33
- # Alternative DSL
34
-
35
- # Parameter for positional argument
36
- def positional_param(name, signature = :any, **options, &converter)
37
- param(name, signature, **options.merge(positional: true), &converter)
38
- end
39
-
40
- # Parameter for a keyword or named argument
41
- def named_param(name, signature = :any, **options, &converter)
42
- param(name, signature, **options.merge(positional: false), &converter)
43
- end
44
-
45
- # Wrap a type signature in a NilableType meaning it is optional to TypedOperation
46
- def optional(type_signature)
47
- NilableType.new(type_signature)
48
- end
49
-
50
- # Introspection methods
51
-
52
- def positional_parameters
53
- literal_attributes.filter_map { |name, attribute| name if attribute.positional? }
54
- end
9
+ include Operations::Callable
10
+ include Operations::Lifecycle
11
+ include Operations::Deconstruct
55
12
 
56
- def keyword_parameters
57
- literal_attributes.filter_map { |name, attribute| name unless attribute.positional? }
58
- end
59
-
60
- def required_positional_parameters
61
- required_parameters.filter_map { |name, attribute| name if attribute.positional? }
62
- end
63
-
64
- def required_keyword_parameters
65
- required_parameters.filter_map { |name, attribute| name unless attribute.positional? }
66
- end
67
-
68
- def optional_positional_parameters
69
- positional_parameters - required_positional_parameters
70
- end
71
-
72
- def optional_keyword_parameters
73
- keyword_parameters - required_keyword_parameters
74
- end
75
-
76
- private
77
-
78
- def required_parameters
79
- literal_attributes.filter do |name, attribute|
80
- attribute.default.nil? # Any optional parameters will have a default value/proc in their Literal::Attribute
81
- end
13
+ class << self
14
+ def attribute(name, type, special = nil, reader: :public, writer: :public, positional: false, default: nil)
15
+ super(name, type, special, reader:, writer: false, positional:, default:)
82
16
  end
83
17
  end
84
18
 
85
- def after_initialization
86
- prepare if respond_to?(:prepare)
87
- end
88
-
89
- def call
90
- raise InvalidOperationError, "You must implement #call"
91
- end
92
-
93
- def to_proc
94
- method(:call).to_proc
95
- end
96
-
97
- def deconstruct
98
- attributes.values
99
- end
100
-
101
- def deconstruct_keys(keys)
102
- h = attributes.to_h
103
- keys ? h.slice(*keys) : h
19
+ def with(...)
20
+ # copy to new operation with new attrs
21
+ self.class.new(**attributes.merge(...))
104
22
  end
105
23
  end
106
24
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedOperation
4
+ class ImmutableBase < Literal::Data
5
+ extend Operations::Introspection
6
+ extend Operations::Parameters
7
+ extend Operations::PartialApplication
8
+
9
+ include Operations::Callable
10
+ include Operations::Lifecycle
11
+ include Operations::Deconstruct
12
+ end
13
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedOperation
4
+ module Operations
5
+ class AttributeBuilder
6
+ def initialize(typed_operation, parameter_name, type_signature, options)
7
+ @typed_operation = typed_operation
8
+ @name = parameter_name
9
+ @signature = type_signature
10
+ @optional = options[:optional]
11
+ @positional = options[:positional]
12
+ @reader = options[:reader] || :public
13
+ @default_key = options.key?(:default)
14
+ @default = options[:default]
15
+
16
+ prepare_type_signature_for_literal
17
+ end
18
+
19
+ def define(&converter)
20
+ @typed_operation.attribute(
21
+ @name,
22
+ @signature,
23
+ default: default_value_for_literal,
24
+ positional: @positional,
25
+ reader: @reader,
26
+ &converter
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ def prepare_type_signature_for_literal
33
+ @signature = Literal::Types::NilableType.new(@signature) if needs_to_be_nilable?
34
+ union_with_nil_to_support_nil_default
35
+ validate_positional_order_params! if @positional
36
+ end
37
+
38
+ # If already wrapped in a Nilable then don't wrap again
39
+ def needs_to_be_nilable?
40
+ @optional && !type_nilable?
41
+ end
42
+
43
+ def type_nilable?
44
+ @signature.is_a?(Literal::Types::NilableType)
45
+ end
46
+
47
+ def union_with_nil_to_support_nil_default
48
+ @signature = Literal::Union.new(@signature, NilClass) if has_default_value_nil?
49
+ end
50
+
51
+ def has_default_value_nil?
52
+ default_provided? && @default.nil?
53
+ end
54
+
55
+ def validate_positional_order_params!
56
+ # Optional ones can always be added after required ones, or before any others, but required ones must be first
57
+ unless type_nilable? || @typed_operation.optional_positional_parameters.empty?
58
+ raise ParameterError, "Cannot define required positional parameter '#{@name}' after optional positional parameters"
59
+ end
60
+ end
61
+
62
+ def default_provided?
63
+ @default_key
64
+ end
65
+
66
+ def default_value_for_literal
67
+ if has_default_value_nil? || type_nilable?
68
+ -> {}
69
+ else
70
+ @default
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedOperation
4
+ module Operations
5
+ module Callable
6
+ def self.included(base)
7
+ base.extend(CallableMethods)
8
+ end
9
+
10
+ module CallableMethods
11
+ def call(...)
12
+ new(...).call
13
+ end
14
+
15
+ def to_proc
16
+ method(:call).to_proc
17
+ end
18
+ end
19
+
20
+ include CallableMethods
21
+
22
+ def call
23
+ raise InvalidOperationError, "You must implement #call"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedOperation
4
+ module Operations
5
+ module Deconstruct
6
+ def deconstruct
7
+ attributes.values
8
+ end
9
+
10
+ def deconstruct_keys(keys)
11
+ h = attributes.to_h
12
+ keys ? h.slice(*keys) : h
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedOperation
4
+ module Operations
5
+ # Introspection methods
6
+ module Introspection
7
+ def positional_parameters
8
+ literal_attributes.filter_map { |name, attribute| name if attribute.positional? }
9
+ end
10
+
11
+ def keyword_parameters
12
+ literal_attributes.filter_map { |name, attribute| name unless attribute.positional? }
13
+ end
14
+
15
+ 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
19
+ end
20
+
21
+ def required_positional_parameters
22
+ required_parameters.filter_map { |name, attribute| name if attribute.positional? }
23
+ end
24
+
25
+ def required_keyword_parameters
26
+ required_parameters.filter_map { |name, attribute| name unless attribute.positional? }
27
+ end
28
+
29
+ def optional_positional_parameters
30
+ positional_parameters - required_positional_parameters
31
+ end
32
+
33
+ def optional_keyword_parameters
34
+ keyword_parameters - required_keyword_parameters
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedOperation
4
+ module Operations
5
+ module Lifecycle
6
+ def after_initialization
7
+ prepare if respond_to?(:prepare)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedOperation
4
+ module Operations
5
+ # Method to define parameters for your operation.
6
+ module Parameters
7
+ # Parameter for keyword argument, or a positional argument if you use positional: true
8
+ # Required, but you can set a default or use optional: true if you want optional
9
+ def param(name, signature = :any, **options, &converter)
10
+ AttributeBuilder.new(self, name, signature, options).define(&converter)
11
+ end
12
+
13
+ # Alternative DSL
14
+
15
+ # Parameter for positional argument
16
+ def positional_param(name, signature = :any, **options, &converter)
17
+ param(name, signature, **options.merge(positional: true), &converter)
18
+ end
19
+
20
+ # Parameter for a keyword or named argument
21
+ def named_param(name, signature = :any, **options, &converter)
22
+ param(name, signature, **options.merge(positional: false), &converter)
23
+ end
24
+
25
+ # Wrap a type signature in a NilableType meaning it is optional to TypedOperation
26
+ def optional(type_signature)
27
+ Literal::Types::NilableType.new(type_signature)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedOperation
4
+ module Operations
5
+ module PartialApplication
6
+ def with(...)
7
+ PartiallyApplied.new(self, ...).with
8
+ end
9
+ alias_method :[], :with
10
+
11
+ def curry
12
+ Curried.new(self)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,3 +1,3 @@
1
1
  module TypedOperation
2
- VERSION = "1.0.0.pre1"
2
+ VERSION = "1.0.0.pre2"
3
3
  end
@@ -2,11 +2,19 @@ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2.0")
2
2
  require "polyfill-data"
3
3
  end
4
4
 
5
+ require "literal"
6
+
5
7
  require "typed_operation/version"
6
8
  require "typed_operation/railtie" if defined?(Rails::Railtie)
7
- require "typed_operation/nilable_type"
8
- require "typed_operation/attribute_builder"
9
+ require "typed_operation/operations/introspection"
10
+ require "typed_operation/operations/parameters"
11
+ require "typed_operation/operations/partial_application"
12
+ require "typed_operation/operations/callable"
13
+ require "typed_operation/operations/lifecycle"
14
+ require "typed_operation/operations/deconstruct"
15
+ require "typed_operation/operations/attribute_builder"
9
16
  require "typed_operation/curried"
17
+ require "typed_operation/immutable_base"
10
18
  require "typed_operation/base"
11
19
  require "typed_operation/partially_applied"
12
20
  require "typed_operation/prepared"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typed_operation
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre1
4
+ version: 1.0.0.pre2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Ierodiaconou
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-20 00:00:00.000000000 Z
11
+ date: 2023-08-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: TypedOperation is a command pattern implementation where inputs can be
14
14
  defined with runtime type checks. Operations can be partially applied.
@@ -30,10 +30,16 @@ files:
30
30
  - lib/generators/typed_operation_generator.rb
31
31
  - lib/tasks/typed_operation_tasks.rake
32
32
  - lib/typed_operation.rb
33
- - lib/typed_operation/attribute_builder.rb
34
33
  - lib/typed_operation/base.rb
35
34
  - lib/typed_operation/curried.rb
36
- - lib/typed_operation/nilable_type.rb
35
+ - lib/typed_operation/immutable_base.rb
36
+ - lib/typed_operation/operations/attribute_builder.rb
37
+ - lib/typed_operation/operations/callable.rb
38
+ - lib/typed_operation/operations/deconstruct.rb
39
+ - lib/typed_operation/operations/introspection.rb
40
+ - lib/typed_operation/operations/lifecycle.rb
41
+ - lib/typed_operation/operations/parameters.rb
42
+ - lib/typed_operation/operations/partial_application.rb
37
43
  - lib/typed_operation/partially_applied.rb
38
44
  - lib/typed_operation/prepared.rb
39
45
  - lib/typed_operation/railtie.rb
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TypedOperation
4
- class AttributeBuilder
5
- def initialize(typed_operation, parameter_name, type_signature, options)
6
- @typed_operation = typed_operation
7
- @name = parameter_name
8
- @signature = type_signature
9
- @optional = options[:optional]
10
- @positional = options[:positional]
11
- @reader = options[:reader] || :public
12
- @default_key = options.key?(:default)
13
- @default = options[:default]
14
-
15
- prepare_type_signature_for_literal
16
- end
17
-
18
- def define(&converter)
19
- @typed_operation.attribute(
20
- @name,
21
- @signature,
22
- default: default_value_for_literal,
23
- positional: @positional,
24
- reader: @reader,
25
- &converter
26
- )
27
- end
28
-
29
- private
30
-
31
- def prepare_type_signature_for_literal
32
- @signature = NilableType.new(@signature) if needs_to_be_nilable?
33
- union_with_nil_to_support_nil_default
34
- validate_positional_order_params! if @positional
35
- end
36
-
37
- # If already wrapped in a Nilable then don't wrap again
38
- def needs_to_be_nilable?
39
- @optional && !type_nilable?
40
- end
41
-
42
- def type_nilable?
43
- @signature.is_a?(NilableType)
44
- end
45
-
46
- def union_with_nil_to_support_nil_default
47
- @signature = Literal::Union.new(@signature, NilClass) if has_default_value_nil?
48
- end
49
-
50
- def has_default_value_nil?
51
- default_provided? && @default.nil?
52
- end
53
-
54
- def validate_positional_order_params!
55
- # Optional ones can always be added after required ones, or before any others, but required ones must be first
56
- unless type_nilable? || @typed_operation.optional_positional_parameters.empty?
57
- raise ParameterError, "Cannot define required positional parameter '#{@name}' after optional positional parameters"
58
- end
59
- end
60
-
61
- def default_provided?
62
- @default_key
63
- end
64
-
65
- def default_value_for_literal
66
- if has_default_value_nil? || type_nilable?
67
- -> {}
68
- else
69
- @default
70
- end
71
- end
72
- end
73
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "literal"
4
-
5
- module TypedOperation
6
- class NilableType < Literal::Union
7
- def initialize(*types)
8
- @types = types + [NilClass]
9
- end
10
- end
11
- end