metaractor 2.1.0 → 3.1.1

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: 6e1ab1a1cd9e5ab11a43462021e80787506646d87cf41816ce67396a3cf5da9d
4
- data.tar.gz: e9b61e4f8181d10ae02d2b84f712f30eb9a36e457d1dac7dff6a43c802b93785
3
+ metadata.gz: 70a949834d71b20665b6acc4e7e820de727a333226e26af6fa4f0c5cfa7c431d
4
+ data.tar.gz: 9e4fa64a34d534f2725a24bb7d3c4e9c963d61a36530a9d4b5543c29193c29fa
5
5
  SHA512:
6
- metadata.gz: eb29390b6649c80867a4455e642cd75bc1fbc6732b583e4441f5fc0a42b3dee14ede6ceaad933322073e3f0dcc328e936e2d9c3f120ea97e75e38acb01cbe1d8
7
- data.tar.gz: faa366cc5ad09fed640918a295696e3089a9ef96de61ce3cb073c0a6ee029be58c73db16bc056f781f9b795c776149437c7cfddc0bd84d2182d7ca5da3dabb96
6
+ metadata.gz: b23927d92d66bd17b109e5519cba3e9a25439e04866d9d5159be09d3bcd2802da5a5c31e01c3c8124bae1f7128fff6d46c0f82f9d3faefe4ddac0955a015a1e3
7
+ data.tar.gz: 9a157b4b2d932b3c8b730ee6ffd11dca110395fb8cf859ea30a13073fb60c89373ab38515d571120ce7814661f00aa24fcdfbbfc4eadffde177387288ee7dc2d
@@ -0,0 +1,92 @@
1
+ env:
2
+ BUILDKITE_PLUGIN_DOCKER_CACHE_S3_BUCKET: "outstand-buildkite-cache"
3
+ # BUILDKITE_PLUGIN_DOCKER_CACHE_VOLUME_DEBUG: "true"
4
+ BUILDKITE_PLUGIN_DOCKER_COMPOSE_SHELL: "false"
5
+ # BUILDKITE_PLUGIN_DOCKER_COMPOSE_UPLOAD_CONTAINER_LOGS: "always"
6
+ BUILDKITE_PLUGIN_DOCKER_COMPOSE_PULL_RETRIES: 5
7
+ BUILDKITE_PLUGIN_DOCKER_COMPOSE_PUSH_RETRIES: 5
8
+ PLUGIN_DOCKER_COMPOSE_VERSION: "17bac3aaee1360e39381b89bb45513b11838238e"
9
+ PLUGIN_DOCKER_CACHE_VERSION: "f3d8feb52a25c69c75565e9f0b80375eae51850a"
10
+
11
+ steps:
12
+ - label: ":docker: Build"
13
+ command: ./docker/ci-prep.sh
14
+ key: build
15
+ plugins:
16
+ - seek-oss/aws-sm#v2.2.1:
17
+ env:
18
+ DOCKER_LOGIN_PASSWORD: "/buildkite/docker_password"
19
+
20
+ - docker-login#v2.0.1:
21
+ username: outstandci
22
+
23
+ - ecr#v2.1.1:
24
+ login: true
25
+ region: "us-east-1"
26
+
27
+ - https://github.com/outstand/docker-compose-buildkite-plugin.git#${PLUGIN_DOCKER_COMPOSE_VERSION}:
28
+ build: metaractor
29
+ image-repository: 786715713882.dkr.ecr.us-east-1.amazonaws.com/ci-images
30
+ config:
31
+ - docker-compose.yml
32
+
33
+ - label: ":bundler: :rubygems:"
34
+ key: bundle_install
35
+ command: bundle install
36
+ depends_on: build
37
+ plugins:
38
+ - seek-oss/aws-sm#v2.2.1:
39
+ env:
40
+ DOCKER_LOGIN_PASSWORD: "/buildkite/docker_password"
41
+
42
+ - docker-login#v2.0.1:
43
+ username: outstandci
44
+
45
+ - ecr#v2.1.1:
46
+ login: true
47
+ region: "us-east-1"
48
+
49
+ - https://github.com/outstand/docker-compose-buildkite-plugin.git#${PLUGIN_DOCKER_COMPOSE_VERSION}:
50
+ run: metaractor
51
+ dependencies: false
52
+ config:
53
+ - docker-compose.yml
54
+
55
+ - https://github.com/outstand/docker-cache-buildkite-plugin.git#${PLUGIN_DOCKER_CACHE_VERSION}:
56
+ name: bundler-cache
57
+ keys:
58
+ - v1-bundler-cache-{{ arch }}-{{ checksum "metaractor.gemspec" }}-{{ checksum "Gemfile" }}
59
+ - v1-bundler-cache-{{ arch }}-
60
+ save: true
61
+ volumes:
62
+ - bundler-data
63
+
64
+ - label: ":ruby: Specs"
65
+ command: rspec spec
66
+ depends_on: bundle_install
67
+ plugins:
68
+ - seek-oss/aws-sm#v2.2.1:
69
+ env:
70
+ DOCKER_LOGIN_PASSWORD: "/buildkite/docker_password"
71
+ json-to-env:
72
+ - secret-id: "/buildkite/rails/env_vars"
73
+
74
+ - docker-login#v2.0.1:
75
+ username: outstandci
76
+
77
+ - ecr#v2.1.1:
78
+ login: true
79
+ region: "us-east-1"
80
+
81
+ - https://github.com/outstand/docker-compose-buildkite-plugin.git#${PLUGIN_DOCKER_COMPOSE_VERSION}:
82
+ run: metaractor
83
+ config:
84
+ - docker-compose.yml
85
+
86
+ - https://github.com/outstand/docker-cache-buildkite-plugin.git#${PLUGIN_DOCKER_CACHE_VERSION}:
87
+ name: bundler-cache
88
+ keys:
89
+ - v1-bundler-cache-{{ arch }}-{{ checksum "metaractor.gemspec" }}-{{ checksum "Gemfile" }}
90
+ - v1-bundler-cache-{{ arch }}-
91
+ volumes:
92
+ - bundler-data
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /Deskfile
data/.rspec CHANGED
@@ -1,2 +1,4 @@
1
1
  --color
