typed_operation 1.0.0.pre1 → 1.0.0.pre2

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: 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