portrayal 0.3.1 → 0.7.1

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: 577a51dc874a79a5cb7aece4157c2d802005f3fa9c3daba4b807db522c7cd7ae
4
- data.tar.gz: b30f1ea6a9438ba5b4574a68935602fce1dd0a447d15dce4227d0a67a10b2e87
3
+ metadata.gz: afdba4b2e460ce31fb430639e1270683d61d2b41bcb276a056e042ac8de8c61d
4
+ data.tar.gz: 39f66cc7e9224d04660d3cc1e1317f9c66752e2fd739eeb9068f10751275f9f8
5
5
  SHA512:
6
- metadata.gz: a02d197cb3c56e88afcda74a58d5d7034ef4739de707380c05b7a1a76c95e8ae1c8aa9341fde97d8efa69e6b5fcc00df429b2ada5e1b18f5dc9c2c51cc73504a
7
- data.tar.gz: 4c6f5e4d359551e7420dd4331192f857f8a4518ab679b8052f36c6854a34a100a97bd4af98358589dcd08144cff9ddda071f6b2bd4602dc7495d2fc8467c6994
6
+ metadata.gz: 894bcfa84622297c663f9fa88c4f829a8475fa092caff07d047768adef1a110e5726747f53a284e6d823c57ead388a14fc4e5d0bb68342c463a170a725fa9442
7
+ data.tar.gz: 19dc417df48b783a7847957fdc8899c143b0e92ac4aaef35f1b07342d7d02fbea1091d504d9d47318c7baee0cf538616209555903307f5e8aace2267fd777fce
@@ -1,24 +1,18 @@
1
1
  name: RSpec
2
-
3
- on:
4
- push:
5
- branches: [ master ]
6
- pull_request:
7
- branches: [ master ]
8
-
2
+ on: [push, pull_request]
9
3
  jobs:
10
4
  test:
11
5
  runs-on: ubuntu-latest
12
6
  strategy:
7
+ fail-fast: false
13
8
  matrix:
14
- ruby: [ '2.4', '2.5', '2.6', '2.7' ]
9
+ ruby: [ '2.4', '2.5', '2.6', '2.7', '3.0' ]
15
10
 
16
11
  name: Ruby ${{ matrix.ruby }}
17
12
  steps:
18
13
  - uses: actions/checkout@v2
19
- - uses: actions/setup-ruby@v1
14
+ - uses: ruby/setup-ruby@v1
20
15
  with:
21
16
  ruby-version: ${{ matrix.ruby }}
22
- - run: gem install bundler
23
- - run: bundle install
17
+ bundler-cache: true
24
18
  - run: bundle exec rake
data/.gitignore CHANGED
@@ -7,5 +7,8 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
 
10
+ # Recommended for gems
11
+ /Gemfile.lock
12
+
10
13
  # rspec failure tracking
11
14
  .rspec_status
