portrayal 0.8.0 → 0.9.0

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: 6e6b15fe5bee4f60e8369a839ecad908c35bdbba34559e324f71c3a8404586ad
4
- data.tar.gz: 279dc9030eea091d806fa9e02178783d02063c6efc731bc601a48a68e50f8281
3
+ metadata.gz: acf906a0356260f40fbe25b94b12f72f8c696ee723dff3c2f7dc0f3817154fd0
4
+ data.tar.gz: 0bb4b5a5d4982a59f6e66cd7fa2fe7eeec93b7880537c9e605c47f2d1b0df009
5
5
  SHA512:
6
- metadata.gz: a3c414e415b48127c4f86ee41b7b5d0eed7730ea7316c356afa533f539acd42bdf70c08e1c64f78ddd7ba444da47f4069dbdf8716d42592a8d1b15ec38ca7ff4
7
- data.tar.gz: 2fdab00d88f76ae116af03e14d883346797cf60c21ab0569823f6dd869f3efe368db1b9b2d1e41548619e1666de8acf38d555647a2e29270f0c35e6e512d7aa8
6
+ metadata.gz: 5d3543a802ba4dad5b8c746fa2e187b0debb95bd52354509bb29c2d88e6977d622aeafc8fca3db0a65811a9918c7044985c3355bea78799a0b7730b84da28e21
7
+ data.tar.gz: 66f61afd2be1633c12739d49db088265d90279b622b4f4bf050bba2c4d7dc6444ce232e16be00485702ef9285fe2507758d8fdfee447414ac35bc44fe07a5e89
data/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.0 - 2023-05-06
6
+
7
+ None of these changes should break anything for you, only speed things up, unless you're doing something very weird.
8
+
9
+ * Rewrite internals to improve runtime performance. No longer depend on `portrayal.attributes`, instead generating ruby code that references keywords literally (much more efficient).
10
+ * All attr_readers and other methods such as `eql?`, `==`, `freeze`, etc are now included as a module, rather than class_eval'ed into your class. This lets you use `super` when overriding them.
11
+ * Class method `portrayal` now appears when you call `extend Portrayal`, and not after the first `keyword` declaration. (Instance methods are still added upon `keyword`.)
12
+ * Remove `portrayal[]` shortcut that accessed `portrayal.schema` (use `portrayal.schema[]` directly instead).
13
+ * Remove `portrayal.render_initialize`.
14
+ * Add `portrayal.module`, which is the module included in your struct.
15
+ * Add `portrayal.render_module_code`, which renders the code for the module.
16
+ * Bring back class comparison to `==` (reverses a change in 0.3.0). Upon further research, it seems class comparison is always necessary.
17
+ * Methods `==`, `eql?`, `hash`, `initialize_dup`, `initialize_clone`, and `freeze` now operate on @instance @variables, not reader method return values.
18
+ * Methods `deconstruct` and `deconstruct_keys` now quietly exclude private/protected keywords.
19
+
5
20
  ## 0.8.0 - 2023-01-27
6
21
 
