portrayal 0.7.1 → 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/.github/workflows/rspec.yml +2 -2
- data/CHANGELOG.md +29 -10
- data/README.md +92 -12
- data/bin/bench +78 -0
- data/bin/loc +2 -0
- data/bin/module +15 -0
- data/lib/portrayal/default.rb +9 -10
- data/lib/portrayal/schema.rb +60 -60
- data/lib/portrayal/version.rb +1 -1
- data/lib/portrayal.rb +11 -23
- data/portrayal.gemspec +7 -7
- metadata +24 -21
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/.github/workflows/rspec.yml
CHANGED
@@ -6,11 +6,11 @@ jobs:
|
|
6
6
|
strategy:
|
7
7
|
fail-fast: false
|
8
8
|
matrix:
|
9
|
-
ruby: [ '2.4', '2.5', '2.6', '2.7', '3.0' ]
|
9
|
+
ruby: [ '2.4', '2.5', '2.6', '2.7', '3.0', '3.2' ]
|
10
10
|
|
11
11
|
name: Ruby ${{ matrix.ruby }}
|
12
12
|
steps:
|
13
|
-
- uses: actions/checkout@
|
13
|
+
- uses: actions/checkout@v3
|
14
14
|
- uses: ruby/setup-ruby@v1
|
15
15
|
with:
|
16
16
|
ruby-version: ${{ matrix.ruby }}
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,25 @@ 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
|
+
|
20
|
+
## 0.8.0 - 2023-01-27
|
21
|
+
|
22
|
+
* Add pattern matching support (`#deconstruct` and `#deconstruct_keys`).
|
23
|
+
|
5
24
|
## 0.7.1 - 2021-03-22
|
6
25
|
|
7
26
|
* 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:
|
@@ -31,31 +50,31 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
31
50
|
|
32
51
|
## 0.6.0 - 2020-08-10
|
33
52
|
|
34
|
-
* Return keyword name from `keyword`, allowing usage such as `private keyword :foo`. [[commit]](https://github.com/
|
53
|
+
* Return keyword name from `keyword`, allowing usage such as `private keyword :foo`. [[commit]](https://github.com/maxim/portrayal/commit/9e9db2cafc7eae14789c5b84f70efd18898ace76)
|
35
54
|
|
36
55
|
## 0.5.0 - 2020-05-28
|
37
56
|
|
38
|
-
* Add option `define` for overriding nested class name. [[commit]](https://github.com/
|
57
|
+
* Add option `define` for overriding nested class name. [[commit]](https://github.com/maxim/portrayal/commit/665ad297fb71fcdf5f641c672a457ccbe29e4a49)
|
39
58
|
|
40
59
|
## 0.4.0 - 2020-05-16
|
41
60
|
|
42
|
-
* Portrayal schema is deep-duped to subclasses. [[commit]](https://github.com/
|
61
|
+
* Portrayal schema is deep-duped to subclasses. [[commit]](https://github.com/maxim/portrayal/commit/f346483a379ce9fbdece72cde8b0844f2d22b1cd)
|
43
62
|
|
44
63
|
## 0.3.1 - 2020-05-11
|
45
64
|
|
46
|
-
* Fix the issue introduced in 0.3.0 where `==` and `eql?` were always treating rhs as another portrayal class. [[commit]](https://github.com/
|
65
|
+
* Fix the issue introduced in 0.3.0 where `==` and `eql?` were always treating rhs as another portrayal class. [[commit]](https://github.com/maxim/portrayal/commit/f6ec8f373c6582f7e8d8f872d289222e4a58f8f6)
|
47
66
|
|
48
67
|
## 0.3.0 - 2020-05-09 (yanked)
|
49
68
|
|
50
|
-
* No longer compare classes in `==`, use `eql?` for that. [[commit]](https://github.com/
|
51
|
-
* Define a protected writer for every keyword - useful when applying changes after `dup`/`clone`. [[commit]](https://github.com/
|
52
|
-
* Add definition of `#hash` to fix hash equality. Now `hash[object]` will match if `object` is of the same class with the same keywords and values. [[commit]](https://github.com/
|
53
|
-
* Make `#freeze` propagate to all keyword values. [[commit]](https://github.com/
|
54
|
-
* Make `#dup` and `#clone` propagate to all keyword values. [[commit]](https://github.com/
|
69
|
+
* No longer compare classes in `==`, use `eql?` for that. [[commit]](https://github.com/maxim/portrayal/commit/9c5a37e4fb91e35d23b22e208344452930452af7)
|
70
|
+
* Define a protected writer for every keyword - useful when applying changes after `dup`/`clone`. [[commit]](https://github.com/maxim/portrayal/commit/1c0fa6c6357a09760dae39165e864238d231a08e)
|
71
|
+
* Add definition of `#hash` to fix hash equality. Now `hash[object]` will match if `object` is of the same class with the same keywords and values. [[commit]](https://github.com/maxim/portrayal/commit/ba9e390ab4aea4733ba084ac273da448e313ea53)
|
72
|
+
* Make `#freeze` propagate to all keyword values. [[commit]](https://github.com/maxim/portrayal/commit/0a734411a6eac08e2355c4277e09a2a70800d032)
|
73
|
+
* Make `#dup` and `#clone` propagate to all keyword values. [[commit]](https://github.com/maxim/portrayal/commit/010632d87d81a8d5b5ea5ff27d3d209cc667b0a5)
|
55
74
|
|
56
75
|
## 0.2.0 - 2019-07-03
|
57
76
|
|
58
|
-
* It's now possible to specify non-lambda default values, like `default: "foo"`. There is now also a distinction between a proc and a lambda default. Procs are `call`-ed, while lambdas or any other types are returned as-is. In the majority of cases defaults are static values, and there is no need for the performance overhead of making all defaults into anonymous functions. [[commit]](https://github.com/
|
77
|
+
* It's now possible to specify non-lambda default values, like `default: "foo"`. There is now also a distinction between a proc and a lambda default. Procs are `call`-ed, while lambdas or any other types are returned as-is. In the majority of cases defaults are static values, and there is no need for the performance overhead of making all defaults into anonymous functions. [[commit]](https://github.com/maxim/portrayal/commit/a1cc9d0fd40e413210f61b945d37b81c87280fee)
|
59
78
|
|
60
79
|
## 0.1.0
|
61
80
|
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
![RSpec](https://github.com/
|
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
|
@@ -307,9 +324,38 @@ However, if you try calling `Employee.from_contact(contact)` it will error out,
|
|
307
324
|
|
308
325
|
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
326
|
|
310
|
-
###
|
327
|
+
### Pattern Matching
|
328
|
+
|
329
|
+
If your Ruby has pattern matching, you can pattern match portrayal objects. Both array- and hash-style matching are supported.
|
330
|
+
|
331
|
+
```ruby
|
332
|
+
class Point
|
333
|
+
extend Portrayal
|
334
|
+
|
335
|
+
keyword :x
|
336
|
+
keyword :y
|
337
|
+
end
|
338
|
+
|
339
|
+
point = Point.new(x: 5, y: 10)
|
340
|
+
|
341
|
+
case point
|
342
|
+
in 5, 10
|
343
|
+
'matched'
|
344
|
+
else
|
345
|
+
'did not match'
|
346
|
+
end # => "matched"
|
347
|
+
|
348
|
+
case point
|
349
|
+
in x:, y: 10
|
350
|
+
'matched'
|
351
|
+
else
|
352
|
+
'did not match'
|
353
|
+
end # => "matched"
|
354
|
+
```
|
355
|
+
|
356
|
+
### Introspection
|
311
357
|
|
312
|
-
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.
|
313
359
|
|
314
360
|
#### `portrayal.keywords`
|
315
361
|
|
@@ -319,7 +365,7 @@ Get all keyword names.
|
|
319
365
|
Address.portrayal.keywords # => [:street, :city, :postcode, :country]
|
320
366
|
```
|
321
367
|
|
322
|
-
#### `portrayal.attributes`
|
368
|
+
#### `portrayal.attributes(object)`
|
323
369
|
|
324
370
|
Get all names + values as a hash.
|
325
371
|
|
@@ -342,12 +388,14 @@ Portrayal steps back from things like type enforcement, coercion, and writer met
|
|
342
388
|
|
343
389
|
#### Good Constructors
|
344
390
|
|
345
|
-
|
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.
|
346
392
|
|
347
393
|
```ruby
|
348
394
|
class Address < ApplicationStruct
|
349
395
|
class << self
|
350
396
|
def from_form(params)
|
397
|
+
raise ArgumentError, 'invalid postcode' unless postcode =~ /\A\d+\z/
|
398
|
+
|
351
399
|
new \
|
352
400
|
street: params[:street].to_s,
|
353
401
|
city: params[:city].to_s,
|
@@ -371,16 +419,18 @@ class Address < ApplicationStruct
|
|
371
419
|
end
|
372
420
|
```
|
373
421
|
|
374
|
-
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.
|
375
423
|
|
376
424
|
```ruby
|
377
425
|
class Email < ApplicationStruct
|
378
426
|
class << self
|
427
|
+
# Extract parts of an email from JSON, and kick it over to from_parts.
|
379
428
|
def from_publishing_service_json(json)
|
380
429
|
subject, header, body, footer = *JSON.parse(json)
|
381
430
|
from_parts(subject: subject, header: header, body: body, footer: footer)
|
382
431
|
end
|
383
432
|
|
433
|
+
# Combine parts into the final keywords: subject and body.
|
384
434
|
def from_parts(subject:, header:, body:, footer:)
|
385
435
|
new(subject: subject, body: "#{header}#{body}#{footer}")
|
386
436
|
end
|
@@ -414,15 +464,45 @@ class Address < ApplicationStruct
|
|
414
464
|
end
|
415
465
|
```
|
416
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
|
+
|
417
469
|
#### No Reinventing The Wheel
|
418
470
|
|
419
|
-
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`.
|
420
472
|
|
421
473
|
```irb
|
422
|
-
Address.portrayal.
|
423
|
-
|
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
|
424
493
|
```
|
425
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
|
+
|
426
506
|
## Development
|
427
507
|
|
428
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.
|
@@ -431,7 +511,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
431
511
|
|
432
512
|
## Contributing
|
433
513
|
|
434
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
514
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/maxim/portrayal. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
435
515
|
|
436
516
|
## License
|
437
517
|
|
@@ -439,4 +519,4 @@ The gem is available as open source under the terms of the [Apache License Versi
|
|
439
519
|
|
440
520
|
## Code of Conduct
|
441
521
|
|
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/
|
522
|
+
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/maxim/portrayal/blob/main/CODE_OF_CONDUCT.md).
|
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/loc
ADDED
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,72 +1,72 @@
|
|
1
1
|
require 'portrayal/default'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
class Portrayal::Schema
|
4
|
+
attr_reader :schema, :module
|
5
|
+
NULL = Object.new.freeze
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
end
|
17
|
-
|
18
|
-
def freeze
|
19
|
-
self.class.portrayal.attributes(self).values.each(&:freeze)
|
20
|
-
super
|
21
|
-
end
|
22
|
-
|
23
|
-
def initialize_dup(source)
|
24
|
-
self.class.portrayal.attributes(source).each do |key, value|
|
25
|
-
instance_variable_set("@\#{key}", value.dup)
|
26
|
-
end
|
27
|
-
super
|
28
|
-
end
|
29
|
-
|
30
|
-
def initialize_clone(source)
|
31
|
-
self.class.portrayal.attributes(source).each do |key, value|
|
32
|
-
instance_variable_set("@\#{key}", value.clone)
|
33
|
-
end
|
34
|
-
super
|
35
|
-
end
|
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
|
12
|
+
def initialize_dup(src)
|
13
|
+
@schema = src.schema.transform_values(&:dup)
|
14
|
+
@module = src.module.dup
|
15
|
+
super
|
16
|
+
end
|
45
17
|
|
46
|
-
|
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
|
47
24
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
@schema[name] = default.equal?(NULL) ? nil : Default.new(default)
|
52
|
-
end
|
25
|
+
def render_module_code
|
26
|
+
args, inits, syms, hash, eqls, dups, clones, setters, freezes =
|
27
|
+
'', '', '', '', '', '', '', '', ''
|
53
28
|
|
54
|
-
|
55
|
-
|
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; "
|
56
39
|
end
|
57
40
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
}.join('; ')
|
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
|
68
50
|
|
69
|
-
|
70
|
-
|
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
|
71
71
|
end
|
72
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
@@ -3,17 +3,17 @@ require_relative 'lib/portrayal/version'
|
|
3
3
|
Gem::Specification.new do |spec|
|
4
4
|
spec.name = 'portrayal'
|
5
5
|
spec.version = Portrayal::VERSION
|
6
|
-
spec.authors = ['
|
7
|
-
spec.email = ['
|
6
|
+
spec.authors = ['Max Chernyak']
|
7
|
+
spec.email = ['hello@max.engineer']
|
8
8
|
|
9
9
|
spec.summary = 'A minimal builder for struct-like classes'
|
10
10
|
spec.description = 'Inspired by dry-initializer and virtus, portrayal is a minimalist gem that takes a somewhat different approach to building struct-like classes. It steps away from types, coersion, and writer methods in favor of encouraging well-designed constructors. Read more in the Philosophy section of the README.'
|
11
|
-
spec.homepage = 'https://github.com/
|
11
|
+
spec.homepage = 'https://github.com/maxim/portrayal'
|
12
12
|
spec.license = 'Apache-2.0'
|
13
13
|
|
14
14
|
spec.metadata['homepage_uri'] = spec.homepage
|
15
15
|
spec.metadata['source_code_uri'] = spec.homepage
|
16
|
-
spec.metadata['changelog_uri'] = 'https://github.com/
|
16
|
+
spec.metadata['changelog_uri'] = 'https://github.com/maxim/portrayal/blob/main/CHANGELOG.md'
|
17
17
|
|
18
18
|
spec.required_ruby_version = Gem::Requirement.new('>= 2.4.0')
|
19
19
|
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
@@ -21,8 +21,8 @@ Gem::Specification.new do |spec|
|
|
21
21
|
end
|
22
22
|
spec.require_paths = ['lib']
|
23
23
|
|
24
|
-
spec.add_development_dependency 'bundler', '~> 2.1'
|
25
24
|
spec.add_development_dependency 'rake', '~> 13.0'
|
26
|
-
spec.add_development_dependency 'rspec', '~> 3.
|
27
|
-
spec.add_development_dependency 'pry', '~> 0.
|
25
|
+
spec.add_development_dependency 'rspec', '~> 3.12'
|
26
|
+
spec.add_development_dependency 'pry', '~> 0.14'
|
27
|
+
spec.add_development_dependency 'benchmark-ips', '~> 2.11'
|
28
28
|
end
|
metadata
CHANGED
@@ -1,77 +1,77 @@
|
|
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:
|
11
|
+
date: 2023-05-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: rake
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '13.0'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '13.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: rspec
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '3.12'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '3.12'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: pry
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '0.14'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '0.14'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: benchmark-ips
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '2.11'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '2.11'
|
69
69
|
description: Inspired by dry-initializer and virtus, portrayal is a minimalist gem
|
70
70
|
that takes a somewhat different approach to building struct-like classes. It steps
|
71
71
|
away from types, coersion, and writer methods in favor of encouraging well-designed
|
72
72
|
constructors. Read more in the Philosophy section of the README.
|
73
73
|
email:
|
74
|
-
-
|
74
|
+
- hello@max.engineer
|
75
75
|
executables: []
|
76
76
|
extensions: []
|
77
77
|
extra_rdoc_files: []
|
@@ -85,20 +85,23 @@ files:
|
|
85
85
|
- LICENSE.txt
|
86
86
|
- README.md
|
87
87
|
- Rakefile
|
88
|
+
- bin/bench
|
88
89
|
- bin/console
|
90
|
+
- bin/loc
|
91
|
+
- bin/module
|
89
92
|
- bin/setup
|
90
93
|
- lib/portrayal.rb
|
91
94
|
- lib/portrayal/default.rb
|
92
95
|
- lib/portrayal/schema.rb
|
93
96
|
- lib/portrayal/version.rb
|
94
97
|
- portrayal.gemspec
|
95
|
-
homepage: https://github.com/
|
98
|
+
homepage: https://github.com/maxim/portrayal
|
96
99
|
licenses:
|
97
100
|
- Apache-2.0
|
98
101
|
metadata:
|
99
|
-
homepage_uri: https://github.com/
|
100
|
-
source_code_uri: https://github.com/
|
101
|
-
changelog_uri: https://github.com/
|
102
|
+
homepage_uri: https://github.com/maxim/portrayal
|
103
|
+
source_code_uri: https://github.com/maxim/portrayal
|
104
|
+
changelog_uri: https://github.com/maxim/portrayal/blob/main/CHANGELOG.md
|
102
105
|
post_install_message:
|
103
106
|
rdoc_options: []
|
104
107
|
require_paths:
|
@@ -114,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
117
|
- !ruby/object:Gem::Version
|
115
118
|
version: '0'
|
116
119
|
requirements: []
|
117
|
-
rubygems_version: 3.
|
120
|
+
rubygems_version: 3.4.2
|
118
121
|
signing_key:
|
119
122
|
specification_version: 4
|
120
123
|
summary: A minimal builder for struct-like classes
|