2
2
  --require spec_helper
3
+ --format progress
4
+ --format RSpec::Buildkite::AnnotationFormatter
data/Dockerfile CHANGED
@@ -1,4 +1,6 @@
1
- FROM ruby:2.6.5-alpine
1
+ FROM outstand/fixuid as fixuid
2
+
3
+ FROM ruby:2.7.3-alpine
2
4
  LABEL maintainer="Ryan Schlesinger <ryan@outstand.com>"
3
5
 
4
6
  RUN addgroup -g 1000 -S metaractor && \
@@ -11,7 +13,14 @@ RUN addgroup -g 1000 -S metaractor && \
11
13
  git \
12
14
  openssh
13
15
 
14
- ENV BUNDLER_VERSION 2.1.4
16
+ COPY --from=fixuid /usr/local/bin/fixuid /usr/local/bin/fixuid
17
+ RUN chmod 4755 /usr/local/bin/fixuid && \
18
+ USER=metaractor && \
19
+ GROUP=metaractor && \
20
+ mkdir -p /etc/fixuid && \
21
+ printf "user: $USER\ngroup: $GROUP\n" > /etc/fixuid/config.yml
22
+
23
+ ENV BUNDLER_VERSION 2.2.16
15
24
  RUN gem install bundler -v ${BUNDLER_VERSION} -i /usr/local/lib/ruby/gems/$(ls /usr/local/lib/ruby/gems) --force
16
25
 
