blueprinter 1.0.2 → 1.1.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: 8f568763ae7429ac9d060433ca9b95ac7c22642bea54b615e27325976b2fe9ec
4
- data.tar.gz: e613aab3c612acef0077905839d63f87b15ecf698b16a5df6d7103daa47e904c
3
+ metadata.gz: c9d38943f08cbd1ded61e894b8b3360ec2e0948f8fa87a1fe4cb789d740a99d5
4
+ data.tar.gz: c17c2f18caafe392da5f6e8aeee5efa3bf238a1b16e081de7db6f783fd84ac1c
5
5
  SHA512:
6
- metadata.gz: 9f81f013c2a94c7acad72dea46d2c8e0d472311f13e2588fd78c53c0ab23b2c8a86e55be64b49c690485d6aaca8d506545255c29bed456cef1c6aece5d0271fd
7
- data.tar.gz: ac3eaa8b6c4d1f9883ae1117d9c30b3c8801cab831ae9d474e3d5bafc3417cfaeef18f52305059909234f7b60a03906645b5fcdd7615f8dc4e880c859610830a
6
+ metadata.gz: 44975112b64d2c569299fdf06a30012c44560eaf2f774de9ff595291af9de34178baa6f393367224ffa1c7ae20568a5085eb8c9754e67f9af98bb82d1ce6d28a
7
+ data.tar.gz: ccbc9b45404a7a30a3dae41505bf6759b3032d839139b1b0a3f15ca0b9d565a742fb26f770d55049528264fb4154978e39557b459668b2080bc8678379c4526d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
+ ## 1.1.0 - 2024/08/02
2
+ * [BREAKING] Drops support for Ruby 2.7. See [#402](https://github.com/procore-oss/blueprinter/pull/402). Thanks to [@jmeridth](https://github.com/jmeridth)
3
+ * 🚜 [REFACTOR] Cleans up Blueprint validation logic and implements an `Association` class with a clearer interface. See [#414](https://github.com/procore-oss/blueprinter/pull/414). Thanks to [@lessthanjacob](https://github.com/lessthanjacob).
4
+ * 💅 [ENHANCEMENT] Updates **Transform Classes** documentation to provide a more understandable example. See [#415](https://github.com/procore-oss/blueprinter/pull/415). Thanks to [@SaxtonDrey](https://github.com/SaxtonDrey).
5
+ * 💅 [ENHANCEMENT] Implements field-level configuration option for excluding an attribute from the result of a render if its value is `nil`. See [#425](https://github.com/procore-oss/blueprinter/pull/425). Thanks to [jamesst20](https://github.com/jamesst20).
6
+ * 🚜 [REFACTOR] Adds explicit dependency on `json` within `Blueprinter::Configuration`. See [#444](https://github.com/procore-oss/blueprinter/pull/444). Thanks to [@lessthanjacob](https://github.com/lessthanjacob).
7
+ * 🚜 [REFACTOR] Alters file loading to leverage `autoload` instead of `require` for (future) optional, top-level constants. See [#445](https://github.com/procore-oss/blueprinter/pull/445). Thanks to [@jhollinger](https://github.com/jhollinger).
8
+
1
9
  ## 1.0.2 - 2024/02/02
2
- * 🐛 [BUGFIX] Fixes an issue with reflection where fields are incorrectly override by their definitions in the default view. See [#391](https://github.com/procore-oss/blueprinter/pull/391). Thanks to [@elliothursh](https://github.com/elliothursh).
10
+ * 🐛 [BUGFIX] [BREAKING] Fixes an issue with reflection where fields are incorrectly override by their definitions in the default view. Note: this may be a breaking change for users of the extensions API, but restores the intended functionality. See [#391](https://github.com/procore-oss/blueprinter/pull/391). Thanks to [@elliothursh](https://github.com/elliothursh).
3
11
 
4
12
  ## 1.0.1 - 2024/01/19
5
13
  * 🐛 [BUGFIX] Fixes an issue where serialization performance would become degraded when using a Blueprint that leverages transformers. See [#381](https://github.com/procore-oss/blueprinter/pull/381). Thanks to [@Pritilender](https://github.com/Pritilender).
data/README.md CHANGED
@@ -694,6 +694,36 @@ _NOTE:_ The field-level setting overrides the global config setting (for the fie
694
694
 
695
695
  </details>
696
696
 
697
+ <details>
698
+ <summary>Exclude Fields with nil Values</summary>
699
+
700
+
701
+ By default, fields with `nil` values are included when rendering. You can override this behavior by setting `:exclude_if_nil: true` in the field definition.
702
+
703
+ Usage:
704
+
705
+ ```ruby
706
+ class UserBlueprint < Blueprinter::Base
707
+ identifier :uuid
708
+
709
+ field :name
710
+ field :birthday, exclude_if_nil: true
711
+ end
712
+
713
+ user = User.new(name: 'John Doe')
714
+ puts UserBlueprint.render(user)
715
+ ```
716
+
717
+ Output:
718
+
719
+ ```json
720
+ {
721
+ "name": "John Doe"
722
+ }
723
+ ```
724
+
725
+ </details>
726
+
697
727
  <details>
698
728
  <summary>Custom Formatting for Dates and Times</summary>
699
729
 
@@ -773,7 +803,7 @@ Create a Transform class extending from `Blueprinter::Transformer`
773
803
 
774
804
  ```ruby
775
805
  class DynamicFieldTransformer < Blueprinter::Transformer
776
- def transform(hash, object, options)
806
+ def transform(hash, object, _options)
777
807
  hash.merge!(object.dynamic_fields)
778
808
  end
779
809
  end
@@ -781,12 +811,23 @@ end
781
811
 
782
812
  ```ruby
783
813
  class User
784
- def custom_columns
785
- self.dynamic_fields #which is an array of some columns
786
- end
787
-
788
- def custom_fields
789
- custom_columns.each_with_object({}){|col,result| result[col] = self.send(col)}
814
+ def dynamic_fields
815
+ case role
816
+ when :admin
817
+ {
818
+ employer: employer,
819
+ time_in_role: determine_time_in role
820
+ }
821
+ when :maintainer
822
+ {
823
+ label: label,
824
+ settings: generate_settings_hash
825
+ }
826
+ when :read_only
827
+ {
828
+ last_login_at: last_login_at
829
+ }
830
+ end
790
831
  end
791
832
  end
792
833
  ```
@@ -930,6 +971,66 @@ Output:
930
971
 
931
972
  </details>
932
973
 
974
+ <details>
975
+ <summary>Reflection</summary>
976
+
977
+ Blueprint classes may be reflected on to inspect their views, fields, and associations. Extensions often make use of this ability.
978
+
979
+ ```ruby
980
+ class WidgetBlueprint < Blueprinter::Base
981
+ fields :name, :description
982
+ association :category, blueprint: CategoryBlueprint
983
+
984
+ view :extended do
985
+ field :price
986
+ association :parts, blueprint: WidgetPartBlueprint
987
+ end
988
+ end
989
+
990
+ # A Hash of views keyed by name
991
+ views = WidgetBlueprint.reflections
992
+ views.keys
993
+ => [:default, :extended]
994
+
995
+ # Hashes of fields and associations, keyed by name
996
+ fields = views[:default].fields
997
+ assoc = views[:default].associations
998
+
999
+ # Get info about a field
1000
+ fields[:description].name
1001
+ fields[:description].display_name
1002
+ fields[:description].options
1003
+
1004
+ # Get info about an association
1005
+ assoc[:category].name
1006
+ assoc[:category].display_name
1007
+ assoc[:category].blueprint
1008
+ assoc[:category].view
1009
+ assoc[:category].options
1010
+ ```
1011
+ </details>
1012
+
1013
+ <details>
1014
+ <summary>Extensions</summary>
1015
+
1016
+ Blueprinter offers an extension system to hook into and modify certain behavior.
1017
+
1018
+ ```ruby
1019
+ Blueprinter.configure do |config|
1020
+ config.extensions << MyExtension.new
1021
+ config.extensions << OtherExtension.new
1022
+ end
1023
+ ```
1024
+
1025
+ Extension hooks:
1026
+
1027
+ * [pre_render](https://github.com/procore-oss/blueprinter/blob/abca9ca8ed23edd65a0f4b5ae43e25b8e27a2afc/lib/blueprinter/extension.rb#L18): Intercept the object before rendering begins
1028
+
1029
+ Some known extensions are:
1030
+
1031
+ * [blueprinter-activerecord](https://github.com/procore-oss/blueprinter-activerecord)
1032
+ </details>
1033
+
933
1034
  <details>
934
1035
  <summary>Deprecations</summary>
935
1036
 
data/Rakefile CHANGED
@@ -34,7 +34,7 @@ YARD::Rake::YardocTask.new do |t|
34
34
  end
35
35
 
36
36
  Rake::TestTask.new(:benchmarks) do |t|
37
- t.libs << 'spec'
37
+ t.libs.append('lib', 'spec')
38
38
  t.pattern = 'spec/benchmarks/**/*_test.rb'
39
39
  t.verbose = false
40
40
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'blueprinter/field'
4
+ require 'blueprinter/blueprint_validator'
5
+ require 'blueprinter/extractors/association_extractor'
6
+
7
+ module Blueprinter
8
+ # @api private
9
+ class Association < Field
10
+ # @param method [Symbol] The method to call on the source object to retrieve the associated data
11
+ # @param name [Symbol] The name of the association as it will appear when rendered
12
+ # @param blueprint [Blueprinter::Base] The blueprint to use for rendering the association
13
+ # @param view [Symbol] The view to use in conjunction with the blueprint
14
+ # @param extractor [Blueprinter::Extractor] The extractor to use when retrieving the associated data
15
+ # @param options [Hash]
16
+ #
17
+ # @return [Blueprinter::Association]
18
+ def initialize(method:, name:, blueprint:, view:, extractor: AssociationExtractor.new, options: {})
19
+ BlueprintValidator.validate!(blueprint)
20
+
21
+ super(
22
+ method,
23
+ name,
24
+ extractor,
25
+ blueprint,
26
+ options.merge(
27
+ blueprint: blueprint,
28
+ view: view,
29
+ association: true
30
+ )
31
+ )
32
+ end
33
+ end
34
+ end
@@ -1,23 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'blueprinter_error'
4
- require_relative 'configuration'
5
- require_relative 'deprecation'
6
- require_relative 'empty_types'
7
- require_relative 'extractor'
8
- require_relative 'extractors/association_extractor'
9
- require_relative 'extractors/auto_extractor'
10
- require_relative 'extractors/block_extractor'
11
- require_relative 'extractors/hash_extractor'
12
- require_relative 'extractors/public_send_extractor'
13
- require_relative 'formatters/date_time_formatter'
14
- require_relative 'field'
15
- require_relative 'helpers/type_helpers'
16
- require_relative 'helpers/base_helpers'
17
- require_relative 'view'
18
- require_relative 'view_collection'
19
- require_relative 'transformer'
20
- require_relative 'reflection'
3
+ require 'blueprinter/association'
4
+ require 'blueprinter/extractors/association_extractor'
5
+ require 'blueprinter/field'
6
+ require 'blueprinter/helpers/base_helpers'
7
+ require 'blueprinter/reflection'
21
8
 
22
9
  module Blueprinter
23
10
  class Base
@@ -159,17 +146,18 @@ module Blueprinter
159
146
  # end
160
147
  # end
161
148
  #
162
- # @return [Field] A Field object
149
+ # @return [Association] An object
150
+ # @raise [Blueprinter::Errors::InvalidBlueprint] if provided blueprint is not valid
163
151
  def self.association(method, options = {}, &block)
164
- validate_blueprint!(options[:blueprint], method)
152
+ raise ArgumentError, ':blueprint must be provided when defining an association' unless options[:blueprint]
165
153
 
166
- field(
167
- method,
168
- options.merge(
169
- association: true,
170
- extractor: options.fetch(:extractor) { AssociationExtractor.new }
171
- ),
172
- &block
154
+ current_view << Association.new(
155
+ method: method,
156
+ name: options.delete(:name) || method,
157
+ extractor: options.delete(:extractor) || AssociationExtractor.new,
158
+ blueprint: options.delete(:blueprint),
159
+ view: options.delete(:view) || :default,
160
+ options: options.merge(block: block)
173
161
  )
174
162
  end
175
163
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blueprinter
4
+ # @api private
5
+ class BlueprintValidator
6
+ class << self
7
+ # Determines whether the provided object is a valid Blueprint.
8
+ #
9
+ # @param blueprint [Object] The object to validate.
10
+ # @return [Boolean] true if object is a valid Blueprint
11
+ # @raise [Blueprinter::Errors::InvalidBlueprint] if the object is not a valid Blueprint.
12
+ def validate!(blueprint)
13
+ if valid_blueprint?(blueprint)
14
+ true
15
+ else
16
+ raise(
17
+ Errors::InvalidBlueprint,
18
+ "#{blueprint} is not a valid blueprint. Please ensure it subclasses Blueprinter::Base or is a Proc."
19
+ )
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def valid_blueprint?(blueprint)
26
+ return false unless blueprint
27
+ return true if blueprint.is_a?(Proc)
28
+ return false unless blueprint.is_a?(Class)
29
+
30
+ blueprint <= Blueprinter::Base
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'extensions'
3
+ require 'json'
4
+ require 'blueprinter/extensions'
5
+ require 'blueprinter/extractors/auto_extractor'
4
6
 
5
7
  module Blueprinter
6
8
  class Configuration
@@ -48,12 +50,4 @@ module Blueprinter
48
50
  VALID_CALLABLES.include?(callable_name)
49
51
  end
50
52
  end
51
-
52
- def self.configuration
53
- @configuration ||= Configuration.new
54
- end
55
-
56
- def self.configure
57
- yield configuration if block_given?
58
- end
59
53
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'helpers/type_helpers'
3
+ require 'blueprinter/helpers/type_helpers'
4
4
 
5
5
  module Blueprinter
6
6
  EMPTY_COLLECTION = 'empty_collection'
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blueprinter
4
+ module Errors
5
+ class InvalidBlueprint < Blueprinter::BlueprinterError; end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blueprinter
4
+ module Errors
5
+ autoload :InvalidBlueprint, 'blueprinter/errors/invalid_blueprint'
6
+ end
7
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'blueprinter/extractor'
4
+ require 'blueprinter/empty_types'
5
+
3
6
  module Blueprinter
4
7
  # @api private
5
8
  class AssociationExtractor < Extractor
@@ -10,7 +13,7 @@ module Blueprinter
10
13
  end
11
14
 
12
15
  def extract(association_name, object, local_options, options = {})
13
- options_without_default = options.reject { |k, _| %i[default default_if].include?(k) }
16
+ options_without_default = options.except(:default, :default_if)
14
17
  # Merge in assocation options hash
15
18
  local_options = local_options.merge(options[:options]) if options[:options].is_a?(Hash)
16
19
  value = @extractor.extract(association_name, object, local_options, options_without_default)
@@ -1,5 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'blueprinter/extractor'
4
+ require 'blueprinter/empty_types'
5
+ require 'blueprinter/extractors/block_extractor'
6
+ require 'blueprinter/extractors/hash_extractor'
7
+ require 'blueprinter/extractors/public_send_extractor'
8
+ require 'blueprinter/formatters/date_time_formatter'
9
+
3
10
  module Blueprinter
4
11
  # @api private
5
12
  class AutoExtractor < Extractor
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'blueprinter/extractor'
4
+
3
5
  module Blueprinter
4
6
  # @api private
5
7
  class BlockExtractor < Extractor
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'blueprinter/extractor'
4
+
3
5
  module Blueprinter
4
6
  # @api private
5
7
  class HashExtractor < Extractor
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'blueprinter/extractor'
4
+
3
5
  module Blueprinter
4
6
  # @api private
5
7
  class PublicSendExtractor < Extractor
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'blueprinter/helpers/type_helpers'
4
+ require 'blueprinter/view_collection'
5
+
3
6
  module Blueprinter
4
7
  module BaseHelpers
5
8
  def self.included(base)
@@ -48,7 +51,11 @@ module Blueprinter
48
51
  result_hash = view_collection.fields_for(view_name).each_with_object({}) do |field, hash|
49
52
  next if field.skip?(field.name, object, local_options)
50
53
 
51
- hash[field.name] = field.extract(object, local_options)
54
+ value = field.extract(object, local_options)
55
+
56
+ next if value.nil? && field.options[:exclude_if_nil]
57
+
58
+ hash[field.name] = value
52
59
  end
53
60
  view_collection.transformers(view_name).each do |transformer|
54
61
  transformer.transform(result_hash, object, local_options)
@@ -67,44 +74,6 @@ module Blueprinter
67
74
  end
68
75
  end
69
76
 
70
- def dynamic_blueprint?(blueprint)
71
- blueprint.is_a?(Proc)
72
- end
73
-
74
- def validate_blueprint!(blueprint, method)
75
- validate_presence_of_blueprint!(blueprint)
76
- return if dynamic_blueprint?(blueprint)
77
-
78
- validate_blueprint_has_ancestors!(blueprint, method)
79
- validate_blueprint_has_blueprinter_base_ancestor!(blueprint, method)
80
- end
81
-
82
- def validate_presence_of_blueprint!(blueprint)
83
- raise BlueprinterError, 'Blueprint required' unless blueprint
84
- end
85
-
86
- def validate_blueprint_has_ancestors!(blueprint, association_name)
87
- # If the class passed as a blueprint does not respond to ancestors
88
- # it means it, at the very least, does not have Blueprinter::Base as
89
- # one of its ancestor classes (e.g: Hash) and thus an error should
90
- # be raised.
91
- return if blueprint.respond_to?(:ancestors)
92
-
93
- raise BlueprinterError, "Blueprint provided for #{association_name} " \
94
- 'association is not valid.'
95
- end
96
-
97
- def validate_blueprint_has_blueprinter_base_ancestor!(blueprint, association_name)
98
- # Guard clause in case Blueprinter::Base is present in the ancestor list
99
- # for the blueprint class provided.
100
- return if blueprint.ancestors.include? Blueprinter::Base
101
-
102
- # Raise error describing what's wrong.
103
- raise BlueprinterError, "Class #{blueprint.name} does not inherit from " \
104
- 'Blueprinter::Base and is not a valid Blueprinter ' \
105
- "for #{association_name} association."
106
- end
107
-
108
77
  def jsonify(blob)
109
78
  Blueprinter.configuration.jsonify(blob)
110
79
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Blueprinter
4
- VERSION = '1.0.2'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'blueprinter/view'
4
+
3
5
  module Blueprinter
4
6
  # @api private
5
7
  class ViewCollection
data/lib/blueprinter.rb CHANGED
@@ -1,7 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'blueprinter/base'
4
- require_relative 'blueprinter/extension'
5
-
6
3
  module Blueprinter
4
+ autoload :Base, 'blueprinter/base'
5
+ autoload :BlueprinterError, 'blueprinter/blueprinter_error'
6
+ autoload :Configuration, 'blueprinter/configuration'
7
+ autoload :Errors, 'blueprinter/errors'
8
+ autoload :Extension, 'blueprinter/extension'
9
+ autoload :Transformer, 'blueprinter/transformer'
10
+
11
+ class << self
12
+ # @return [Configuration]
13
+ def configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+
17
+ def configure
18
+ yield(configuration) if block_given?
19
+ end
20
+
21
+ # Resets global configuration.
22
+ def reset_configuration!
23
+ @configuration = nil
24
+ end
25
+ end
7
26
  end
metadata CHANGED
@@ -1,35 +1,39 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blueprinter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Procore Technologies, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-04 00:00:00.000000000 Z
11
+ date: 2024-08-12 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Blueprinter is a JSON Object Presenter for Ruby that takes business objects
14
14
  and breaks them down into simple hashes and serializes them to JSON. It can be used
15
15
  in Rails in place of other serializers (like JBuilder or ActiveModelSerializers).
16
16
  It is designed to be simple, direct, and performant.
17
17
  email:
18
- - blueprinter@googlegroups.com
18
+ - opensource@procore.com
19
19
  executables: []
20
20
  extensions: []
21
21
  extra_rdoc_files: []
22
22
  files:
23
23
  - CHANGELOG.md
24
- - MIT-LICENSE
24
+ - LICENSE.md
25
25
  - README.md
26
26
  - Rakefile
27
27
  - lib/blueprinter.rb
28
+ - lib/blueprinter/association.rb
28
29
  - lib/blueprinter/base.rb
30
+ - lib/blueprinter/blueprint_validator.rb
29
31
  - lib/blueprinter/blueprinter_error.rb
30
32
  - lib/blueprinter/configuration.rb
31
33
  - lib/blueprinter/deprecation.rb
32
34
  - lib/blueprinter/empty_types.rb
35
+ - lib/blueprinter/errors.rb
36
+ - lib/blueprinter/errors/invalid_blueprint.rb
33
37
  - lib/blueprinter/extension.rb
34
38
  - lib/blueprinter/extensions.rb
35
39
  - lib/blueprinter/extractor.rb
@@ -63,7 +67,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
63
67
  requirements:
64
68
  - - ">="
65
69
  - !ruby/object:Gem::Version
66
- version: '2.7'
70
+ version: '3.0'
67
71
  required_rubygems_version: !ruby/object:Gem::Requirement
68
72
  requirements:
69
73
  - - ">="
File without changes