phlexi-table 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.ruby-version +1 -0
  4. data/Appraisals +8 -0
  5. data/CHANGELOG.md +5 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +13 -0
  8. data/Rakefile +14 -0
  9. data/TODO +0 -0
  10. data/config.ru +6 -0
  11. data/gemfiles/default.gemfile +5 -0
  12. data/gemfiles/rails_7.gemfile +8 -0
  13. data/lib/phlexi/table/base.rb +119 -0
  14. data/lib/phlexi/table/components/base.rb +38 -0
  15. data/lib/phlexi/table/components/concerns/displays_value.rb +54 -0
  16. data/lib/phlexi/table/components/date_time.rb +49 -0
  17. data/lib/phlexi/table/components/description.rb +21 -0
  18. data/lib/phlexi/table/components/hint.rb +21 -0
  19. data/lib/phlexi/table/components/label.rb +15 -0
  20. data/lib/phlexi/table/components/number.rb +37 -0
  21. data/lib/phlexi/table/components/placeholder.rb +15 -0
  22. data/lib/phlexi/table/components/string.rb +17 -0
  23. data/lib/phlexi/table/components/wrapper.rb +17 -0
  24. data/lib/phlexi/table/field_options/associations.rb +21 -0
  25. data/lib/phlexi/table/field_options/attachments.rb +21 -0
  26. data/lib/phlexi/table/field_options/description.rb +22 -0
  27. data/lib/phlexi/table/field_options/hints.rb +22 -0
  28. data/lib/phlexi/table/field_options/inferred_types.rb +129 -0
  29. data/lib/phlexi/table/field_options/labels.rb +28 -0
  30. data/lib/phlexi/table/field_options/placeholders.rb +18 -0
  31. data/lib/phlexi/table/field_options/themes.rb +132 -0
  32. data/lib/phlexi/table/structure/dom.rb +42 -0
  33. data/lib/phlexi/table/structure/field_builder.rb +158 -0
  34. data/lib/phlexi/table/structure/field_collection.rb +39 -0
  35. data/lib/phlexi/table/structure/namespace.rb +123 -0
  36. data/lib/phlexi/table/structure/namespace_collection.rb +40 -0
  37. data/lib/phlexi/table/structure/node.rb +24 -0
  38. data/lib/phlexi/table/version.rb +7 -0
  39. data/lib/phlexi/table.rb +30 -0
  40. data/lib/phlexi-table.rb +3 -0
  41. data/sig/phlexi/table.rbs +6 -0
  42. metadata +241 -0
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+
5
+ module Phlexi
6
+ module Table
7
+ module FieldOptions
8
+ module InferredTypes
9
+ def inferred_db_type
10
+ @inferred_db_type ||= infer_db_type
11
+ end
12
+
13
+ def inferred_display_component
14
+ @inferred_display_component ||= infer_display_component
15
+ end
16
+
17
+ private
18
+
19
+ def infer_display_component
20
+ case inferred_db_type
21
+ when :string, :text
22
+ infer_string_display_type(key)
23
+ when :integer, :float, :decimal
24
+ :number
25
+ when :date, :datetime, :time
26
+ :date
27
+ when :boolean
28
+ :boolean
29
+ when :json, :jsonb, :hstore
30
+ :code
31
+ else
32
+ if association_reflection
33
+ :association
34
+ elsif attachment_reflection
35
+ :attachment
36
+ else
37
+ :text
38
+ end
39
+ end
40
+ end
41
+
42
+ def infer_db_type
43
+ if object.class.respond_to?(:columns_hash)
44
+ # ActiveRecord object
45
+ column = object.class.columns_hash[key.to_s]
46
+ return column.type if column
47
+ end
48
+
49
+ if object.class.respond_to?(:attribute_types)
50
+ # ActiveModel::Attributes
51
+ custom_type = object.class.attribute_types[key.to_s]
52
+ return custom_type.type if custom_type
53
+ end
54
+
55
+ # Check if object responds to the key
56
+ if object.respond_to?(key)
57
+ # Fallback to inferring type from the value
58
+ return infer_db_type_from_value(object.send(key))
59
+ end
60
+
61
+ # Default to string if we can't determine the type
62
+ :string
63
+ end
64
+
65
+ def infer_db_type_from_value(value)
66
+ case value
67
+ when Integer
68
+ :integer
69
+ when Float
70
+ :float
71
+ when BigDecimal
72
+ :decimal
73
+ when TrueClass, FalseClass
74
+ :boolean
75
+ when Date
76
+ :date
77
+ when Time, DateTime
78
+ :datetime
79
+ when Hash
80
+ :json
81
+ else
82
+ :string
83
+ end
84
+ end
85
+
86
+ def infer_string_display_type(key)
87
+ key = key.to_s.downcase
88
+
89
+ return :password if is_password_field?
90
+
91
+ custom_type = custom_string_display_type(key)
92
+ return custom_type if custom_type
93
+
94
+ :text
95
+ end
96
+
97
+ def custom_string_display_type(key)
98
+ custom_mappings = {
99
+ /url$|^link|^site/ => :url,
100
+ /^email/ => :email,
101
+ /phone|tel(ephone)?/ => :phone,
102
+ /^time/ => :time,
103
+ /^date/ => :date,
104
+ /^number|_count$|_amount$/ => :number,
105
+ /^color/ => :color
106
+ }
107
+
108
+ custom_mappings.each do |pattern, type|
109
+ return type if key.match?(pattern)
110
+ end
111
+
112
+ nil
113
+ end
114
+
115
+ def is_password_field?
116
+ key = self.key.to_s.downcase
117
+
118
+ exact_matches = ["password"]
119
+ prefixes = ["encrypted_"]
120
+ suffixes = ["_password", "_digest", "_hash"]
121
+
122
+ exact_matches.include?(key) ||
123
+ prefixes.any? { |prefix| key.start_with?(prefix) } ||
124
+ suffixes.any? { |suffix| key.end_with?(suffix) }
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module FieldOptions
6
+ module Labels
7
+ def label(label = nil)
8
+ if label.nil?
9
+ options[:label] = options.fetch(:label) { calculate_label }
10
+ else
11
+ options[:label] = label
12
+ self
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def calculate_label
19
+ if object.class.respond_to?(:human_attribute_name)
20
+ object.class.human_attribute_name(key.to_s, {base: object})
21
+ else
22
+ key.to_s.humanize
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module FieldOptions
6
+ module Placeholders
7
+ def placeholder(placeholder = nil)
8
+ if placeholder.nil?
9
+ options[:placeholder] || "-"
10
+ else
11
+ options[:placeholder] = placeholder
12
+ self
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module FieldOptions
6
+ module Themes
7
+ # Resolves theme classes for components based on their type.
8
+ #
9
+ # This method is responsible for determining the appropriate CSS classes for a given display component.
10
+ # It supports a hierarchical theming system, allowing for cascading themes and easy customization.
11
+ #
12
+ # @param component [Symbol, String] The type of display component (e.g., :text, :date, :boolean)
13
+ #
14
+ # @return [String, nil] A string of CSS classes for the component, or nil if no theme is applied
15
+ #
16
+ # @example Basic usage
17
+ # themed(:text)
18
+ # # => "text-gray-700 text-sm"
19
+ #
20
+ # @example Cascading themes
21
+ # # Assuming email inherits from text in the theme definition
22
+ # themed(:email)
23
+ # # => "text-gray-700 text-sm text-blue-600 underline"
24
+ #
25
+ # @note The actual CSS classes returned will depend on the theme definitions in the `theme` hash
26
+ # and any overrides specified in the `options` hash.
27
+ #
28
+ # @see #resolve_theme
29
+ # @see #theme
30
+ def themed(component)
31
+ return unless component
32
+
33
+ resolve_theme(component)
34
+ end
35
+
36
+ protected
37
+
38
+ # Recursively resolves the theme for a given property, handling nested symbol references
39
+ #
40
+ # @param property [Symbol, String] The theme property to resolve
41
+ # @param visited [Set] Set of already visited properties to prevent infinite recursion
42
+ # @return [String, nil] The resolved theme value or nil if not found
43
+ #
44
+ # @example Resolving a nested theme
45
+ # # Assuming the theme is: { email: :text, text: "text-gray-700" }
46
+ # resolve_theme(:email)
47
+ # # => "text-gray-700"
48
+ def resolve_theme(property, visited = Set.new)
49
+ return nil if !property.present? || visited.include?(property)
50
+ visited.add(property)
51
+
52
+ result = theme[property]
53
+ if result.is_a?(Symbol)
54
+ resolve_theme(result, visited)
55
+ else
56
+ result
57
+ end
58
+ end
59
+
60
+ # Retrieves or initializes the theme hash for the display builder.
61
+ #
62
+ # This method returns a hash containing theme definitions for various display components.
63
+ # If a theme has been explicitly set in the options, it returns that. Otherwise, it
64
+ # initializes and returns a default theme.
65
+ #
66
+ # The theme hash defines CSS classes or references to other theme keys for different
67
+ # components.
68
+ #
69
+ # @return [Hash] A hash containing theme definitions for display components
70
+ #
71
+ # @example Accessing the theme
72
+ # theme[:text]
73
+ # # => "text-gray-700 text-sm"
74
+ #
75
+ # @example Theme inheritance
76
+ # theme[:email] # Returns :text, indicating email inherits text's theme
77
+ #
78
+ # @note The actual content of the theme hash depends on the default_theme method
79
+ # and any theme overrides specified in the options when initializing the field builder.
80
+ #
81
+ # @see #default_theme
82
+ def theme
83
+ @theme ||= options[:theme] || default_theme
84
+ end
85
+
86
+ # Defines and returns the default theme hash for the display builder.
87
+ #
88
+ # This method returns a hash containing the base theme definitions for various components.
89
+ # It sets up the default styling and relationships between different components.
90
+ # The theme uses a combination of explicit CSS classes and symbolic references to other theme keys,
91
+ # allowing for a flexible and inheritance-based theming system.
92
+ #
93
+ # @return [Hash] A frozen hash containing default theme definitions for components
94
+ #
95
+ # @example Accessing the default theme
96
+ # default_theme[:text]
97
+ # # => "text-gray-700 text-sm"
98
+ #
99
+ # @example Theme inheritance
100
+ # default_theme[:email]
101
+ # # => :text (indicates that :email inherits from :text)
102
+ #
103
+ # @note This method returns a frozen hash to prevent accidental modifications.
104
+ # To customize the theme, users should provide their own theme hash when initializing the display builder.
105
+ #
106
+ # @see #theme
107
+ def default_theme
108
+ {
109
+ label: "text-base font-bold text-gray-500 dark:text-gray-400 mb-1",
110
+ description: "text-sm text-gray-400 dark:text-gray-500",
111
+ placeholder: "text-xl font-semibold text-gray-500 dark:text-gray-300 mb-1 italic",
112
+ string: "text-xl font-semibold text-gray-900 dark:text-white mb-1",
113
+ # text: :string,
114
+ number: :string,
115
+ datetime: :string,
116
+ # boolean: :string,
117
+ # code: :string,
118
+ # email: :text,
119
+ # url: :text,
120
+ # phone: :text,
121
+ # color: :text,
122
+ # search: :text,
123
+ # password: :string,
124
+ # association: :string,
125
+ attachment: :string,
126
+ wrapper: nil
127
+ }.freeze
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module Structure
6
+ # Generates DOM IDs for a Field, Namespace, or Node based on
7
+ # norms that were established by Rails. These can be used outside of Rails in
8
+ # other Ruby web frameworks since it has no dependencies on Rails.
9
+ class DOM
10
+ def initialize(field:)
11
+ @field = field
12
+ end
13
+
14
+ # Converts the value of the field to a String, which is required to work
15
+ # with Phlex. Assumes that `Object#to_s` emits a format suitable for display.
16
+ def value
17
+ @field.value.to_s
18
+ end
19
+
20
+ # Walks from the current node to the parent node, grabs the names, and separates
21
+ # them with a `_` for a DOM ID.
22
+ def id
23
+ @id ||= begin
24
+ root, *rest = lineage
25
+ root_key = root.respond_to?(:dom_id) ? root.dom_id : root.key
26
+ rest.map(&:key).unshift(root_key).join("_")
27
+ end
28
+ end
29
+
30
+ # One-liner way of walking from the current node all the way up to the parent.
31
+ def lineage
32
+ @lineage ||= Enumerator.produce(@field, &:parent).take_while(&:itself).reverse
33
+ end
34
+
35
+ # Emit the id and value in an HTML tag-ish that doesn't have an element.
36
+ def inspect
37
+ "<#{self.class.name} id=#{id.inspect} value=#{value.inspect}/>"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "phlex"
4
+
5
+ module Phlexi
6
+ module Table
7
+ module Structure
8
+ # FieldBuilder class is responsible for building display fields with various options and components.
9
+ #
10
+ # @attr_reader [Structure::DOM] dom The DOM structure for the field.
11
+ # @attr_reader [Hash] options Options for the field.
12
+ # @attr_reader [Object] object The object associated with the field.
13
+ # @attr_reader [Hash] attributes Attributes for the field.
14
+ # @attr_accessor [Object] value The value of the field.
15
+ class FieldBuilder < Node
16
+ include Phlex::Helpers
17
+ include FieldOptions::Themes
18
+ include FieldOptions::Associations
19
+ include FieldOptions::Attachments
20
+ include FieldOptions::InferredTypes
21
+ include FieldOptions::Labels
22
+ include FieldOptions::Placeholders
23
+ include FieldOptions::Description
24
+ # include FieldOptions::Hints
25
+
26
+ attr_reader :dom, :options, :object, :value
27
+
28
+ # Initializes a new FieldBuilder instance.
29
+ #
30
+ # @param key [Symbol, String] The key for the field.
31
+ # @param parent [Structure::Namespace] The parent object.
32
+ # @param object [Object, nil] The associated object.
33
+ # @param value [Object] The initial value for the field.
34
+ # @param options [Hash] Additional options for the field.
35
+ def initialize(key, parent:, object: nil, value: NIL_VALUE, **options)
36
+ super(key, parent: parent)
37
+
38
+ @object = object
39
+ @value = determine_value(value)
40
+ @options = options
41
+ @dom = Structure::DOM.new(field: self)
42
+ end
43
+
44
+ # Creates a label tag for the field.
45
+ #
46
+ # @param attributes [Hash] Additional attributes for the label.
47
+ # @return [Components::Label] The label component.
48
+ def label_tag(**, &)
49
+ create_component(Components::Label, :label, **, &)
50
+ end
51
+
52
+ # Creates a Placeholder tag for the field.
53
+ #
54
+ # @param attributes [Hash] Additional attributes for the placeholder.
55
+ # @return [Components::Placeholder] The placeholder component.
56
+ def placeholder_tag(**, &)
57
+ create_component(Components::Placeholder, :placeholder, **, &)
58
+ end
59
+
60
+ # Creates a Description tag for the field.
61
+ #
62
+ # @param attributes [Hash] Additional attributes for the description.
63
+ # @return [Components::Description] The description component.
64
+ def description_tag(**, &)
65
+ create_component(Components::Description, :description, **, &)
66
+ end
67
+
68
+ # Creates a string display tag for the field.
69
+ #
70
+ # @param attributes [Hash] Additional attributes for the string display.
71
+ # @return [Components::String] The string component.
72
+ def string_tag(**, &)
73
+ create_component(Components::String, :string, **, &)
74
+ end
75
+
76
+ # # Creates a text display tag for the field.
77
+ # #
78
+ # # @param attributes [Hash] Additional attributes for the text display.
79
+ # # @return [Components::Text] The text component.
80
+ # def text_tag(**, &)
81
+ # create_component(Components::Text, :text, **, &)
82
+ # end
83
+
84
+ # Creates a number display tag for the field.
85
+ #
86
+ # @param attributes [Hash] Additional attributes for the number display.
87
+ # @return [Components::Number] The number component.
88
+ def number_tag(**, &)
89
+ create_component(Components::Number, :number, **, &)
90
+ end
91
+
92
+ # Creates a datetime display for the field.
93
+ #
94
+ # @param attributes [Hash] Additional attributes for the datetime display.
95
+ # @return [Components::DateTime] The datetime component.
96
+ def datetime_tag(**, &)
97
+ create_component(Components::DateTime, :datetime, **, &)
98
+ end
99
+
100
+ # # Creates a boolean display tag for the field.
101
+ # #
102
+ # # @param attributes [Hash] Additional attributes for the boolean display.
103
+ # # @return [Components::Boolean] The boolean component.
104
+ # def boolean_tag(**, &)
105
+ # create_component(Components::Boolean, :boolean, **, &)
106
+ # end
107
+
108
+ # # Creates an association display tag for the field.
109
+ # #
110
+ # # @param attributes [Hash] Additional attributes for the association display.
111
+ # # @return [Components::Association] The association component.
112
+ # def association_tag(**, &)
113
+ # create_component(Components::Association, :association, **, &)
114
+ # end
115
+
116
+ # # Creates an attachment display tag for the field.
117
+ # #
118
+ # # @param attributes [Hash] Additional attributes for the attachment display.
119
+ # # @return [Components::Attachment] The attachment component.
120
+ # def attachment_tag(**, &)
121
+ # create_component(Components::Attachment, :attachment, **, &)
122
+ # end
123
+
124
+ # Wraps the field with additional markup.
125
+ #
126
+ # @param attributes [Hash] Additional attributes for the wrapper.
127
+ # @yield [block] The block to be executed within the wrapper.
128
+ # @return [Components::Wrapper] The wrapper component.
129
+ def wrapped(**, &)
130
+ create_component(Components::Wrapper, :wrapper, **, &)
131
+ end
132
+
133
+ # Creates a repeated field collection.
134
+ #
135
+ # @param range [#each] The collection of items to generate displays for.
136
+ # @yield [block] The block to be executed for each item in the collection.
137
+ # @return [FieldCollection] The field collection.
138
+ def repeated(collection = [], &)
139
+ FieldCollection.new(field: self, collection:, &)
140
+ end
141
+
142
+ protected
143
+
144
+ def create_component(component_class, theme_key, **attributes, &)
145
+ theme_key = attributes.delete(:theme) || theme_key
146
+ attributes = mix({class: themed(theme_key)}, attributes) unless attributes.key?(:class!)
147
+ component_class.new(self, **attributes, &)
148
+ end
149
+
150
+ def determine_value(value)
151
+ return value unless value == NIL_VALUE
152
+
153
+ object.respond_to?(key) ? object.public_send(key) : nil
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module Structure
6
+ class FieldCollection
7
+ include Enumerable
8
+
9
+ class Builder
10
+ attr_reader :key, :index
11
+
12
+ def initialize(key, field, index)
13
+ @key = key.to_s
14
+ @field = field
15
+ @index = index
16
+ end
17
+
18
+ def field(**)
19
+ @field.class.new(key, **, parent: @field).tap do |field|
20
+ yield field if block_given?
21
+ end
22
+ end
23
+ end
24
+
25
+ def initialize(field:, collection:, &)
26
+ @field = field
27
+ @collection = collection
28
+ each(&) if block_given?
29
+ end
30
+
31
+ def each(&)
32
+ @collection.each.with_index do |item, index|
33
+ yield Builder.new(item, @field, index)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module Structure
6
+ # A Namespace maps an object to values, but doesn't actually have a value itself. For
7
+ # example, a `User` object or ActiveRecord model could be passed into the `:user` namespace.
8
+ #
9
+ # To access single values on a Namespace, #field can be used.
10
+ #
11
+ # To access nested objects within a namespace, two methods are available:
12
+ #
13
+ # 1. #nest_one: Used for single nested objects, such as if a `User belongs_to :profile` in
14
+ # ActiveRecord. This method returns another Namespace object.
15
+ #
16
+ # 2. #nest_many: Used for collections of nested objects, such as if a `User has_many :addresses` in
17
+ # ActiveRecord. This method returns a NamespaceCollection object.
18
+ class Namespace < Structure::Node
19
+ include Enumerable
20
+
21
+ attr_reader :builder_klass, :object
22
+
23
+ def initialize(key, parent:, builder_klass:, object: nil)
24
+ super(key, parent: parent)
25
+ @builder_klass = builder_klass
26
+ @object = object
27
+ @children = {}
28
+ yield self if block_given?
29
+ end
30
+
31
+ def field(key, **attributes)
32
+ create_child(key, attributes.delete(:builder_klass) || builder_klass, object: object, **attributes).tap do |field|
33
+ yield field if block_given?
34
+ end
35
+ end
36
+
37
+ # Creates a `Namespace` child instance with the parent set to the current instance, adds to
38
+ # the `@children` Hash to ensure duplicate child namespaces aren't created, then calls the
39
+ # method on the `@object` to get the child object to pass into that namespace.
40
+ #
41
+ # For example, if a `User#permission` returns a `Permission` object, we could map that to a
42
+ # display like this:
43
+ #
44
+ # ```ruby
45
+ # Phlexi::Table(user) do |display|
46
+ # display.nest_one :profile do |profile|
47
+ # render profile.field(:gender).text
48
+ # end
49
+ # end
50
+ # ```
51
+ def nest_one(key, object: nil, &)
52
+ object ||= object_value_for(key: key)
53
+ create_child(key, self.class, object:, builder_klass:, &)
54
+ end
55
+
56
+ # Wraps an array of objects in Namespace classes. For example, if `User#addresses` returns
57
+ # an enumerable or array of `Address` classes:
58
+ #
59
+ # ```ruby
60
+ # Phlexi::Table(user) do |display|
61
+ # render display.field(:email).text
62
+ # render display.field(:name).text
63
+ # display.nest_many :addresses do |address|
64
+ # render address.field(:street).text
65
+ # render address.field(:state).text
66
+ # render address.field(:zip).text
67
+ # end
68
+ # end
69
+ # ```
70
+ # The object within the block is a `Namespace` object that maps each object within the enumerable
71
+ # to another `Namespace` or `Field`.
72
+ def nest_many(key, collection: nil, &)
73
+ collection ||= Array(object_value_for(key: key))
74
+ create_child(key, NamespaceCollection, collection:, &)
75
+ end
76
+
77
+ # Iterates through the children of the current namespace, which could be `Namespace` or `Field`
78
+ # objects.
79
+ def each(&)
80
+ @children.values.each(&)
81
+ end
82
+
83
+ def dom_id
84
+ @dom_id ||= begin
85
+ id = if object.nil?
86
+ nil
87
+ elsif object.class.respond_to?(:primary_key)
88
+ object.public_send(object.class.primary_key) || :new
89
+ elsif object.respond_to?(:id)
90
+ object.id || :new
91
+ end
92
+ [key, id].compact.join("_").underscore
93
+ end
94
+ end
95
+
96
+ # Creates a root Namespace
97
+ def self.root(*, builder_klass:, **, &)
98
+ new(*, parent: nil, builder_klass:, **, &)
99
+ end
100
+
101
+ protected
102
+
103
+ # Calls the corresponding method on the object for the `key` name, if it exists. For example
104
+ # if the `key` is `email` on `User`, this method would call `User#email` if the method is
105
+ # present.
106
+ #
107
+ # This method could be overwritten if the mapping between the `@object` and `key` name is not
108
+ # a method call. For example, a `Hash` would be accessed via `user[:email]` instead of `user.send(:email)`
109
+ def object_value_for(key:)
110
+ @object.send(key) if @object.respond_to? key
111
+ end
112
+
113
+ private
114
+
115
+ # Checks if the child exists. If it does then it returns that. If it doesn't, it will
116
+ # build the child.
117
+ def create_child(key, child_class, **kwargs, &block)
118
+ @children.fetch(key) { @children[key] = child_class.new(key, parent: self, **kwargs, &block) }
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end