17
26
  WORKDIR /metaractor
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
- gem 'pry-byebug'
4
+ gem 'rspec-buildkite', github: 'outstand/rspec-buildkite', branch: 'error-output'
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Metaractor
1
+ # Metaractor [![Build status](https://badge.buildkite.com/70063a5154eb7366b8b7fd65a875c5f64301bc60f6d29a2ad7.svg)](https://buildkite.com/outstand/metaractor)
2
2
  Adds parameter validation and error control to [interactor](https://github.com/collectiveidea/interactor).
3
3
 
4
4
  ## Installation
@@ -44,7 +44,7 @@ result.failure?
44
44
  # => true
45
45
  result.valid?
46
46
  # => false
47
- result.errors
47
+ result.error_messages
48
48
  # => ["Required parameters: (user_id or user)"]
49
49
  ```
50
50
 
@@ -68,20 +68,59 @@ Metaractor supports complex required parameter statements and you can chain thes
68
68
  required and: [:token, or: [:recipient_id, :recipient] ]
69
69
  ```
70
70
 
71
+ You can also mark a parameter as required with the `required` option:
72
+ ```ruby
73
+ parameter :user, required: true
74
+ ```
75
+
71
76
  ### Optional Parameters
72
77
  As optional parameters have no enforcement, they are merely advisory.
73
78
  ```ruby
74
79
  optional :enable_logging
75
80
  ```
76
81
 
77
- ### Skipping Blank Parameter Removal
82
+ ### Parameter Options
83
+ Metaractor supports arbitrary parameter options. The following are currently built in.
84
+ Note that you can specify a block of `required` or `optional` parameters and then use
85
+ `parameter` or `parameters` to add options to one or more of them.
86
+
87
+ #### Skipping Blank Parameter Removal
78
88
  By default Metaractor removes blank values that are passed in. You may skip this behavior on a per-parameter basis:
79
89
  ```ruby
80
- allow_blank :name
90
+ parameter :name, allow_blank: true
81
91
  ```
82
92
 
83
93
  You may check to see if a parameter exists via `context.has_key?`.
84
94
 
95
+ #### Default Values
96
+ You can specify a default value for a parameter:
97
+ ```ruby
98
+ optional :role, default: :user
99
+ ```
100
+
101
+ This works with `allow_blank` and can also be anything that responds to `#call`.
102
+ ```ruby
103
+ parameter :role, allow_blank: true, default: -> { context.default_role }
104
+ ```
105
+
106
+ #### Typecasting/Coersion
107
+ You can supply Metaractor with a callable that will typecast incoming parameters:
108
+ ```ruby
109
+ optional :needs_to_be_a_string, type: ->(value) { value.to_s }
110
+ ```
111
+
112
+ You can also configure Metaractor with named types and use them:
113
+ ```ruby
114
+ Metaractor.configure do |config|
115
+ config.register_type(:boolean, ->(value) { ActiveModel::Type::Boolean.new.cast(value) })
116
+ end
117
+ ```
118
+ ```ruby
119
+ required :is_awesome, type: :boolean
120
+ ```
121
+
122
+ **Note**: Typecasters will _not_ be called on `nil` values.
123
+
85
124
  ### Custom Validation
86
125
  Metaractor supports doing custom validation before any user supplied before_hooks run.
87
126
  ```ruby
@@ -161,6 +200,64 @@ result.errors.to_h
161
200
  # }
162
201
  ```
163
202
 
203
+ ### I18n
204
+ As of v3.0.0, metaractor supports i18n along with structured errors.
205
+ ```ruby
206
+ module Users
207
+ class UpdateUser
208
+ include Metaractor
209
+
210
+ optional :is_admin
211
+ optional :user
212
+
213
+ def call
214
+ fail_with_error!(
215
+ errors: {
216
+ base: :invalid_configuration,
217
+ is_admin: :true_or_false,
218
+ user: [ title: :blank, username: [:unique, :blank] ]
219
+ }
220
+ )
221
+ end
222
+ end
223
+ end
224
+ ```
225
+
226
+ Locale:
227
+ ```yaml
228
+ en:
229
+ errors:
230
+ parameters:
231
+ invalid_configuration: 'Invalid configuration'
232
+ blank: '%{parameter} cannot be blank'
233
+ unique: '%{parameter} must be unique'
234
+
235
+ users:
236
+ is_admin:
237
+ true_or_false: 'must be true or false'
238
+ user:
239
+ username:
240
+ unique: 'Username has already been taken'
241
+ ```
242
+
243
+ Metaractor will attempt to use the namespace of the code that reported the error.
244
+ You can see that above with the `users` key in the locale.
245
+
246
+ The i18n integration will walk its way from the most specific message to the least specific one, stopping at the first one it can find.
247
+ We currently expose the following variables for use in the message:
248
+ - `error_key`: the error we added (ex: `blank` or `invalid_configuration`)
249
+ - `parameter`: the name of the parameter
250
+
251
+ You can also use this feature to work with machine readable keys:
252
+ ```ruby
253
+ result = Users::UpdateUser.call
254
+ if result.failure? &&
255
+ result.errors[:is_admin].include?(:true_or_false)
256
+
257
+ # handle this specific case
258
+ end
259
+ ```
260
+
164
261
  ### Spec Helpers
165
262
  Enable the helpers and/or matchers:
166
263
  ```ruby
@@ -221,8 +318,8 @@ expect(result).to include_errors(
221
318
  expect(result).to include_errors('user.title cannot be blank')
222
319
  ```
223
320
 
224
- ### Error Output
225
- Metaractor customizes the exception message for `Interactor::Failure`:
321
+ ### Hash Formatting
322
+ Metaractor customizes the output for `Metaractor::Errors#inspect` and `Interactor::Failure`:
226
323
  ```
227
324
  Interactor::Failure:
228
325
  Errors:
@@ -235,10 +332,12 @@ Interactor::Failure:
235
332
  {:parent=>true, :chained=>true}
236
333
  ```
237
334
 
238
- You can further customize the exception message:
335
+ You can further customize the hash formatting:
239
336
  ```ruby
240
- # Configure FailureOutput to use awesome_print
241
- Metaractor::FailureOutput.hash_formatter = ->(hash) { hash.ai }
337
+ Metaractor.configure do |config|
338
+ # Configure Metaractor to use awesome_print
339
+ config.hash_formatter = ->(hash) { hash.ai }
340
+ end
242
341
  ```
243
342
 
244
343
  ### Further Reading
data/docker-compose.yml CHANGED
@@ -5,6 +5,13 @@ services:
5
5
  image: outstand/metaractor:dev
6
6
  stdin_open: true
7
7
  tty: true
8
+ environment:
9
+ FIXUID:
10
+ FIXGID:
11
+ BUILDKITE:
12
+ BUILDKITE_BUILD_URL:
13
+ BUILDKITE_JOB_ID:
14
+ BUILDKITE_AGENT_ACCESS_TOKEN:
8
15
  volumes:
9
16
  - bundler-data:/usr/local/bundle
10
17
  - .:/metaractor
@@ -13,6 +20,9 @@ services:
13
20
  stdin_open: true
14
21
  tty: true
15
22
  command: rake release
23
+ environment:
24
+ FIXUID:
25
+ FIXGID:
16
26
  volumes:
17
27
  - bundler-data:/usr/local/bundle
18
28
  - ~/.gitconfig:/home/metaractor/.gitconfig
data/docker-entrypoint.sh CHANGED
@@ -1,6 +1,19 @@
1
1
  #!/bin/sh
2
+
2
3
  set -e
3
4
 
5
+ su-exec ${FIXUID:?Missing FIXUID var}:${FIXGID:?Missing FIXGID var} fixuid
6
+
7
+ chown_dir() {
8
+ dir=$1
9
+ if [[ -d ${dir} ]] && [[ "$(stat -c %u:%g ${dir})" != "${FIXUID}:${FIXGID}" ]]; then
10
+ echo chown $dir
11
+ chown metaractor:metaractor $dir
12
+ fi
13
+ }
14
+
15
+ chown_dir /usr/local/bundle
16
+
4
17
  if [ "$(which "$1")" = '' ]; then
5
18
  if [ "$(ls -A /usr/local/bundle/bin)" = '' ]; then
6
19
  echo 'command not in path and bundler not initialized'
data/lib/metaractor.rb CHANGED
@@ -10,6 +10,8 @@ require 'metaractor/chain_failures'
10
10
  require 'metaractor/fail_from_context'
11
11
  require 'metaractor/context_has_key'
12
12
  require 'metaractor/failure_output'
13
+ require 'i18n'
14
+ require 'metaractor/namespace'
13
15
 
14
16
  module Metaractor
15
17
  def self.included(base)
@@ -43,7 +45,8 @@ module Metaractor
43
45
  { module: Metaractor::HandleErrors, method: :include },
44
46
  { module: Metaractor::Parameters, method: :include },
45
47
  { module: Metaractor::RunWithContext, method: :include },
46
- { module: Metaractor::ChainFailures, method: :include }
48
+ { module: Metaractor::ChainFailures, method: :include },
49
+ { module: Metaractor::Namespace, method: :include }
47
50
  ]
