phlexi-field 0.0.1 → 0.0.2

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: 0be36da5b3ce1dae979b31c55d9a0ad18e9b4aa9f206e47f29ea97f55bc2c89b
4
- data.tar.gz: 73584d4874fa1fa59746b34053f5e5d77edf6f3780f73be42b3d2ca677167c12
3
+ metadata.gz: 908d11456437457ad31bb719555098d0f1b9dce04fc4703548f5e1147896c3f8
4
+ data.tar.gz: 23e4bb8265525dd4acc3e7ad529fa31121b68b91751e12482173d42184a12b96
5
5
  SHA512:
6
- metadata.gz: de662238d5a8716422335d4d33b21e5b317e78c0fd247eb9c6185f3a0cd1294faab5192fcd1d1887a548825953c04ebfb2fc5696e3c7762fed662ae0199ee7d5
7
- data.tar.gz: 631cdd6a03e32750863c632e61b0a447dde1ce20ccb5f20e1b513aba7937ea120f0733b1a6327c39f729561898c3f7a99e7bbefaf81736f0b27bd359d2cf23b8
6
+ metadata.gz: 9936ccc9e4fcb1e7e7a0255036be6106962758a5b23e854fdf7434c029fcac389e9527c9717eee81384ebc0cb6cea173c98a018e7f100288fc6e6c15aecb9ef3
7
+ data.tar.gz: a766d9646d8d0e36967caf5be49cc5546e7909ad174326407011db6ad6695e3dcc56b001f2cd1882e40479b3d81d6fd066a486456eb8af356bff760563fa9be9
@@ -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 association_reflection&.macro == :has_many
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.2"
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.2
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-09-15 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