7
22
  * Add pattern matching support (`#deconstruct` and `#deconstruct_keys`).
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ![RSpec](https://github.com/maxim/portrayal/workflows/RSpec/badge.svg)
1
+ [![Gem Version](https://badge.fury.io/rb/portrayal.svg)](https://badge.fury.io/rb/portrayal) ![RSpec](https://github.com/maxim/portrayal/workflows/RSpec/badge.svg)
2
2
 
3
3
  # Portrayal
4
4
 
@@ -8,7 +8,7 @@ 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 (~130 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 (~110 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
@@ -38,6 +38,7 @@ When you call `keyword`:
38
38
  * It defines `#hash` for hash equality
39
39
  * It defines `#dup` and `#clone` that propagate to all keyword values
40
40
  * It defines `#freeze` that propagates to all keyword values
41
+ * It defines `#deconstruct` and `#deconstruct_keys` for pattern matching
41
42
  * It creates a nested class when you supply a block
42
43
  * It inherits parent's superclass when creating a nested class
43
44
 
@@ -76,6 +77,14 @@ class Person < MySuperClass
76
77
  super
77
78
  end
78
79
 
80
+ def deconstruct
81
+ [ name, age, favorite_fruit, address ]
82
+ end
83
+
84
+ def deconstruct_keys(*)
85
+ { name: name, age: age, favorite_fruit: favorite_fruit, address: address }
86
+ end
87
+
79
88
  def initialize_dup(source)
80
89
  @name = source.name.dup
81
90
  @age = source.age.dup
@@ -123,6 +132,14 @@ class Person < MySuperClass
123
132
  super
124
133
  end
125
134
 
135
+ def deconstruct
136
+ [ street, city ]
137
+ end
138
+
139
+ def deconstruct_keys(*)
140
+ { street: street, city: city }
141
+ end
142
+
126
143
  def initialize_dup(source)
127
144
  @street = source.street.dup
128
145
  @city = source.city.dup
@@ -336,9 +353,9 @@ else
336
353
  end # => "matched"
337
354
  ```
338
355
 
339
- ### Schema
356
+ ### Introspection
340
357
 
341
- 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.
358
+ Every class that extends Portrayal receives a method called `portrayal`. This method is a schema of your object with some additional helpers.
342
359
 
343
360
  #### `portrayal.keywords`
344
361
 
@@ -348,7 +365,7 @@ Get all keyword names.
348
365
  Address.portrayal.keywords # => [:street, :city, :postcode, :country]
349
366
  ```
350
367
 
351
- #### `portrayal.attributes`
368
+ #### `portrayal.attributes(object)`
352
369
 
353
370
  Get all names + values as a hash.
354
371
 
@@ -371,12 +388,14 @@ Portrayal steps back from things like type enforcement, coercion, and writer met
371
388
 
372
389
  #### Good Constructors
373
390
 
374
- The fact that we keep these portrayal structs read-only (nothing stops you from adding writers, but I will personally frown upon you), all of the responsibility of building them shifts into constructors. This is a good thing, because good constructors clearly define their dependencies, as well as giving us ample room for performing coercion.
391
+ Since a portrayal object is read-only (nothing stops you from adding writers, but I will personally frown upon you), you must set all its values in a constructor. This is a good thing, because it lets us study, coerce, and validate all the passed-in arguments in one convenient place. We're assured that once instantiated, the object is valid. And of course we can have multiple constructors if needed. They serve as adapters for different kinds of input.
375
392
 
376
393
  ```ruby
377
394
  class Address < ApplicationStruct
378
395
  class << self
379
396
  def from_form(params)
397
+ raise ArgumentError, 'invalid postcode' unless postcode =~ /\A\d+\z/
398
+
380
399
  new \
381
400
  street: params[:street].to_s,
382
401
  city: params[:city].to_s,
@@ -400,16 +419,18 @@ class Address < ApplicationStruct
400
419
  end
401
420
  ```
402
421
 
403
- Good constructors can also depend on one another to successively break down dependnecies into essential parts. This is similar to how in functional languages one can use recursion and pattern matching.
422
+ Good constructors can depend on one another to successively convert arguments into keywords. This is similar to how in functional languages one can use recursion and pattern matching.
404
423
 
405
424
  ```ruby
406
425
  class Email < ApplicationStruct
407
426
  class << self
427
+ # Extract parts of an email from JSON, and kick it over to from_parts.
408
428
  def from_publishing_service_json(json)
409
429
  subject, header, body, footer = *JSON.parse(json)
410
430
  from_parts(subject: subject, header: header, body: body, footer: footer)
411
431
  end
412
432
 
433
+ # Combine parts into the final keywords: subject and body.
413
434
  def from_parts(subject:, header:, body:, footer:)
414
435
  new(subject: subject, body: "#{header}#{body}#{footer}")
415
436
  end
@@ -443,15 +464,45 @@ class Address < ApplicationStruct
443
464
  end
444
465
  ```
445
466
 
467
+ If a particular constructor doesn't belong on your object (i.e. a 3rd party module is responsible for parsing its own data and producing your object) — you don't need to have a special constructor. Remember that each portrayal object comes with `.new`, which accepts every keyword directly. Let the module do all the parsing on its side and call `.new` with final values.
468
+
446
469
  #### No Reinventing The Wheel
447
470
 
448
- Portrayal leans on Ruby to take care of enforcing required keyword arguments, and setting keyword argument defaults. It actually generates standard ruby keyword arguments for you behind the scenes. You can even see the code by checking `YourClass.portrayal.definition_of_initialize`.
471
+ Portrayal leans on Ruby's built-in features as much as possible. For initialize and default values it generates standard ruby keyword arguments. You can see all the code portrayal generates for your objects by running `YourClass.portrayal.render_module_code`.
449
472
 
450
473
  ```irb
451
- Address.portrayal.definition_of_initialize
452
- => "def initialize(street:,city:,postcode:,country: self.class.portrayal.call_default(:country)); @street = street; @city = city; @postcode = postcode; @country = country end"
474
+ [1] pry(main)> puts Address.portrayal.render_module_code
475
+ attr_accessor :street, :city, :postcode, :country
476
+ protected :street=, :city=, :postcode=, :country=
477
+ def initialize(street:, city:, postcode:, country: self.class.portrayal.schema[:country]); @street = street.is_a?(::Portrayal::Default) ? street.(self) : street; @city = city.is_a?(::Portrayal::Default) ? city.(self) : city; @postcode = postcode.is_a?(::Portrayal::Default) ? postcode.(self) : postcode; @country = country.is_a?(::Portrayal::Default) ? country.(self) : country end
478
+ def hash; [self.class, {street: @street, city: @city, postcode: @postcode, country: @country}].hash end
479
+ def ==(other); self.class == other.class && @street == other.instance_variable_get('@street') && @city == other.instance_variable_get('@city') && @postcode == other.instance_variable_get('@postcode') && @country == other.instance_variable_get('@country') end
480
+ alias eql? ==
481
+ def freeze; @street.freeze; @city.freeze; @postcode.freeze; @country.freeze; super end
482
+ def initialize_dup(src); @street = src.instance_variable_get('@street').dup; @city = src.instance_variable_get('@city').dup; @postcode = src.instance_variable_get('@postcode').dup; @country = src.instance_variable_get('@country').dup; super end
483
+ def initialize_clone(src); @street = src.instance_variable_get('@street').clone; @city = src.instance_variable_get('@city').clone; @postcode = src.instance_variable_get('@postcode').clone; @country = src.instance_variable_get('@country').clone; super end
484
+ def deconstruct
485
+ public_syms = [:street, :city, :postcode, :country].select { |s| self.class.public_method_defined?(s) }
486
+ public_syms.map { |s| public_send(s) }
487
+ end
488
+ def deconstruct_keys(keys)
489
+ filtered_keys = [:street, :city, :postcode, :country].select {|s| self.class.public_method_defined?(s) }
490
+ filtered_keys &= keys if Array === keys
491
+ Hash[filtered_keys.map { |k| [k, public_send(k)] }]
492
+ end
453
493
  ```
454
494
 
495
+ #### Implementation decisions
496
+
497
+ Here are some key architectural decisions that took a lot of thinking. If you have good counter-arguments please make an issue, or contact me on [mastodon](https://ruby.social/@maxim) / [twitter](https://twitter.com/hakunin).
498
+
499
+ 1. **Why do methods `#==`, `#eql?`, `#hash` rely on @instance @variables instead of calling reader methods?**
500
+ Portrayal makes a careful assumption on what most people would expect from object equality: a comparison of type and runtime state (which is what instance variables are). Portrayal avoids comparing object structure and method return values, because it's too situational whether they should participate in equality or not. If you have such a situation, you're welcome to redefine `==` in your class.
501
+ 2. **Why do methods `clone` and `dup` copy @instance @variables instead of calling reader methods?**
502
+ As with the reason for `==`, when we clone an object, we want to clone its type and runtime state. Not the artifacts of its structure. It's too presumptious for a clone to assume that method outputs are authoritative. If objects are written deterministically, then by cloning their inner runtime state we should get the same reader method outputs anyway. If you are doing something else, you're welcome to redefine `initialize_clone`/`initialize_dup` in your class.
503
+ 3. **Why does pattern matching (`deconstruct`/`deconstruct_keys`) call reader methods rather than reading @instance @variables?**
504
+ Unlike equality or object replication, in case of pattern matching we're no longer trying to figure out object's identity, rather we are now an external caller working directly with the values that an object exposes. That's why portrayal lets pattern matching depend on reader methods that get to decide how to expose data outwardly, while making a conscious effort to exclude private and protected readers. You're welcome to override `deconstruct` and `deconstruct_keys` in your class if you'd like to do something different.
505
+
455
506
  ## Development
456
507
 
457
508
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/bin/bench ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'portrayal'
5
+ require 'benchmark/ips'
6
+
7
+ class Address1
8
+ extend Portrayal
9
+
10
+ keyword :street
11
+ keyword :city
12
+ keyword :postal_code
13
+
14
+ def ==(other)
15
+ self.class.portrayal.attributes(self) ==
16
+ other.class.portrayal.attributes(other)
17
+ end
18
+ end
19
+
20
+ class Address2
21
+ extend Portrayal
22
+
23
+ keyword :street
24
+ keyword :city
25
+ keyword :postal_code
26
+ end
27
+
28
+ Address2.portrayal.module.module_eval <<-RUBY
29
+ def ==(o)
30
+ street == o.street && city == o.city && postal_code == o.postal_code
31
+ end
32
+ RUBY
33
+
34
+ class Address3
35
+ extend Portrayal
36
+
37
+ keyword :street
38
+ keyword :city
39
+ keyword :postal_code
40
+
41
+ def ==(o)
42
+ { street: street, city: city, postal_code: postal_code } ==
43
+ { street: o.street, city: o.city, postal_code: o.postal_code }
44
+ end
45
+ end
46
+
47
+ a1 = Address1.new(street: 'street', city: 'city', postal_code: 123)
48
+ a2 = Address2.new(street: 'street', city: 'city', postal_code: 123)
49
+ a3 = Address3.new(street: 'street', city: 'city', postal_code: 123)
50
+
51
+
52
+ Benchmark.ips do |x|
53
+ x.report("equality-via-hash-construction") {
54
+ a1 == a1
55
+ }
56
+
57
+ x.report("equality-via-boolean-expression") {
58
+ a2 == a2
59
+ }
60
+
61
+ x.report("equality-via-hash-literal") {
62
+ a3 == a3
63
+ }
64
+
65
+ x.compare!
66
+ end
67
+
68
+ Benchmark.ips do |x|
69
+ x.report("hash of literal Hash") {
70
+ { foo: 'foo', bar: 'bar', baz: 'baz' }.hash
71
+ }
72
+
73
+ x.report("hash of literal Array") {
74
+ [ [:foo, 'foo'], [:bar, 'bar'], [:baz, 'baz'] ].hash
75
+ }
76
+
77
+ x.compare!
78
+ end
data/bin/module ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'portrayal'
5
+
6
+ class Address
7
+ extend Portrayal
8
+
9
+ keyword :street
10
+ keyword :city
11
+ keyword :postcode
12
+ keyword :country, default: nil
13
+ end
14
+
15
+ puts Address.portrayal.render_module_code
@@ -1,13 +1,12 @@
1
- module Portrayal
2
- class Default
3
- attr_reader :value
1
+ class Portrayal::Default
2
+ attr_reader :value
3
+ protected :value
4
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
5
+ def initialize(value)
6
+ @value = value
7
+ @callable = value.is_a?(Proc) && !value.lambda?
12
8
  end
9
+
10
+ def call(obj); @callable ? obj.instance_exec(&value) : value end
11
+ def initialize_dup(src); @value = src.value.dup; super end
13
12
  end
@@ -1,79 +1,72 @@
1
1
  require 'portrayal/default'
2
2
 
3
- module Portrayal
4
- class Schema
5
- attr_reader :schema
6
-
7
- DEFINITION_OF_OBJECT_ENHANCEMENTS = <<~RUBY.freeze
8
- def eql?(other); self.class == other.class && self == other end
9
- def hash; [self.class, self.class.portrayal.attributes(self)].hash end
10
- def deconstruct; self.class.portrayal.attributes(self).values end
11
-
12
- def deconstruct_keys(keys)
13
- keys ||= self.class.portrayal.keywords
14
- keys &= self.class.portrayal.keywords
15
- Hash[keys.map { |k| [k, send(k)] }]
16
- end
17
-
18
- def ==(other)
19
- return super unless other.class.is_a?(Portrayal)
20
-
21
- self.class.portrayal.attributes(self) ==
22
- self.class.portrayal.attributes(other)
23
- end
24
-
25
- def freeze
26
- self.class.portrayal.attributes(self).values.each(&:freeze)
27
- super
28
- end
29
-
30
- def initialize_dup(source)
31
- self.class.portrayal.attributes(source).each do |key, value|
32
- instance_variable_set("@\#{key}", value.dup)
33
- end
34
- super
35
- end
36
-
37
- def initialize_clone(source)
38
- self.class.portrayal.attributes(source).each do |key, value|
39
- instance_variable_set("@\#{key}", value.clone)
40
- end
41
- super
42
- end
43
- RUBY
44
-
45
- def initialize; @schema = {} end
46
- def keywords; @schema.keys end
47
- def [](name); @schema[name] end
48
-
49
- def attributes(object)
50
- Hash[object.class.portrayal.keywords.map { |k| [k, object.send(k)] }]
51
- end
52
-
53
- def camelize(string); string.to_s.gsub(/(?:^|_+)([^_])/) { $1.upcase } end
3
+ class Portrayal::Schema
4
+ attr_reader :schema, :module
5
+ NULL = Object.new.freeze
6
+
7
+ def initialize; @schema = {}; @module = Module.new end
8
+ def keywords; @schema.keys end
9
+ def attributes(obj); Hash[keywords.map { |k| [k, obj.send(k)] }] end
10
+ def camelize(string); string.to_s.gsub(/(?:^|_+)([^_])/) { $1.upcase } end
11
+
12
+ def initialize_dup(src)
13
+ @schema = src.schema.transform_values(&:dup)
14
+ @module = src.module.dup
15
+ super
16
+ end
54
17
 
55
- def add_keyword(name, default)
56
- name = name.to_sym
57
- @schema.delete(name) # Forcing keyword to be added at the end of the hash.
58
- @schema[name] = default.equal?(NULL) ? nil : Default.new(default)
59
- end
18
+ def add_keyword(name, default)
19
+ name = name.to_sym
20
+ @schema.delete(name) # Forcing keyword to be added at the end of the hash.
21
+ @schema[name] = default.equal?(NULL) ? nil : Portrayal::Default.new(default)
22
+ @module.module_eval(render_module_code)
23
+ end
60
24
 
61
- def initialize_dup(other)
62
- super; @schema = other.schema.transform_values(&:dup)
25
+ def render_module_code
26
+ args, inits, syms, hash, eqls, dups, clones, setters, freezes =
27
+ '', '', '', '', '', '', '', '', ''
28
+
29
+ @schema.each do |k, default|
30
+ args << "#{k}:#{default && " self.class.portrayal.schema[:#{k}]"}, "
31
+ inits << "@#{k} = #{k}.is_a?(::Portrayal::Default) ? #{k}.(self) : #{k}; "
32
+ syms << ":#{k}, "
33
+ hash << "#{k}: @#{k}, "
34
+ eqls << "@#{k} == other.instance_variable_get('@#{k}') && "
35
+ dups << "@#{k} = src.instance_variable_get('@#{k}').dup; "
36
+ clones << "@#{k} = src.instance_variable_get('@#{k}').clone; "
37
+ setters << ":#{k}=, "
38
+ freezes << "@#{k}.freeze; "
63
39
  end
64
40
 
65
- def definition_of_initialize
66
- init_args = @schema.map { |name, default|
67
- "#{name}:#{default && " self.class.portrayal[:#{name}]"}"
68
- }.join(', ')
69
-
70
- init_assigns = @schema.keys.map { |name|
71
- "@#{name} = #{name}.is_a?(::Portrayal::Default) ? " \
72
- "(#{name}.call? ? instance_exec(&#{name}.value) : #{name}.value) : " \
73
- "#{name}"
74
- }.join('; ')
75
-
76
- "def initialize(#{init_args}); #{init_assigns} end"
77
- end
41
+ args.chomp!(', ') # key1:, key2: self.class.portrayal.schema[:key2]
42
+ inits.chomp!('; ') # Assignments in initialize
43
+ syms.chomp!(', ') # :key1, :key2
44
+ hash.chomp!(', ') # key1: @key1, key2: @key2
45
+ eqls.chomp!(' && ') # @key1 == other.instance_variable_get('@key1') &&
46
+ dups.chomp!('; ') # @key1 = src.instance_variable_get('@key1').dup;
47
+ clones.chomp!('; ') # @key1 = src.instance_variable_get('@key1').clone;
48
+ setters.chomp!(', ') # :key1=, :key2=
49
+ freezes.chomp!('; ') # @key1.freeze; @key2.freeze
50
+
51
+ <<-RUBY
52
+ attr_accessor #{syms}
53
+ protected #{setters}
54
+ def initialize(#{args}); #{inits} end
55
+ def hash; [self.class, {#{hash}}].hash end
56
+ def ==(other); self.class == other.class && #{eqls} end
57
+ alias eql? ==
58
+ def freeze; #{freezes}; super end
59
+ def initialize_dup(src); #{dups}; super end
60
+ def initialize_clone(src); #{clones}; super end
61
+ def deconstruct
62
+ public_syms = [#{syms}].select { |s| self.class.public_method_defined?(s) }
63
+ public_syms.map { |s| public_send(s) }
64
+ end
65
+ def deconstruct_keys(keys)
66
+ filtered_keys = [#{syms}].select {|s| self.class.public_method_defined?(s) }
67
+ filtered_keys &= keys if Array === keys
68
+ Hash[filtered_keys.map { |k| [k, public_send(k)] }]
69
+ end
70
+ RUBY
78
71
  end
79
72
  end
@@ -1,3 +1,3 @@
1
1
  module Portrayal
2
- VERSION = '0.8.0'
2
+ VERSION = '0.9.0'
3
3
  end
data/lib/portrayal.rb CHANGED
@@ -2,32 +2,20 @@ require 'portrayal/version'
2
2
  require 'portrayal/schema'
3
3
 
4
4
  module Portrayal
5
- NULL = :_portrayal_value_not_set
5
+ attr_reader :portrayal
6
+ def self.extended(c); c.instance_variable_set(:@portrayal, Schema.new) end
6
7
 
7
- def keyword(name, default: NULL, define: nil, &block)
8
- unless respond_to?(:portrayal)
9
- class << self
10
- attr_reader :portrayal
11
- def inherited(base)
12
- base.instance_variable_set('@portrayal', portrayal.dup)
13
- end
14
- end
15
-
16
- @portrayal = Schema.new
17
- class_eval(Schema::DEFINITION_OF_OBJECT_ENHANCEMENTS)
18
- end
19
-
20
- attr_accessor name
21
- protected "#{name}="
8
+ def inherited(c)
9
+ c.instance_variable_set(:@portrayal, portrayal.dup)
10
+ c.include(c.portrayal.module)
11
+ end
22
12
 
13
+ def keyword(name, default: Schema::NULL, define: nil, &block)
14
+ include portrayal.module
23
15
  portrayal.add_keyword(name, default)
24
- class_eval(portrayal.definition_of_initialize)
25
-
26
- if block_given?
27
- kw_class = Class.new(superclass) { extend Portrayal }
28
- const_set(define || portrayal.camelize(name), kw_class).class_eval(&block)
29
- end
30
-
16
+ return name unless block_given?
17
+ nested = Class.new(superclass) { extend ::Portrayal }
18
+ const_set(define || portrayal.camelize(name), nested).class_eval(&block)
31
19
  name
32
20
  end
33
21
  end
data/portrayal.gemspec CHANGED
@@ -24,4 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency 'rake', '~> 13.0'
25
25
  spec.add_development_dependency 'rspec', '~> 3.12'
26
26
  spec.add_development_dependency 'pry', '~> 0.14'
27
+ spec.add_development_dependency 'benchmark-ips', '~> 2.11'
27
28
  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.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max Chernyak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-28 00:00:00.000000000 Z
11
+ date: 2023-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.14'
55
+ - !ruby/object:Gem::Dependency
56
+ name: benchmark-ips
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.11'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.11'
55
69
  description: Inspired by dry-initializer and virtus, portrayal is a minimalist gem
56
70
  that takes a somewhat different approach to building struct-like classes. It steps
57
71
  away from types, coersion, and writer methods in favor of encouraging well-designed
@@ -71,8 +85,10 @@ files:
71
85
  - LICENSE.txt
72
86
  - README.md
73
87
  - Rakefile
88
+ - bin/bench
74
89
  - bin/console
75
90
  - bin/loc
91
+ - bin/module
76
92
  - bin/setup
77
93
  - lib/portrayal.rb
78
94
  - lib/portrayal/default.rb