48
51
  end
49
52
 
@@ -54,4 +57,36 @@ module Metaractor
54
57
  def self.prepend_module(mod)
55
58
  modules << { module: mod, method: :prepend }
56
59
  end
60
+
61
+ def self.format_hash(hash)
62
+ if @hash_formatter.nil?
63
+ @hash_formatter = default_hash_formatter
64
+ end
65
+
66
+ @hash_formatter.call(hash)
67
+ end
68
+
69
+ def self.default_hash_formatter
70
+ ->(hash){ hash.inspect }
71
+ end
72
+
73
+ def self.hash_formatter
74
+ @hash_formatter
75
+ end
76
+
77
+ def self.hash_formatter=(callable)
78
+ @hash_formatter = callable
79
+ end
80
+
81
+ def self.types
82
+ @types ||= {}
83
+ end
84
+
85
+ def self.register_type(type, callable)
86
+ types[type] = callable
87
+ end
88
+
89
+ def self.clear_types!
90
+ @types = {}
91
+ end
57
92
  end
@@ -8,29 +8,29 @@ module Metaractor
8
8
  super
9
9
  end
10
10
 
11
- def fail_with_error!(message: nil, errors: nil)
12
- add_error(message: message, errors: errors)
11
+ def fail_with_error!(message: nil, errors: nil, **args)
12
+ add_error(message: message, errors: errors, **args)
13
13
  fail!
14
14
  end
15
15
 
16
- def fail_with_errors!(messages: [], errors: {})
17
- add_errors(messages: messages, errors: errors)
16
+ def fail_with_errors!(messages: [], errors: {}, **args)
17
+ add_errors(messages: messages, errors: errors, **args)
18
18
  fail!
19
19
  end
20
20
 
21
- def add_error(message: nil, errors: nil)
21
+ def add_error(message: nil, errors: nil, **args)
22
22
  if message.nil?
23
- add_errors(errors: errors)
23
+ add_errors(errors: errors, **args)
24
24
  else
25
- add_errors(messages: Array(message))
25
+ add_errors(messages: Array(message), **args)
26
26
  end
27
27
  end
28
28
 
29
- def add_errors(messages: [], errors: {})
29
+ def add_errors(messages: [], errors: {}, **args)
30
30
  if !messages.empty?
31
- self.errors.add(errors: { base: messages })
31
+ self.errors.add(errors: { base: messages }, **args)
32
32
  else