data/CHANGELOG.md CHANGED
@@ -2,11 +2,50 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.1 - 2021-03-22
6
+
7
+ * Fix default procs' behavior when overriding keywords in subclasses. Portrayal relies on an ordered ruby hash to initialize keywords in the correct order. However, if overriding the same keyword in a subclass (by declaring it again), it didn't move keyword to the bottom of the hash, so this would happen:
8
+
9
+ ```ruby
10
+ class Person
11
+ extend Portrayal
12
+ keyword :email, default: nil
13
+ end
14
+
15
+ class Employee < Person
16
+ keyword :employee_id
17
+ keyword :email, default: proc { "#{employee_id}@example.com" }
18
+ end
19
+
20
+ employee = Employee.new(employee_id: '1234')
21
+ employee.email # => "@example.com"
22
+ ```
23
+
24
+ The email is broken because it relies on having employee_id declared before email, but email was already declared first in the superclass. This change fixes situations like this by re-adding the keyword to the bottom of the hash on every re-declaration.
25
+
26
+ ## 0.7.0 - 2020-12-13
27
+
28
+ * **Breaking change:** Remove `optional` setting. To update find all `optional: true` and change to `default: nil` instead.
29
+
30
+ * **Breaking change:** Move `self` of default procs to `initialize` context. Before this change, default procs used to be executed naively in class context. Now they can access other keyword readers and instance methods since their `self` is now coming from `initialize`. To update, look through your default procs and replace any reference to current class's methods such as `method_name` with `self.class.method_name`.
31
+
32
+ ## 0.6.0 - 2020-08-10
33
+
34
+ * Return keyword name from `keyword`, allowing usage such as `private keyword :foo`. [[commit]](https://github.com/scottscheapflights/portrayal/commit/9e9db2cafc7eae14789c5b84f70efd18898ace76)
35
+
36
+ ## 0.5.0 - 2020-05-28
37
+
38
+ * Add option `define` for overriding nested class name. [[commit]](https://github.com/scottscheapflights/portrayal/commit/665ad297fb71fcdf5f641c672a457ccbe29e4a49)
39
+
40
+ ## 0.4.0 - 2020-05-16
41
+
42
+ * Portrayal schema is deep-duped to subclasses. [[commit]](https://github.com/scottscheapflights/portrayal/commit/f346483a379ce9fbdece72cde8b0844f2d22b1cd)
43
+
5
44
  ## 0.3.1 - 2020-05-11
6
45
 
7
- * Fix the issue introduced in 0.3.0 where `==` and `eql?` were always treating rhs as another portrayal class.
46
+ * Fix the issue introduced in 0.3.0 where `==` and `eql?` were always treating rhs as another portrayal class. [[commit]](https://github.com/scottscheapflights/portrayal/commit/f6ec8f373c6582f7e8d8f872d289222e4a58f8f6)
8
47
 
9
- ## 0.3.0 - 2020-05-09
48
+ ## 0.3.0 - 2020-05-09 (yanked)
10
49
 
11
50
  * No longer compare classes in `==`, use `eql?` for that. [[commit]](https://github.com/scottscheapflights/portrayal/commit/9c5a37e4fb91e35d23b22e208344452930452af7)
12
51
  * Define a protected writer for every keyword - useful when applying changes after `dup`/`clone`. [[commit]](https://github.com/scottscheapflights/portrayal/commit/1c0fa6c6357a09760dae39165e864238d231a08e)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ![RSpec](https://github.com/scottscheapflights/portrayal/workflows/RSpec/badge.svg?branch=master)
1
+ ![RSpec](https://github.com/scottscheapflights/portrayal/workflows/RSpec/badge.svg)
2
2
 
3
3
  # Portrayal
4
4
 
@@ -8,14 +8,14 @@ Inspired by:
8
8
  - Piotr Solnica's [virtus](https://github.com/solnic/virtus)
9
9
  - Everything [Michel Martens](https://github.com/soveran)
10
10
 
11
- Portrayal is a minimalist gem (~115 loc, no dependencies) for building struct-like classes. It provides a small yet powerful step up from plain ruby with its one and only `keyword` method.
11
+ Portrayal is a minimalist gem (~120 loc, no dependencies) for building struct-like classes. It provides a small yet powerful step up from plain ruby with its one and only `keyword` method.
12
12
 
13
13
  ```ruby
14
14
  class Person < MySuperClass
15
15
  extend Portrayal
16
16
 
17
17
  keyword :name
18
- keyword :age, optional: true
18
+ keyword :age, default: nil
19
19
  keyword :favorite_fruit, default: 'feijoa'
20
20
 
21
21
  keyword :address do
@@ -172,7 +172,7 @@ class Address < ApplicationStruct
172
172
  keyword :street
173
173
  keyword :city
174
174
  keyword :postcode
175
- keyword :country, optional: true
175
+ keyword :country, default: nil
176
176
  end
177
177
  ```
178
178
 
@@ -198,6 +198,115 @@ Any other value works as normal.
198
198
  keyword :foo, default: 4
199
199
  ```
200
200
 
201
+ #### Default procs
202
+
203
+ Default procs are executed as though they were called in your class's `initialize`, so they have access to other keywords and instance methods.
204
+
205
+ ```ruby
206
+ keyword :name
207
+ keyword :greeting, default: proc { "Hello, #{name}" }
208
+ ```
209
+
210
+ Defaults can also use results of other defaults.
211
+
212
+ ```ruby
213
+ keyword :four, default: proc { 2 + 2 }
214
+ keyword :eight, default: proc { four * 2 }
215
+ ```
216
+
217
+ Or instance methods of the class.
218
+
219
+ ```ruby
220
+ keyword :id, default: proc { generate_id }
221
+
222
+ private
223
+
224
+ def generate_id
225
+ SecureRandom.alphanumeric
226
+ end
227
+ ```
228
+
229
+ Note: The order in which you declare keywords matters when specifying defaults that depend on other keywords. This will not have the desired effect:
230
+
231
+ ```ruby
232
+ keyword :greeting, default: proc { "Hello, #{name}" }
233
+ keyword :name
234
+ ```
235
+
236
+ ### Nested Classes
237
+
238
+ When you pass a block to a keyword, it creates a nested class named after camelized keyword name.
239
+
240
+ ```ruby
241
+ class Person
242
+ extend Portrayal
243
+
244
+ keyword :address do
245
+ keyword :street
246
+ end
247
+ end
248
+ ```
249
+
250
+ The above block created class `Person::Address`.
251
+
252
+ If you want to change the name of the created class, use the option `define`.
253
+
254
+ ```ruby
255
+ class Person
256
+ extend Portrayal
257
+
258
+ keyword :visited_countries, define: 'Country' do
259
+ keyword :name
260
+ end
261
+ end
262
+ ```
263
+
264
+ This defines `Person::Country`, while the accessor remains `visited_countries`.
265
+
266
+ ### Subclassing
267
+
268
+ Portrayal supports subclassing.
269
+
270
+ ```ruby
271
+ class Person
272
+ extend Portrayal
273
+
274
+ class << self
275
+ def from_contact(contact)
276
+ new name: contact.full_name,
277
+ address: contact.address.to_s,
278
+ email: contact.email
279
+ end
280
+ end
281
+
282
+ keyword :name
283
+ keyword :address
284
+ keyword :email, default: nil
285
+ end
286
+ ```
287
+
288
+ ```ruby
289
+ class Employee < Person
290
+ keyword :employee_id
291
+ keyword :email, default: proc { "#{employee_id}@example.com" }
292
+ end
293
+ ```
294
+
295
+ Now when you call `Employee.new` it will accept keywords of both superclass and subclass. You can also see how `email`'s default is overridden in the subclass.
296
+
297
+ However, if you try calling `Employee.from_contact(contact)` it will error out, because that constructor doesn't set an `employee_id` required in the subclass. You can remedy that with a small change.
298
+
299
+ ```ruby
300
+ def from_contact(contact, **kwargs)
301
+ new name: contact.full_name,
302
+ address: contact.address.to_s,
303
+ email: contact.email,
304
+ **kwargs
305
+ end
306
+ ```
307
+
308
+ If you add `**kwargs` to `Person.from_contact` and pass them through to new, then you are now able to call `Employee.from_contact(contact, employee_id: 'some_id')`
309
+
201
310
  ### Schema
202
311
 
203
312
  Every class that has at least one keyword defined in it automatically receives a class method called `portrayal`. This method is a schema of your object with some additional helpers.
@@ -224,7 +333,7 @@ Address.portrayal.attributes(address) # => {street: '34th st', city: 'NYC', post
224
333
  Get everything portrayal knows about your keywords in one hash.
225
334
 
226
335
  ```ruby
227
- Address.portrayal.schema # => {:street=>{:optional=>false, :default=>nil}, :city=>{:optional=>false, :default=>nil}, :postcode=>{:optional=>false, :default=>nil}, :country=>{:optional=>true, :default=>[:return, nil]}}
336
+ Address.portrayal.schema # => {:street=>nil, :city=>nil, :postcode=>nil, :country=><Portrayal::Default @value=nil @callable=false>}
228
337
  ```
229
338
 
230
339
  ## Philosophy
@@ -258,7 +367,7 @@ class Address < ApplicationStruct
258
367
  keyword :street
259
368
  keyword :city
260
369
  keyword :postcode
261
- keyword :country, optional: true
370
+ keyword :country, default: nil
262
371
  end
263
372
  ```
264
373
 
@@ -301,7 +410,7 @@ class Address < ApplicationStruct
301
410
  keyword :street
302
411
  keyword :city
303
412
  keyword :postcode
304
- keyword :country, optional: true
413
+ keyword :country, default: nil
305
414
  end
306
415
  ```
307
416
 
@@ -330,4 +439,4 @@ The gem is available as open source under the terms of the [Apache License Versi
330
439
 
331
440
  ## Code of Conduct
332
441
 
333
- Everyone interacting in the Portrayal project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/scottscheapflights/portrayal/blob/master/CODE_OF_CONDUCT.md).
442
+ Everyone interacting in the Portrayal project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/scottscheapflights/portrayal/blob/main/CODE_OF_CONDUCT.md).
data/lib/portrayal.rb CHANGED
@@ -4,23 +4,30 @@ require 'portrayal/schema'
4
4
  module Portrayal
5
5
  NULL = :_portrayal_value_not_set
6
6
 
7
- def keyword(name, optional: NULL, default: NULL, &block)
7
+ def keyword(name, default: NULL, define: nil, &block)
8
8
  unless respond_to?(:portrayal)
9
- class << self; attr_reader :portrayal end
9
+ class << self
10
+ attr_reader :portrayal
11
+ def inherited(base)
12
+ base.instance_variable_set('@portrayal', portrayal.dup)
13
+ end
14
+ end
15
+
10
16
  @portrayal = Schema.new
11
- class_eval(portrayal.definition_of_object_enhancements)
17
+ class_eval(Schema::DEFINITION_OF_OBJECT_ENHANCEMENTS)
12
18
  end
13
19
 
14
20
  attr_accessor name
15
21
  protected "#{name}="
16
22
 
17
- portrayal.add_keyword(name, optional, default)
23
+ portrayal.add_keyword(name, default)
18
24
  class_eval(portrayal.definition_of_initialize)
19
25
 
20
26
  if block_given?
21
- keyword_class = Class.new(superclass) { extend Portrayal }
22
- keyword_class.class_eval(&block)
23
- const_set(portrayal.camelcase(name), keyword_class)
27
+ kw_class = Class.new(superclass) { extend Portrayal }
28
+ const_set(define || portrayal.camelize(name), kw_class).class_eval(&block)
24
29
  end
30
+
31
+ name
25
32
  end
26
33
  end
@@ -0,0 +1,13 @@
1
+ module Portrayal
2
+ class Default
3
+ attr_reader :value
4
+
5
+ def initialize(value)
6
+ @value = value
7
+ @callable = value.is_a?(Proc) && !value.lambda?
8
+ end
9
+
10
+ def call?; @callable end
11
+ def initialize_dup(src); super; @value = src.value.dup end
12
+ end
13
+ end
@@ -1,56 +1,10 @@
1
+ require 'portrayal/default'
2
+
1
3
  module Portrayal
2
4
  class Schema
3
5
  attr_reader :schema
4
6
 
5
- def initialize
6
- @schema = {}
7
- @equality_defined = false
8
- end
9
-
10
- def keywords; @schema.keys end
11
- def [](name); @schema[name] end
12
-
13
- def attributes(object)
14
- Hash[object.class.portrayal.keywords.map { |k| [k, object.send(k)] }]
15
- end
16
-
17
- def add_keyword(name, optional, default)
18
- optional, default =
19
- case [optional == NULL, default == NULL]
20
- when [true, true]; [false, nil]
21
- when [false, true]; [optional, optional ? [:return, nil] : nil]
22
- when [true, false]; [true, [default_strategy(default), default]]
23
- else; [optional, optional ? [default_strategy(default), default] : nil]
24
- end
25
-
26
- @schema[name.to_sym] = { optional: optional, default: default }
27
- end
28
-
29
- def camelcase(string)
30
- string.to_s.gsub(/(?:^|_+)([^_])/) { $1.upcase }
31
- end
32
-
33
- def get_default(name)
34
- action, value = @schema[name][:default]
35
- action == :call ? value.call : value
36
- end
37
-
38
- def default_strategy(value)
39
- (value.is_a?(Proc) && !value.lambda?) ? :call : :return
40
- end
41
-
42
- def definition_of_initialize
43
- init_args = @schema.map { |name, config|
44
- config[:optional] ?
45
- "#{name}: self.class.portrayal.get_default(:#{name})" : "#{name}:"
46
- }.join(',')
47
-
48
- init_assigns = @schema.keys.map { |name| "@#{name} = #{name}" }.join('; ')
49
- "def initialize(#{init_args}); #{init_assigns} end"
50
- end
51
-
52
- def definition_of_object_enhancements
53
- <<-RUBY
7
+ DEFINITION_OF_OBJECT_ENHANCEMENTS = <<~RUBY.freeze
54
8
  def eql?(other); self.class == other.class && self == other end
55
9
  def hash; [self.class, self.class.portrayal.attributes(self)].hash end
56
10
 
@@ -68,18 +22,51 @@ module Portrayal
68
22
 
69
23
  def initialize_dup(source)
70
24
  self.class.portrayal.attributes(source).each do |key, value|
71
- instance_variable_set('@' + key.to_s, value.dup)
25
+ instance_variable_set("@\#{key}", value.dup)
72
26
  end
73
27
  super
74
28
  end
75
29
 
76
30
  def initialize_clone(source)
77
31
  self.class.portrayal.attributes(source).each do |key, value|
78
- instance_variable_set('@' + key.to_s, value.clone)
32
+ instance_variable_set("@\#{key}", value.clone)
79
33
  end
80
34
  super
81
35
  end
82
- RUBY
36
+ RUBY
37
+
38
+ def initialize; @schema = {} end
39
+ def keywords; @schema.keys end
40
+ def [](name); @schema[name] end
41
+
42
+ def attributes(object)
43
+ Hash[object.class.portrayal.keywords.map { |k| [k, object.send(k)] }]
44
+ end
45
+
46
+ def camelize(string); string.to_s.gsub(/(?:^|_+)([^_])/) { $1.upcase } end
47
+
48
+ def add_keyword(name, default)
49
+ name = name.to_sym
50
+ @schema.delete(name) # Forcing keyword to be added at the end of the hash.
51
+ @schema[name] = default.equal?(NULL) ? nil : Default.new(default)
52
+ end
53
+
54
+ def initialize_dup(other)
55
+ super; @schema = other.schema.transform_values(&:dup)
56
+ end
57
+
58
+ def definition_of_initialize
59
+ init_args = @schema.map { |name, default|
60
+ "#{name}:#{default && " self.class.portrayal[:#{name}]"}"
61
+ }.join(', ')
62
+
63
+ init_assigns = @schema.keys.map { |name|
64
+ "@#{name} = #{name}.is_a?(::Portrayal::Default) ? " \
65
+ "(#{name}.call? ? instance_exec(&#{name}.value) : #{name}.value) : " \
66
+ "#{name}"
67
+ }.join('; ')
68
+
69
+ "def initialize(#{init_args}); #{init_assigns} end"
83
70
  end
84
71
  end
85
72
  end
@@ -1,3 +1,3 @@
1
1
  module Portrayal
2
- VERSION = "0.3.1"
2
+ VERSION = '0.7.1'
3
3
  end
data/portrayal.gemspec CHANGED
@@ -1,6 +1,4 @@
1
- lib = File.expand_path('../lib', __FILE__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require 'portrayal/version'
1
+ require_relative 'lib/portrayal/version'
4
2
 
5
3
  Gem::Specification.new do |spec|
6
4
  spec.name = 'portrayal'
@@ -13,6 +11,11 @@ Gem::Specification.new do |spec|
13
11
  spec.homepage = 'https://github.com/scottscheapflights/portrayal'
14
12
  spec.license = 'Apache-2.0'
15
13
 
14
+ spec.metadata['homepage_uri'] = spec.homepage
15
+ spec.metadata['source_code_uri'] = spec.homepage
16
+ spec.metadata['changelog_uri'] = 'https://github.com/scottscheapflights/portrayal/blob/main/CHANGELOG.md'
17
+
18
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.4.0')
16
19
  spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
17
20
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
18
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: portrayal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maxim Chernyak
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-11 00:00:00.000000000 Z
11
+ date: 2021-03-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -82,21 +82,24 @@ files:
82
82
  - CHANGELOG.md
83
83
  - CODE_OF_CONDUCT.md
84
84
  - Gemfile
85
- - Gemfile.lock
86
85
  - LICENSE.txt
87
86
  - README.md
88
87
  - Rakefile
89
88
  - bin/console
90
89
  - bin/setup
91
90
  - lib/portrayal.rb
91
+ - lib/portrayal/default.rb
92
92
  - lib/portrayal/schema.rb
93
93
  - lib/portrayal/version.rb
94
94
  - portrayal.gemspec
95
95
  homepage: https://github.com/scottscheapflights/portrayal
96
96
  licenses:
97
97
  - Apache-2.0
98
- metadata: {}
99
- post_install_message:
98
+ metadata:
99
+ homepage_uri: https://github.com/scottscheapflights/portrayal
100
+ source_code_uri: https://github.com/scottscheapflights/portrayal
101
+ changelog_uri: https://github.com/scottscheapflights/portrayal/blob/main/CHANGELOG.md
102
+ post_install_message:
100
103
  rdoc_options: []
101
104
  require_paths:
102
105
  - lib
@@ -104,7 +107,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
104
107
  requirements:
105
108
  - - ">="
106
109
  - !ruby/object:Gem::Version
107
- version: '0'
110
+ version: 2.4.0
108
111
  required_rubygems_version: !ruby/object:Gem::Requirement
109
112
  requirements:
110
113
  - - ">="
@@ -112,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
115
  version: '0'
113
116
  requirements: []
114
117
  rubygems_version: 3.1.2
115
- signing_key:
118
+ signing_key:
116
119
  specification_version: 4
117
120
  summary: A minimal builder for struct-like classes
118
121
  test_files: []
data/Gemfile.lock DELETED
@@ -1,41 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- portrayal (0.3.1)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- coderay (1.1.2)
10
- diff-lcs (1.3)
11
- method_source (1.0.0)
12
- pry (0.13.1)
13
- coderay (~> 1.1)
14
- method_source (~> 1.0)
15
- rake (13.0.1)
16
- rspec (3.9.0)
17
- rspec-core (~> 3.9.0)
18
- rspec-expectations (~> 3.9.0)
19
- rspec-mocks (~> 3.9.0)
20
- rspec-core (3.9.2)
21
- rspec-support (~> 3.9.3)
22
- rspec-expectations (3.9.2)
23
- diff-lcs (>= 1.2.0, < 2.0)
24
- rspec-support (~> 3.9.0)
25
- rspec-mocks (3.9.1)
26
- diff-lcs (>= 1.2.0, < 2.0)
27
- rspec-support (~> 3.9.0)
28
- rspec-support (3.9.3)
29
-
30
- PLATFORMS
31
- ruby
32
-
33
- DEPENDENCIES
34
- bundler (~> 2.1)
35
- portrayal!
36
- pry (~> 0.13)
37
- rake (~> 13.0)
38
- rspec (~> 3.9)
39
-
40
- BUNDLED WITH
41
- 2.1.4