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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afdba4b2e460ce31fb430639e1270683d61d2b41bcb276a056e042ac8de8c61d
4
- data.tar.gz: 39f66cc7e9224d04660d3cc1e1317f9c66752e2fd739eeb9068f10751275f9f8
3
+ metadata.gz: acf906a0356260f40fbe25b94b12f72f8c696ee723dff3c2f7dc0f3817154fd0
4
+ data.tar.gz: 0bb4b5a5d4982a59f6e66cd7fa2fe7eeec93b7880537c9e605c47f2d1b0df009
5
5
  SHA512:
6
- metadata.gz: 894bcfa84622297c663f9fa88c4f829a8475fa092caff07d047768adef1a110e5726747f53a284e6d823c57ead388a14fc4e5d0bb68342c463a170a725fa9442
7
- data.tar.gz: 19dc417df48b783a7847957fdc8899c143b0e92ac4aaef35f1b07342d7d02fbea1091d504d9d47318c7baee0cf538616209555903307f5e8aace2267fd777fce
6
+ metadata.gz: 5d3543a802ba4dad5b8c746fa2e187b0debb95bd52354509bb29c2d88e6977d622aeafc8fca3db0a65811a9918c7044985c3355bea78799a0b7730b84da28e21
7
+ data.tar.gz: 66f61afd2be1633c12739d49db088265d90279b622b4f4bf050bba2c4d7dc6444ce232e16be00485702ef9285fe2507758d8fdfee447414ac35bc44fe07a5e89
@@ -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@v2
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/scottscheapflights/portrayal/commit/9e9db2cafc7eae14789c5b84f70efd18898ace76)
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/scottscheapflights/portrayal/commit/665ad297fb71fcdf5f641c672a457ccbe29e4a49)
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/scottscheapflights/portrayal/commit/f346483a379ce9fbdece72cde8b0844f2d22b1cd)
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/scottscheapflights/portrayal/commit/f6ec8f373c6582f7e8d8f872d289222e4a58f8f6)
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/scottscheapflights/portrayal/commit/9c5a37e4fb91e35d23b22e208344452930452af7)
51
- * Define a protected writer for every keyword - useful when applying changes after `dup`/`clone`. [[commit]](https://github.com/scottscheapflights/portrayal/commit/1c0fa6c6357a09760dae39165e864238d231a08e)
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/scottscheapflights/portrayal/commit/ba9e390ab4aea4733ba084ac273da448e313ea53)
53
- * Make `#freeze` propagate to all keyword values. [[commit]](https://github.com/scottscheapflights/portrayal/commit/0a734411a6eac08e2355c4277e09a2a70800d032)
54
- * Make `#dup` and `#clone` propagate to all keyword values. [[commit]](https://github.com/scottscheapflights/portrayal/commit/010632d87d81a8d5b5ea5ff27d3d209cc667b0a5)
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/scottscheapflights/portrayal/commit/a1cc9d0fd40e413210f61b945d37b81c87280fee)
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/scottscheapflights/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 (~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.
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
- ### Schema
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 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.
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
- 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.
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 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.
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 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`.
420
472
 
421
473
  ```irb
422
- Address.portrayal.definition_of_initialize
423
- => "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
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/scottscheapflights/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.
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/scottscheapflights/portrayal/blob/main/CODE_OF_CONDUCT.md).
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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ find lib -name '*.rb' | xargs wc -l
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,72 +1,72 @@
1
1
  require 'portrayal/default'
2
2
 
3
- module Portrayal
4
- class Schema
5
- attr_reader :schema
3
+ class Portrayal::Schema
4
+ attr_reader :schema, :module
5
+ NULL = Object.new.freeze
6
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
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
- def ==(other)
12
- return super unless other.class.is_a?(Portrayal)
13
-
14
- self.class.portrayal.attributes(self) ==
15
- self.class.portrayal.attributes(other)
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
- def camelize(string); string.to_s.gsub(/(?:^|_+)([^_])/) { $1.upcase } 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
47
24
 
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
25
+ def render_module_code
26
+ args, inits, syms, hash, eqls, dups, clones, setters, freezes =
27
+ '', '', '', '', '', '', '', '', ''
53
28
 
54
- def initialize_dup(other)
55
- super; @schema = other.schema.transform_values(&:dup)
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
- 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('; ')
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
- "def initialize(#{init_args}); #{init_assigns} end"
70
- end
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
@@ -1,3 +1,3 @@
1
1
  module Portrayal
2
- VERSION = '0.7.1'
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
@@ -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 = ['Maxim Chernyak']
7
- spec.email = ['madfancier@gmail.com']
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/scottscheapflights/portrayal'
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/scottscheapflights/portrayal/blob/main/CHANGELOG.md'
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.9'
27
- spec.add_development_dependency 'pry', '~> 0.13'
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.7.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
- - Maxim Chernyak
7
+ - Max Chernyak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-22 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
- name: bundler
14
+ name: rake
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.1'
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: '2.1'
26
+ version: '13.0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rake
28
+ name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '13.0'
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: '13.0'
40
+ version: '3.12'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rspec
42
+ name: pry
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.9'
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: '3.9'
54
+ version: '0.14'
55
55
  - !ruby/object:Gem::Dependency
56
- name: pry
56
+ name: benchmark-ips
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.13'
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: '0.13'
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
- - madfancier@gmail.com
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/scottscheapflights/portrayal
98
+ homepage: https://github.com/maxim/portrayal
96
99
  licenses:
97
100
  - Apache-2.0
98
101
  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
+ 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.1.2
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