33
- self.errors.add(errors: errors)
33
+ self.errors.add(errors: errors, **args)
34
34
  end
35
35
  end
36
36
 
@@ -4,13 +4,72 @@ module Metaractor
4
4
  class Errors
5
5
  extend Forwardable
6
6
 
7
+ class Error
8
+ attr_reader :value, :object
9
+
10
+ def initialize(value:, object: nil)
11
+ @value = value
12
+ @object = object
13
+ end
14
+
15
+ def generate_message(path_elements:)
16
+ if @value.is_a? Symbol
17
+ defaults = []
18
+
19
+ if object.class.respond_to?(:i18n_parent_names) &&
20
+ !object.class.i18n_parent_names.empty?
21
+
22
+ names = object.class.i18n_parent_names
23
+ until names.empty?
24
+ defaults << ['errors', names.join('.'), 'parameters', path_elements.join('.'), @value.to_s].reject do |item|
25
+ item.nil? || item == ''
26
+ end.join('.').to_sym
27
+ names.pop
28
+ end
29
+ end
30
+
31
+ unless path_elements.empty?
32
+ defaults << :"errors.parameters.#{path_elements.join('.')}.#{@value}"
33
+ end
34
+ defaults << :"errors.parameters.#{@value}"
35
+
36
+ key = defaults.shift
37
+ I18n.translate(
38
+ key,
39
+ default: defaults,
40
+ error_key: @value,
41
+ parameter: path_elements.last
42
+ )
43
+ else
44
+ "#{path_elements.join('.')} #{@value}".lstrip
45
+ end
46
+ end
47
+
48
+ def ==(other)
49
+ if other.is_a?(self.class)
50
+ @value == other.value
51
+ else
52
+ @value == other
53
+ end
54
+ end
55
+ alias eql? ==
56
+
57
+ def hash
58
+ @value.hash
59
+ end
60
+
61
+ def inspect
62
+ "(Error) #{@value.inspect}"
63
+ end
64
+ end
65
+
7
66
  def initialize
8
67
  @tree = Sycamore::Tree.new
9
68
  end
10
69
 
11
- def_delegators :@tree, :to_h, :empty?
70
+ def_delegators :@tree, :empty?
12
71
 
13
- def add(error: {}, errors: {})
72
+ def add(error: {}, errors: {}, object: nil)
14
73
  trees = []
15
74
  [error, errors].each do |h|
16
75
  tree = nil
@@ -29,7 +88,17 @@ module Metaractor
29
88
  end
30
89
 
31
90
  trees.each do |tree|
32
- @tree.add(tree)
91
+ tree.each_path do |path|
92
+ node = path.node
93
+ unless node.is_a?(Error)
94
+ node = Error.new(
95
+ value: path.node,
96
+ object: object
97
+ )
98
+ end
99
+
100
+ @tree[path.parent] << node
101
+ end
33
102
  end
34
103
  @tree.compact
35
104
  end
@@ -58,9 +127,9 @@ module Metaractor
58
127
  result = @tree.dig(*path)
59
128
 
60
129
  if result.strict_leaves?
61
- result.nodes
130
+ unwrapped_enum(result.nodes)
62
131
  else
63
- result.to_h
132
+ unwrapped_tree(result).to_h
64
133
  end
65
134
  end
66
135
  alias [] dig
@@ -68,9 +137,15 @@ module Metaractor
68
137
  def include?(*elements)
69
138
  if elements.size == 1 &&
70
139
  elements.first.is_a?(Hash)
71
- @tree.include?(*elements)
140
+ unwrapped_tree.include?(*elements)
72
141
  else
73
- full_messages.include?(*elements)
142
+ if elements.all? {|e| e.is_a? String }
143
+ full_messages.include?(*elements)
144
+ else
145
+ elements.all? do |element|
146
+ @tree.include_path?(element)
147
+ end
148
+ end
74
149
  end
75
150
  end
76
151
 
@@ -83,7 +158,29 @@ module Metaractor
83
158
  end
84
159
  end
85
160
 
86
- new_tree.to_h
161
+ unwrapped_tree(new_tree).to_h
162
+ end
163
+
164
+ def to_h(unwrap: true)
165
+ if unwrap
166
+ unwrapped_tree.to_h
167
+ else
168
+ @tree.to_h
169
+ end
170
+ end
171
+ alias to_hash to_h
172
+
173
+ def inspect
174
+ str = "<##{self.class.name}: "
175
+
176
+ if !self.empty?
177
+ str << "Errors:\n"
178
+ str << Metaractor.format_hash(to_h(unwrap: false))
179
+ str << "\n"
180
+ end
181
+
182
+ str << ">"
183
+ str
87
184
  end
88
185
 
89
186
  private
@@ -96,7 +193,32 @@ module Metaractor
96
193
  end
97
194
  end
98
195
 
