phlexi-field 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0be36da5b3ce1dae979b31c55d9a0ad18e9b4aa9f206e47f29ea97f55bc2c89b
4
- data.tar.gz: 73584d4874fa1fa59746b34053f5e5d77edf6f3780f73be42b3d2ca677167c12
3
+ metadata.gz: 70fdd02c6979235fa18a95692de12b4ebefec2468e2bb71bb0a2a25d7692de78
4
+ data.tar.gz: 40feabf408438dc28b7907c8e66b5d04aa5d336e986ab5cbfc08c6a51b290f33
5
5
  SHA512:
6
- metadata.gz: de662238d5a8716422335d4d33b21e5b317e78c0fd247eb9c6185f3a0cd1294faab5192fcd1d1887a548825953c04ebfb2fc5696e3c7762fed662ae0199ee7d5
7
- data.tar.gz: 631cdd6a03e32750863c632e61b0a447dde1ce20ccb5f20e1b513aba7937ea120f0733b1a6327c39f729561898c3f7a99e7bbefaf81736f0b27bd359d2cf23b8
6
+ metadata.gz: ae593aa3bc1c50f6a4f4ff93caf9553de907ddf53759d282ab2ede1b5cba99b0a001ff8853f01fcf205e8eb0dfe237a0a5c378866dd4b7cc1793a901fefbfd24
7
+ data.tar.gz: 7abc8faa67e3a6b7a4baf232f507dc0d7923bb3e722852ff9b63e2d8b8771e09de538d5db05eb5a18ec97e16a4776d7b5bd93dafa6d30cef79162e45155761a5
@@ -13,13 +13,15 @@ module Phlexi
13
13
  # @attr_accessor [Object] value The value of the field.
14
14
  class Builder < Structure::Node
15
15
  include Phlex::Helpers
16
- include Options::Associations
17
- include Options::Attachments
18
- include Options::Descriptions
19
- include Options::Hints
16
+ include Options::Validators
20
17
  include Options::InferredTypes
18
+ include Options::Multiple
21
19
  include Options::Labels
22
20
  include Options::Placeholders
21
+ include Options::Descriptions
22
+ include Options::Hints
23
+ include Options::Associations
24
+ include Options::Attachments
23
25
 
24
26
  class DOM < Structure::DOM; end
25
27
 
@@ -52,12 +54,12 @@ module Phlexi
52
54
  self.class::FieldCollection.new(field: self, collection:, &)
53
55
  end
54
56
 
55
- protected
56
-
57
57
  def has_value?
58
- value.present?
58
+ attachment_reflection.present? ? value.attached? : (value.present? || value == false)
59
59
  end
60
60
 
61
+ protected
62
+
61
63
  def determine_initial_value(value)
62
64
  return value unless value == NIL_VALUE
63
65
 
@@ -4,12 +4,12 @@ module Phlexi
4
4
  module Field
5
5
  module Options
6
6
  module Associations
7
- protected
8
-
9
7
  def association_reflection
10
8
  @association_reflection ||= find_association_reflection
11
9
  end
12
10
 
11
+ protected
12
+
13
13
  def find_association_reflection
14
14
  if object.class.respond_to?(:reflect_on_association)
15
15
  object.class.reflect_on_association(key)
@@ -4,12 +4,12 @@ module Phlexi
4
4
  module Field
5
5
  module Options
6
6
  module Attachments
7
- protected
8
-
9
7
  def attachment_reflection
10
8
  @attachment_reflection ||= find_attachment_reflection
11
9
  end
12
10
 
11
+ protected
12
+
13
13
  def find_attachment_reflection
14
14
  if object.class.respond_to?(:reflect_on_attachment)
15
15
  object.class.reflect_on_attachment(key)
@@ -6,44 +6,38 @@ module Phlexi
6
6
  module Field
7
7
  module Options
8
8
  module InferredTypes
9
+ def inferred_field_component
10
+ @inferred_component ||= infer_field_component
11
+ end
12
+
9
13
  def inferred_field_type
