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 +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +61 -10
- data/bin/bench +78 -0
- data/bin/module +15 -0
- data/lib/portrayal/default.rb +9 -10
- data/lib/portrayal/schema.rb +64 -71
- data/lib/portrayal/version.rb +1 -1
- data/lib/portrayal.rb +11 -23
- data/portrayal.gemspec +1 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: acf906a0356260f40fbe25b94b12f72f8c696ee723dff3c2f7dc0f3817154fd0
|
4
|
+
data.tar.gz: 0bb4b5a5d4982a59f6e66cd7fa2fe7eeec93b7880537c9e605c47f2d1b0df009
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 (~
|
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
|
-
###
|
356
|
+
### Introspection
|
340
357
|
|
341
|
-
Every class that
|
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
|
-
|
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
|
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
|
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.
|
452
|
-
|
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
|
data/lib/portrayal/default.rb
CHANGED
@@ -1,13 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
class Portrayal::Default
|
2
|
+
attr_reader :value
|
3
|
+
protected :value
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
data/lib/portrayal/schema.rb
CHANGED
@@ -1,79 +1,72 @@
|
|
1
1
|
require 'portrayal/default'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
62
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
data/lib/portrayal/version.rb
CHANGED
data/lib/portrayal.rb
CHANGED
@@ -2,32 +2,20 @@ require 'portrayal/version'
|
|
2
2
|
require 'portrayal/schema'
|
3
3
|
|
4
4
|
module Portrayal
|
5
|
-
|
5
|
+
attr_reader :portrayal
|
6
|
+
def self.extended(c); c.instance_variable_set(:@portrayal, Schema.new) end
|
6
7
|
|
7
|
-
def
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
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
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.
|
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-
|
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
|