99
- "#{path_elements.join('.')} #{path.node.to_s}".lstrip
196
+ path.node.generate_message(path_elements: path_elements)
100
197
  end
198
+
199
+ def unwrapped_tree(orig_tree = @tree)
200
+ tree = Sycamore::Tree.new
201
+ orig_tree.each_path do |path|
202
+ node = path.node
203
+ if node.is_a? Error
204
+ node = node.value
205
+ end
206
+
207
+ tree[path.parent] << node
208
+ end
209
+
210
+ tree
211
+ end
212
+
213
+ def unwrapped_enum(orig)
214
+ orig.map do |element|
215
+ if element.is_a? Error
216
+ element.value
217
+ else
218
+ element
219
+ end
220
+ end
221
+ end
222
+
101
223
  end
102
224
  end
@@ -1,23 +1,11 @@
1
1
  module Metaractor
2
2
  module FailureOutput
3
- def self.format_hash(hash)
4
- if @hash_formatter.nil?
5
- @hash_formatter = ->(hash){ hash.inspect }
6
- end
7
-
8
- @hash_formatter.call(hash)
9
- end
10
-
11
- def self.hash_formatter=(callable)
12
- @hash_formatter = callable
13
- end
14
-
15
3
  def to_s
16
4
  str = ''
17
5
 
18
6
  if !context.errors.empty?
19
7
  str << "Errors:\n"
20
- str << Metaractor::FailureOutput.format_hash(context.errors.to_h)
8
+ str << Metaractor.format_hash(context.errors.to_h)
21
9
  str << "\n\n"
22
10
  end
23
11
 
@@ -25,12 +13,13 @@ module Metaractor
25
13
  str << "Previously Called:\n"
26
14
  context._called.each do |interactor|
27
15
  str << interactor.class.name.to_s
16
+ str << "\n"
28
17
  end
29
- str << "\n\n"
18
+ str << "\n"
30
19
  end
31
20
 
32
21
  str << "Context:\n"
33
- str << Metaractor::FailureOutput.format_hash(context.to_h.reject{|k,_| k == :errors})
22
+ str << Metaractor.format_hash(context.to_h.reject{|k,_| k == :errors})
34
23
  str
35
24
  end
36
25
  end
@@ -3,20 +3,20 @@ module Metaractor
3
3
  class InvalidError < Error; end
4
4
 
5
5
  module HandleErrors
6
- def fail_with_error!(*args)
7
- context.fail_with_error!(*args)
6
+ def fail_with_error!(**args)
7
+ context.fail_with_error!(object: self, **args)
8
8
  end
9
9
 
10
- def fail_with_errors!(*args)
11
- context.fail_with_errors!(*args)
10
+ def fail_with_errors!(**args)
11
+ context.fail_with_errors!(object: self, **args)
12
12
  end
13
13
 
14
- def add_error(*args)
15
- context.add_error(*args)
14
+ def add_error(**args)
15
+ context.add_error(object: self, **args)
16
16
  end
17
17
 
18
- def add_errors(*args)
19
- context.add_errors(*args)
18
+ def add_errors(**args)
19
+ context.add_errors(object: self, **args)
20
20
  end
21
21
 
22
22
  def error_messages