10
14
  @inferred_field_type ||= infer_field_type
11
15
  end
12
16
 
13
- def inferred_field_component
14
- @inferred_component ||= infer_field_component
17
+ def inferred_string_field_type
18
+ @inferred_string_field_type || infer_string_field_type
15
19
  end
16
20
 
17
21
  private
18
22
 
19
23
  def infer_field_component
20
- case inferred_field_type
21
- when :string, :text
22
- infer_string_field_component(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
24
+ inferred_field_type
40
25
  end
41
26
 
42
27
  def infer_field_type
28
+ # Check attachments first since they are implemented as associations
29
+ return :attachment if attachment_reflection
30
+
31
+ return :association if association_reflection
32
+
33
+ if object.class.respond_to?(:defined_enums)
34
+ return :enum if object.class.defined_enums.key?(key.to_s)
35
+ end
36
+
43
37
  if object.class.respond_to?(:columns_hash)
44
- # ActiveRecord object
38
+ # ActiveRecord
45
39
  column = object.class.columns_hash[key.to_s]
46
- return column.type if column
40
+ return column.type if column&.type
47
41
  end
48
42
 
49
43
  if object.class.respond_to?(:attribute_types)
@@ -58,6 +52,12 @@ module Phlexi
58
52
  return infer_field_type_from_value(object.send(key))
59
53
  end
60
54
 
55
+ # Check if object is a has that contains key
56
+ if object.respond_to?(:fetch)
57
+ # Fallback to inferring type from the value
58
+ return infer_field_type_from_value(object.fetch(key))
59
+ end
60
+
61
61
  # Default to string if we can't determine the type
62
62
  :string
63
63
  end
@@ -83,18 +83,24 @@ module Phlexi
83
83
  end
84
84
  end
85
85
 
86
- def infer_string_field_component(key)
87
- key = key.to_s.downcase
88
-
89
- return :password if is_password_field?
86
+ def infer_string_field_type
87
+ infer_string_field_type_from_key || infer_string_field_type_from_validations
88
+ end
90
89
 
91
- custom_type = custom_string_field_type(key)
92
- return custom_type if custom_type
90
+ def infer_string_field_type_from_validations
91
+ return unless has_validators?
93
92
 
94
- :text
93
+ if attribute_validators.find { |v| v.kind == :numericality }
94
+ :number
95
+ elsif attribute_validators.find { |v| v.kind == :format && v.options[:with] == URI::MailTo::EMAIL_REGEXP }
96
+ :email
97
+ end
95
98
  end
96
99
 
97
- def custom_string_field_type(key)
100
+ def infer_string_field_type_from_key
101
+ key = self.key.to_s.downcase
102
+ return :password if is_password_field?(key)
103
+
98
104
  custom_mappings = {
99
105
  /url$|^link|^site/ => :url,
100
106
  /^email/ => :email,
@@ -103,7 +109,7 @@ module Phlexi
103
109
  /^time/ => :time,
104
110
  /^date/ => :date,
105
111
  /^number|_count$|_amount$/ => :number,
106
- /^color/ => :color
112
+ /^color|_color$/ => :color
107
113
  }
108
114
 
109
115
  custom_mappings.each do |pattern, type|
@@ -113,9 +119,7 @@ module Phlexi
113
119
  nil
114
120
  end
115
121
 
116
- def is_password_field?
117
- key = self.key.to_s.downcase
118
-
122
+ def is_password_field?(key)
119
123
  exact_matches = ["password"]
120
124
  prefixes = ["encrypted_"]
121
125
  suffixes = ["_password", "_digest", "_hash", "_token"]
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Field
5
+ module Options
6
+ module Multiple
7
+ def multiple?
8
+ options[:multiple] = options.fetch(:multiple) { calculate_multiple_field_value }
9
+ end
10
+
11
+ def multiple!(multiple = true)
12
+ options[:multiple] = multiple
13
+ self
14
+ end
15
+
16
+ private
17
+
18
+ def calculate_multiple_field_value
19
+ return true if attachment_reflection&.macro == :has_many_attached
20
+ return true if %i[has_many has_and_belongs_to_many].include?(association_reflection&.macro)
21
+ return true if multiple_field_array_attribute?
22
+
23
+ check_multiple_field_from_validators
24
+ end
25
+
26
+ def multiple_field_array_attribute?
27
+ return false unless object.class.respond_to?(:columns_hash)
28
+
29
+ column = object.class.columns_hash[key.to_s]
30
+ return false unless column
31
+
32
+ case object.class.connection.adapter_name.downcase
33
+ when "postgresql"
34
+ column.array? || (column.type == :string && column.sql_type.include?("[]"))
35
+ end # || object.class.attribute_types[key.to_s].is_a?(ActiveRecord::Type::Serialized)
36
+ rescue
37
+ # Rails.logger.warn("Error checking multiple field array attribute: #{e.message}")
38
+ false
39
+ end
40
+
41
+ def check_multiple_field_from_validators
42
+ inclusion_validator = find_validator(:inclusion)
43
+ length_validator = find_validator(:length)
44
+
45
+ return false unless inclusion_validator || length_validator
46
+
47
+ check_multiple_field_inclusion_validator(inclusion_validator) ||
48
+ check_multiple_field_length_validator(length_validator)
49
+ end
50
+
51
+ def check_multiple_field_inclusion_validator(validator)
52
+ return false unless validator
53
+ in_option = validator.options[:in] || validator.options[:within]
54
+ return false unless in_option.is_a?(Array)
55
+
56
+ validator.options[:multiple] == true || (multiple_field_array_attribute? && in_option.size > 1)
57
+ end
58
+
59
+ def check_multiple_field_length_validator(validator)
60
+ return false unless validator
61
+ validator.options[:maximum].to_i > 1 if validator.options[:maximum]
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Field
5
+ module Options
6
+ module Validators
7
+ private
8
+
9
+ def has_validators?
10
+ @has_validators ||= object.class.respond_to?(:validators_on)
11
+ end
12
+
13
+ def attribute_validators
14
+ object.class.validators_on(key)
15
+ end
16
+
17
+ def association_reflection_validators
18
+ association_reflection ? object.class.validators_on(association_reflection.name) : []
19
+ end
20
+
21
+ def valid_validator?(validator)
22
+ !conditional_validators?(validator) && action_validator_match?(validator)
23
+ end
24
+
25
+ def conditional_validators?(validator)
26
+ validator.options.include?(:if) || validator.options.include?(:unless)
27
+ end
28
+
29
+ def action_validator_match?(validator)
30
+ return true unless validator.options.include?(:on)
31
+
32
+ case validator.options[:on]
33
+ when :save
34
+ true
35
+ when :create
36
+ !object.persisted?
37
+ when :update
38
+ object.persisted?
39
+ end
40
+ end
41
+
42
+ def find_validator(kind)
43
+ attribute_validators.find { |v| v.kind == kind && valid_validator?(v) } if has_validators?
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -53,7 +53,7 @@ module Phlexi
53
53
  def keys
54
54
  @keys ||= lineage.map do |node|
55
55
  # If the parent of a field is a field, the name should be nil.
56
- node.key unless node.parent.is_a? FieldBuilder
56
+ node.key unless node.parent.is_a? Builder
57
57
  end
58
58
  end
59
59
  end
@@ -7,6 +7,8 @@ module Phlexi
7
7
  include Enumerable
8
8
 
9
9
  class Builder
10
+ include Phlex::Helpers
11
+
10
12
  attr_reader :key, :index
11
13
 
12
14
  def initialize(key, field, index)
@@ -18,6 +18,8 @@ module Phlexi
18
18
  class Namespace < Structure::Node
19
19
  include Enumerable
20
20
 
21
+ class NamespaceCollection < Structure::NamespaceCollection; end
22
+
21
23
  attr_reader :builder_klass, :object
22
24
 
23
25
  def initialize(key, parent:, builder_klass:, object: nil)
@@ -71,7 +73,7 @@ module Phlexi
71
73
  # to another `Namespace` or `Field`.
72
74
  def nest_many(key, collection: nil, &)
73
75
  collection ||= Array(object_value_for(key: key))
74
- create_child(key, NamespaceCollection, collection:, &)
76
+ create_child(key, self.class::NamespaceCollection, collection:, &)
75
77
  end
76
78
 
77
79
  # Iterates through the children of the current namespace, which could be `Namespace` or `Field`
@@ -84,10 +86,8 @@ module Phlexi
84
86
  @dom_id ||= begin
85
87
  id = if object.nil?
86
88
  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
89
+ elsif (primary_key = Phlexi::Field.object_primary_key(object))
90
+ primary_key&.to_s || :new
91
91
  end
92
92
  [key, id].compact.join("_").underscore
93
93
  end
@@ -20,7 +20,7 @@ module Phlexi
20
20
  end
21
21
 
22
22
  def inspect
23
- "<#{self.class.name} key=#{key.inspect} parent=#{id.inspect} />"
23
+ "<#{self.class.name} key=#{key.inspect} parent=#{parent.inspect} />"
24
24
  end
25
25
  end
26
26
  end
@@ -2,11 +2,10 @@ require "fiber/local"
2
2
 
3
3
  module Phlexi
4
4
  module Field
5
- module Theme
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- extend Fiber::Local
5
+ class Theme
6
+ def self.inherited(subclass)
7
+ super
8
+ subclass.extend Fiber::Local
10
9
  end
11
10
 
12
11
  # Retrieves the theme hash
@@ -26,8 +25,12 @@ module Phlexi
26
25
  #
27
26
  # @example Theme inheritance
28
27
  # theme[:email] # Returns :text, indicating email inherits text's theme
28
+ def self.theme
29
+ raise NotImplementedError, "#{self} must implement #self.theme"
30
+ end
31
+
29
32
  def theme
30
- raise NotImplementedError, "#{self.class} must implement #theme"
33
+ @theme ||= self.class.theme.freeze
31
34
  end
32
35
 
33
36
  # Recursively resolves the theme for a given property, handling nested symbol references
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlexi
4
4
  module Field
5
- VERSION = "0.0.1"
5
+ VERSION = "0.0.3"
6
6
  end
7
7
  end
data/lib/phlexi/field.rb CHANGED
@@ -5,6 +5,8 @@ require "phlex"
5
5
  require "active_support/core_ext/object/blank"
6
6
 
7
7
  module Phlexi
8
+ NIL_VALUE = :__i_phlexi_i__
9
+
8
10
  module Field
9
11
  Loader = Zeitwerk::Loader.new.tap do |loader|
10
12
  loader.tag = File.basename(__FILE__, ".rb")
@@ -19,8 +21,14 @@ module Phlexi
19
21
 
20
22
  COMPONENT_BASE = (defined?(::ApplicationComponent) ? ::ApplicationComponent : Phlex::HTML)
21
23
 
22
- NIL_VALUE = :__i_phlexi_i__
23
-
24
24
  class Error < StandardError; end
25
+
26
+ def self.object_primary_key(object)
27
+ if object.class.respond_to?(:primary_key)
28
+ object.send(object.class.primary_key.to_sym)
29
+ elsif object.respond_to?(:id)
30
+ object.id
31
+ end
32
+ end
25
33
  end
26
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlexi-field
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-07 00:00:00.000000000 Z
11
+ date: 2024-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: phlex
@@ -208,7 +208,9 @@ files:
208
208
  - lib/phlexi/field/options/hints.rb
209
209
  - lib/phlexi/field/options/inferred_types.rb
210
210
  - lib/phlexi/field/options/labels.rb
211
+ - lib/phlexi/field/options/multiple.rb
211
212
  - lib/phlexi/field/options/placeholders.rb
213
+ - lib/phlexi/field/options/validators.rb
212
214
  - lib/phlexi/field/structure/dom.rb
213
215
  - lib/phlexi/field/structure/field_collection.rb
214
216
  - lib/phlexi/field/structure/namespace.rb