@@ -0,0 +1,46 @@
1
+ module Metaractor
2
+ module Namespace
3
+ # The following code is adapted from rails.
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def module_parent_name
11
+ if defined?(@parent_name)
12
+ @parent_name
13
+ else
14
+ parent_name = name =~ /::[^:]+\z/ ? -$` : nil
15
+ @parent_name = parent_name unless frozen?
16
+ parent_name
17
+ end
18
+ end
19
+
20
+ def module_parent_names
21
+ parents = []
22
+ if module_parent_name
23
+ parents = module_parent_name.split("::")
24
+ end
25
+ parents
26
+ end
27
+
28
+ def i18n_parent_names
29
+ module_parent_names.map {|name| underscore_module_name(name).to_sym }
30
+ end
31
+
32
+ private
33
+
34
+ def underscore_module_name(camel_cased_word)
35
+ return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
36
+ word = camel_cased_word.to_s.gsub("::", "/")
37
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
38
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
39
+ word.tr!("-", "_")
40
+ word.downcase!
41
+ word
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -1,3 +1,5 @@
1
+ # Special thanks to the `hashie` and `active_attr` gems for code and inspiration.
2
+
1
3
  module Metaractor
2
4
  module Parameters
3
5
  def self.included(base)
@@ -6,40 +8,97 @@ module Metaractor
6
8
  include Metaractor::HandleErrors
7
9
 
8
10
  class << self
9
- attr_writer :_required_parameters
10
- attr_writer :_optional_parameters
11
- attr_writer :_allow_blank
11
+ attr_writer :requirement_trees
12
12
  end
13
13
 
14
14
  before :remove_blank_values
15
+ before :apply_defaults
15
16
  before :validate_required_parameters
17
+ before :apply_types
18
+ end
19
+ end
20
+
21
+ class Parameter
22
+ include Comparable
23
+
24
+ attr_reader :name
25
+
26
+ def initialize(name, **options)
27
+ @name = name.to_sym
28
+ @options = options
29
+ end
30
+
31
+ def <=>(other)
32
+ return nil unless other.instance_of? self.class
33
+ return nil if name == other.name && options != other.options
34
+ self.name.to_s <=> other.name.to_s
35
+ end
36
+
37
+ def [](key)
38
+ @options[key]
39
+ end
40
+
41
+ def has_key?(key)
42
+ @options.has_key?(key)
43
+ end
44
+
45
+ def dig(name, *names)
46
+ @options.dig(name, *names)
16
47
  end
48
+
49
+ def merge!(**options)
50
+ @options.merge!(**options)
51
+ end
52
+
53
+ def to_s
54
+ name.to_s
55
+ end
56
+
57
+ def to_sym
58
+ name
59
+ end
60
+
61
+ protected
62
+ attr_reader :options
17
63
  end
18
64
 
19
65
  module ClassMethods
20
- def _required_parameters
21
- @_required_parameters ||= []
66
+ def parameter(name, **options)
67
+ if param = self.parameter_hash[name.to_sym]
68
+ param.merge!(**options)
69
+ else
70
+ Parameter.new(name, **options).tap do |parameter|
71
+ self.parameter_hash[parameter.name] = parameter
72
+ end
73
+ end
22
74
  end
23
75
 
24
- def required(*params)
25
- self._required_parameters += params
76
+ def parameters(*names, **options)
77
+ names.each do |name|
78
+ parameter(name, **options)
79
+ end
26
80
  end
27
- alias_method :required_parameters, :required
28
81
 
29
- def _optional_parameters
30
- @_optional_parameters ||= []
82
+ def parameter_hash
83
+ @parameters ||= {}
31
84
  end
32
85
 
33
- def optional(*params)
34
- self._optional_parameters += params
86
+ def requirement_trees
87
+ @requirement_trees ||= []
35
88
  end
36
89
 
37
- def _allow_blank
38
- @_allow_blank ||= []
90
+ def required(*params, **options)
91
+ if params.empty?
92
+ tree = options
93
+ self.requirement_trees << tree
94
+ parameters(*parameters_in_tree(tree), required: tree)
95
+ else
96
+ parameters(*params, required: true, **options)
97
+ end
39
98
  end
40
99
 
41
- def allow_blank(*params)
42
- self._allow_blank += params
100
+ def optional(*params, **options)
101
+ parameters(*params, **options)
43
102
  end
44
103
 
45
104
  def validate_parameters(*hooks, &block)
@@ -50,26 +109,91 @@ module Metaractor
50
109
  def validate_hooks
51
110
  @validate_hooks ||= []
52
111
  end
112
+
113
+ def parameters_in_tree(tree)
114
+ if tree.respond_to?(:to_h)
115
+ tree.to_h.values.first.to_a.flat_map {|t| parameters_in_tree(t)}
116
+ else
117
+ [tree]
118
+ end
119
+ end
120
+ end
121
+
122
+ def parameters
123
+ self.class.parameter_hash
124
+ end
125
+
126
+ def requirement_trees
127
+ self.class.requirement_trees
128
+ end
129
+
130
+ def requirement_trees=(trees)
131
+ self.class.requirement_trees=(trees)
53
132
  end
54
133
 
55
134
  def remove_blank_values
56
135
  to_delete = []
57
- context.each_pair do |k,v|
58
- next if self.class._allow_blank.include?(k)
136
+ context.each_pair do |name, value|
137
+ next if parameters.dig(name, :allow_blank)
59
138
 
60
139
  # The following regex is borrowed from Rails' String#blank?
61
- to_delete << k if (v.is_a?(String) && /\A[[:space:]]*\z/ === v) || v.nil?
140
+ to_delete << name if (value.is_a?(String) && /\A[[:space:]]*\z/ === value) || value.nil?
141
+ end
142
+
143
+ to_delete.each do |name|
144
+ context.delete_field name
145
+ end
146
+ end
147
+
148
+ def apply_defaults
149
+ parameters.each do |name, parameter|
150
+ next unless parameter.has_key?(:default)
151
+
152
+ unless context.has_key?(name)
153
+ context[name] = _parameter_default(name)
154
+ end
62
155
  end
63
- to_delete.each do |k|
64
- context.delete_field k
156
+ end
157
+
158
+ def _parameter_default(name)
159
+ default = self.parameters[name][:default]
160
+
161
+ case
162
+ when default.respond_to?(:call) then instance_exec(&default)
163
+ when default.respond_to?(:dup) then default.dup
164
+ else default
165
+ end
166
+ end
167
+
168
+ def apply_types
169
+ parameters.each do |name, parameter|
170
+ next unless parameter[:type]
171
+
172
+ if context.has_key?(name) && context[name] != nil
173
+ callable = parameter[:type]
174
+
175
+ if callable.is_a?(Symbol)
176
+ callable = Metaractor.types[callable]
177
+ raise ArgumentError, "No such type: #{parameter[:type]}" if callable.nil?
178
+ end
179
+
180
+ context[name] = callable.call(context[name])
181
+ end
65
182
  end
66
183
  end
67
184
 
68
185
  def validate_required_parameters
69
186
  context.errors ||= []
70
187
 
71
- self.class._required_parameters.each do |param|
72
- require_parameter param
188
+ parameters.each do |name, parameter|
189
+ next if !parameter[:required] ||
190
+ parameter[:required].is_a?(Hash)
191
+
192
+ require_parameter name
193
+ end
194
+
195
+ requirement_trees.each do |tree|
196
+ require_parameter tree
73
197
  end
74
198
 
75
199
  run_validate_hooks
@@ -1,3 +1,3 @@
1
1
  module Metaractor
2
- VERSION = "2.1.0"
2
+ VERSION = "3.1.1"
3
3
  end
data/metaractor.gemspec CHANGED
@@ -11,7 +11,10 @@ Gem::Specification.new do |spec|
11
11
  spec.email = ['ryan@outstand.com']
12
12
 
13
13
  spec.summary = %q{Adds parameter validation and error control to interactor}
14
- spec.homepage = 'https://github.com/outstand/metaractor'
14
+ spec.metadata = {
15
+ "homepage_uri" => "https://github.com/outstand/metaractor",
16
+ "source_code_uri" => "https://github.com/outstand/metaractor"
17
+ }
15
18
 
16
19
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
20
  spec.bindir = 'exe'
@@ -20,8 +23,12 @@ Gem::Specification.new do |spec|
20
23
 
21
24
  spec.add_runtime_dependency 'interactor', '~> 3.1'
22
25
  spec.add_runtime_dependency 'sycamore', '~> 0.3'
26
+ spec.add_runtime_dependency 'i18n', '~> 1.8'
23
27
 
24
28
  spec.add_development_dependency 'bundler', '~> 2'
25
29
  spec.add_development_dependency 'rake', '~> 13.0'
26
30
  spec.add_development_dependency 'rspec', '~> 3.9'
31
+ spec.add_development_dependency 'awesome_print', '~> 1.8'
32
+ spec.add_development_dependency 'pry-byebug', '~> 3.9'
33
+ spec.add_development_dependency 'activemodel', '~> 6.1'
27
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metaractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Schlesinger
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-09 00:00:00.000000000 Z
11
+ date: 2021-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: interactor
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: i18n
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: bundler
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -80,13 +94,56 @@ dependencies:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
96
  version: '3.9'
83
- description:
97
+ - !ruby/object:Gem::Dependency
98
+ name: awesome_print
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.8'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.8'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry-byebug
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.9'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.9'
125
+ - !ruby/object:Gem::Dependency
126
+ name: activemodel
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '6.1'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '6.1'
139
+ description:
84
140
  email:
85
141
  - ryan@outstand.com
86
142
  executables: []
87
143
  extensions: []
88
144
  extra_rdoc_files: []
89
145
  files:
146
+ - ".buildkite/pipeline.yml"
90
147
  - ".gitignore"
91
148
  - ".rspec"
92
149
  - Dockerfile
@@ -105,16 +162,19 @@ files:
105
162
  - lib/metaractor/fail_from_context.rb
106
163
  - lib/metaractor/failure_output.rb
107
164
  - lib/metaractor/handle_errors.rb
165
+ - lib/metaractor/namespace.rb
108
166
  - lib/metaractor/parameters.rb
109
167
  - lib/metaractor/run_with_context.rb
110
168
  - lib/metaractor/spec.rb
111
169
  - lib/metaractor/version.rb
112
170
  - metaractor.gemspec
113
- homepage: https://github.com/outstand/metaractor
171
+ homepage:
114
172
  licenses:
115
173
  - Apache-2.0
116
- metadata: {}
117
- post_install_message:
174
+ metadata:
175
+ homepage_uri: https://github.com/outstand/metaractor
176
+ source_code_uri: https://github.com/outstand/metaractor
177
+ post_install_message:
118
178
  rdoc_options: []
119
179
  require_paths:
120
180
  - lib
@@ -129,8 +189,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
189
  - !ruby/object:Gem::Version
130
190
  version: '0'
131
191
  requirements: []
132
- rubygems_version: 3.0.3
133
- signing_key:
192
+ rubygems_version: 3.1.6
193
+ signing_key:
134
194
  specification_version: 4
135
195
  summary: Adds parameter validation and error control to interactor
136
196
  test_files: []