vident 0.6.3 → 0.8.0

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.
@@ -1,229 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/concern"
4
-
5
- if Gem.loaded_specs.has_key? "dry-struct"
6
- require_relative "./types"
7
- require_relative "./typed_niling_struct"
8
-
9
- module Vident
10
- # Adapts Dry Types to confinus Typed Attributes. We use dry-struct (see ::Core::NilingStruct) but
11
- # we could probably also use dry-initializer directly, saving us from maintaining the schema.
12
- module Attributes
13
- module Typed
14
- extend ActiveSupport::Concern
15
-
16
- # TODO: better handling of when either class.schema is undefined (as no attributes configured) or when
17
- # other methods ar called before prepare_attributes is called
18
- def prepare_attributes(attributes)
19
- @__attributes = self.class.schema.new(**attributes)
20
- end
21
-
22
- def attributes
23
- @__attributes.attributes
24
- end
25
-
26
- def attribute_names
27
- @attribute_names ||= self.class.attribute_names
28
- end
29
-
30
- def attribute(key)
31
- if Rails.env.development? && !key?(key)
32
- raise StandardError, "Attribute #{key} not found in #{self.class.name}"
33
- end
34
- @__attributes.attributes[key]
35
- end
36
- alias_method :[], :attribute
37
-
38
- def key?(key)
39
- self.class.schema.attribute_names.include?(key)
40
- end
41
-
42
- def to_hash
43
- @__attributes.to_h
44
- end
45
-
46
- class_methods do
47
- def inherited(subclass)
48
- subclass.instance_variable_set(:@schema, @schema.clone)
49
- subclass.instance_variable_set(:@attribute_ivar_names, @attribute_ivar_names.clone)
50
- super
51
- end
52
-
53
- def attribute_names
54
- schema.attribute_names
55
- end
56
-
57
- def attribute_metadata(key)
58
- schema.schema.key(key).meta
59
- end
60
-
61
- attr_reader :schema, :attribute_ivar_names
62
-
63
- def attribute(name, signature = :any, **options, &converter)
64
- strict = !options[:convert]
65
- signatures = extract_member_type_and_subclass(signature, options)
66
- type_info = map_primitive_to_dry_type(signatures, strict, converter)
67
- type_info = set_constraints(type_info, options)
68
- type_info = set_metadata(type_info, signatures, options)
69
- define_on_schema(name, type_info, options)
70
- end
71
-
72
- private
73
-
74
- def set_constraints(type_info, options)
75
- if allows_nil?(options)
76
- type_info = type_info.optional.meta(required: false)
77
- end
78
- unless allows_blank?(options)
79
- type_info = type_info.constrained(filled: true)
80
- end
81
- if options[:default]&.is_a?(Proc)
82
- type_info = type_info.default(options[:default].freeze)
83
- elsif !options[:default].nil?
84
- type_info = type_info.default(->(_) { options[:default] }.freeze)
85
- end
86
- if options[:in]
87
- type_info.constrained(included_in: options[:in].freeze)
88
- end
89
- type_info
90
- end
91
-
92
- def set_metadata(type_info, signatures, options)
93
- metadata = {typed_attribute_type: signatures, typed_attribute_options: options}
94
- type_info.meta(**metadata)
95
- end
96
-
97
- def delegates?(options)
98
- options[:delegates] != false
99
- end
100
-
101
- def define_on_schema(attribute_name, type_info, options)
102
- @attribute_ivar_names ||= {}
103
- @attribute_ivar_names[attribute_name] = :"@#{attribute_name}"
104
- define_attribute_delegate(attribute_name) if delegates?(options)
105
- @schema ||= const_set(:TypedSchema, Class.new(Vident::Attributes::TypedNilingStruct))
106
- @schema.attribute attribute_name, type_info
107
- end
108
-
109
- def define_attribute_delegate(attr_name)
110
- # Define reader & presence check method
111
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
112
- def #{attr_name}
113
- @__attributes.attributes[:#{attr_name}]
114
- end
115
-
116
- def #{attr_name}?
117
- send(:#{attr_name}).present?
118
- end
119
- RUBY
120
- end
121
-
122
- def allows_nil?(options)
123
- return true unless options
124
- allow_nil = options[:allow_nil]
125
- return false if allow_nil == false
126
- allow_nil || allows_blank?(options)
127
- end
128
-
129
- def allows_blank?(options)
130
- return true unless options
131
- allow_blank = options[:allow_blank]
132
- allow_blank.nil? ? true : allow_blank
133
- end
134
-
135
- def map_primitive_to_dry_type(signatures, strict, converter)
136
- types = signatures.map do |type, subtype|
137
- dry_type = dry_type_from_primary_type(type, strict, converter)
138
- if subtype && dry_type.respond_to?(:of)
139
- subtype_info = dry_type_from_primary_type(subtype, strict, converter)
140
- # Sub types of collections currently can be nil - this should be an option
141
- dry_type.of(subtype_info.optional.meta(required: false))
142
- else
143
- dry_type
144
- end
145
- end
146
- types.reduce(:|)
147
- end
148
-
149
- def extract_member_type_and_subclass(signature, options)
150
- case signature
151
- when Set
152
- signature.flat_map { |s| extract_member_type_and_subclass(s, options) }
153
- when Array
154
- [[Array, signature.first]]
155
- else
156
- [[signature, options[:type] || options[:sub_type]]]
157
- end
158
- end
159
-
160
- def dry_type_from_primary_type(type, strict, converter)
161
- # If a converter is provided, we should use it to coerce the value
162
- if converter && !strict && !type.is_a?(Symbol)
163
- return Types.Constructor(type) do |value|
164
- next value if value.is_a?(type)
165
-
166
- converter.call(value).tap do |new_value|
167
- unless new_value.is_a?(type)
168
- raise ArgumentError, "Type conversion proc did not convert #{value} to #{type}"
169
- end
170
- end
171
- end
172
- end
173
-
174
- if type == :any
175
- Types::Nominal::Any
176
- elsif type == Integer
177
- strict ? Types::Strict::Integer : Types::Params::Integer
178
- elsif type == BigDecimal
179
- strict ? Types::Strict::Decimal : Types::Params::Decimal
180
- elsif type == Float
181
- strict ? Types::Strict::Float : Types::Params::Float
182
- elsif type == Numeric
183
- if strict
184
- Types::Strict::Float | Types::Strict::Integer | Types::Strict::Decimal
185
- else
186
- Types::Params::Float | Types::Params::Integer | Types::Params::Decimal
187
- end
188
- elsif type == Symbol
189
- strict ? Types::Strict::Symbol : Types::Coercible::Symbol
190
- elsif type == String
191
- strict ? Types::Strict::String : Types::Coercible::String
192
- elsif type == Time
193
- strict ? Types::Strict::Time : Types::Params::Time
194
- elsif type == Date
195
- strict ? Types::Strict::Date : Types::Params::Date
196
- elsif type == Array
197
- strict ? Types::Strict::Array : Types::Params::Array
198
- elsif type == Hash
199
- strict ? Types::Strict::Hash : Types::Coercible::Hash
200
- elsif type == :boolean
201
- strict ? Types::Strict::Bool : Types::Params::Bool
202
- elsif strict
203
- # when strict create a Nominal type with a is_a? constraint, otherwise create a Nominal type which constructs
204
- # values using the default constructor, `new`.
205
- Types.Instance(type)
206
- else
207
- # dry calls this when initialising the Type. Check if type of input is correct or create new instance
208
- Types.Constructor(type) do |value|
209
- next value if value.is_a?(type)
210
-
211
- type.new(**value)
212
- end
213
- end
214
- end
215
- end
216
- end
217
- end
218
- end
219
- else
220
- module Vident
221
- module Attributes
222
- module Typed
223
- def self.included(base)
224
- raise "Vident::Attributes::Typed requires dry-struct to be installed"
225
- end
226
- end
227
- end
228
- end
229
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- module Attributes
5
- # A dry struct that is loose about keys provided on initialization. It sets them to nil if not provided.
6
- # It is strict about types but not about provided keys
7
- class TypedNilingStruct < ::Dry::Struct
8
- # convert string keys to symbols
9
- transform_keys(&:to_sym)
10
-
11
- # resolve default types on nil
12
- transform_types do |type|
13
- if type.default?
14
- type.constructor { |value| value.nil? ? ::Dry::Types::Undefined : value }
15
- else
16
- type
17
- end
18
- end
19
-
20
- class << self
21
- def check_schema_duplication(new_keys)
22
- # allow overriding keys
23
- end
24
- end
25
- end
26
- end
27
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "dry-struct"
4
-
5
- module Vident
6
- module Attributes
7
- module Types
8
- include ::Dry.Types
9
-
10
- StrippedString = Types::String.constructor(&:strip)
11
- BooleanDefaultFalse = Types::Bool.default(false)
12
- BooleanDefaultTrue = Types::Bool.default(true)
13
- HashDefaultEmpty = Types::Hash.default({}.freeze)
14
- end
15
- end
16
- end
@@ -1,145 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Rails fragment caching works by either expecting the cached key object to respond to `cache_key` or for that object
4
- # to be an array or hash. In our case the object maybe an instance of Core::Presenter so here we add a default
5
- # `cache_key` implementation.
6
- module Vident
7
- module Caching
8
- module CacheKey
9
- extend ActiveSupport::Concern
10
-
11
- class_methods do
12
- def inherited(subclass)
13
- subclass.instance_variable_set(
14
- :@named_cache_key_attributes,
15
- @named_cache_key_attributes.clone
16
- )
17
- super
18
- end
19
-
20
- def with_cache_key(*attrs, name: :_collection)
21
- raise StandardError, "with_cache_key can only be used on components *without* slots as there is no eary way to track their content changes so too risky" if respond_to?(:slots?) && slots?
22
- # Add view file to cache key
23
- attrs << :component_modified_time
24
- named_cache_key_includes(name, *attrs)
25
- end
26
-
27
- attr_reader :named_cache_key_attributes
28
-
29
- # TypedComponents can be used with fragment caching, but you need to be careful! Read on...
30
- #
31
- # <% cache component do %>
32
- # <%= render component %>
33
- # <% end %>
34
- #
35
- # The most important point is that Rails cannot track dependencies on the component itself, so you need to
36
- # be careful to be explicit on the attributes, and manually specify any sub Viewcomponent dependencies that the
37
- # component has. The assumption is that the subcomponent takes any attributes from the parent, so the cache key
38
- # depends on the parent component attributes. Otherwise changes to the parent or sub component views/Ruby class
39
- # will result in different cache keys too. Of course if you invalidate all cache keys with a modifier on deploy
40
- # then no need to worry about changing the cache key on component changes, only on attribute/data changes.
41
- #
42
- # A big caveat is that the cache key cannot depend on anything related to the view_context of the component (such
43
- # as `helpers` as the key is created before the rending pipline is invoked (which is when the view_context is set).
44
- def depends_on(*klasses)
45
- @component_dependencies ||= []
46
- @component_dependencies += klasses
47
- end
48
-
49
- attr_reader :component_dependencies
50
-
51
- def component_modified_time
52
- return @component_modified_time if Rails.env.production? && @component_modified_time
53
- # FIXME: This could stack overflow if there are circular dependencies
54
- deps = component_dependencies&.map(&:component_modified_time)&.join("-") || ""
55
- @component_modified_time = deps + sidecar_view_modified_time + rb_component_modified_time
56
- end
57
-
58
- def sidecar_view_modified_time
59
- return @sidecar_view_modified_time if Rails.env.production? && defined?(@sidecar_view_modified_time)
60
- @sidecar_view_modified_time = ::File.exist?(template_path) ? ::File.mtime(template_path).to_i.to_s : ""
61
- end
62
-
63
- def rb_component_modified_time
64
- return @rb_component_modified_time if Rails.env.production? && defined?(@rb_component_modified_time)
65
- @rb_component_modified_time = ::File.exist?(component_path) ? ::File.mtime(component_path).to_i.to_s : ""
66
- end
67
-
68
- def template_path
69
- File.join components_base_path, "#{virtual_path}.html.erb"
70
- end
71
-
72
- def component_path
73
- File.join components_base_path, "#{virtual_path}.rb"
74
- end
75
-
76
- def components_base_path
77
- ::Rails.configuration.view_component.view_component_path || "app/components"
78
- end
79
-
80
- private
81
-
82
- def named_cache_key_includes(name, *attrs)
83
- define_cache_key_method unless @named_cache_key_attributes
84
- @named_cache_key_attributes ||= {}
85
- @named_cache_key_attributes[name] = attrs
86
- end
87
-
88
- def define_cache_key_method
89
- # If the presenter defines cache key setup then define the method. Otherwise Rails assumes this
90
- # will return a valid key if the class will respond to this
91
- define_method :cache_key do |n = :_collection|
92
- if defined?(@cache_key)
93
- return @cache_key[n] if @cache_key.key?(n)
94
- else
95
- @cache_key ||= {}
96
- end
97
- generate_cache_key(n)
98
- @cache_key[n]
99
- end
100
- end
101
- end
102
-
103
- # Component modified time which is combined with other cache key attributes to generate cache key for an instance
104
- def component_modified_time
105
- self.class.component_modified_time
106
- end
107
-
108
- def cacheable?
109
- respond_to? :cache_key
110
- end
111
-
112
- def cache_key_modifier
113
- ENV["RAILS_CACHE_ID"]
114
- end
115
-
116
- def cache_keys_for_sources(key_attributes)
117
- sources = key_attributes.flat_map { |n| n.is_a?(Proc) ? instance_eval(&n) : send(n) }
118
- sources.compact.map do |item|
119
- next if item == self
120
- generate_item_cache_key_from(item)
121
- end
122
- end
123
-
124
- def generate_item_cache_key_from(item)
125
- if item.respond_to? :cache_key_with_version
126
- item.cache_key_with_version
127
- elsif item.respond_to? :cache_key
128
- item.cache_key
129
- elsif item.is_a?(String)
130
- Digest::SHA1.hexdigest(item)
131
- else
132
- Digest::SHA1.hexdigest(Marshal.dump(item))
133
- end
134
- end
135
-
136
- def generate_cache_key(index)
137
- key_attributes = self.class.named_cache_key_attributes[index]
138
- return nil unless key_attributes
139
- key = "#{self.class.name}/#{cache_keys_for_sources(key_attributes).join("/")}"
140
- raise StandardError, "Cache key for key #{key} is blank!" if key.blank?
141
- @cache_key[index] = cache_key_modifier.present? ? "#{key}/#{cache_key_modifier}" : key
142
- end
143
- end
144
- end
145
- end
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- # Include rake tasks
5
- class Railtie < ::Rails::Railtie
6
- rake_tasks do
7
- load "tasks/vident.rake"
8
- end
9
- end
10
- end
@@ -1,237 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- module RootComponent
5
- module Base
6
- def initialize(
7
- controllers: nil,
8
- actions: nil,
9
- targets: nil,
10
- named_classes: nil, # https://stimulus.hotwired.dev/reference/css-classes
11
- data_maps: nil,
12
- element_tag: nil,
13
- id: nil,
14
- html_options: nil
15
- )
16
- @element_tag = element_tag
17
- @html_options = html_options
18
- @id = id
19
- @controllers = Array.wrap(controllers)
20
- @actions = actions
21
- @targets = targets
22
- @named_classes = named_classes
23
- @data_map_kvs = {}
24
- @data_maps = data_maps
25
- end
26
-
27
- # The view component's helpers for setting stimulus data-* attributes on this component.
28
-
29
- # TODO: rename
30
- # Create a Stimulus action string, and returns it
31
- # examples:
32
- # action(:my_thing) => "current_controller#myThing"
33
- # action(:click, :my_thing) => "click->current_controller#myThing"
34
- # action("click->current_controller#myThing") => "click->current_controller#myThing"
35
- # action("path/to/current", :my_thing) => "path--to--current_controller#myThing"
36
- # action(:click, "path/to/current", :my_thing) => "click->path--to--current_controller#myThing"
37
- def action(*args)
38
- part1, part2, part3 = args
39
- (args.size == 1) ? parse_action_arg(part1) : parse_multiple_action_args(part1, part2, part3)
40
- end
41
-
42
- def action_data_attribute(*actions)
43
- {action: parse_actions(actions).join(" ")}
44
- end
45
-
46
- # TODO: rename & make stimulus Target class instance and returns it, which can convert to String
47
- # Create a Stimulus Target and returns it
48
- # examples:
49
- # target(:my_target) => {controller: 'current_controller' name: 'myTarget'}
50
- # target("path/to/current", :my_target) => {controller: 'path--to--current_controller', name: 'myTarget'}
51
- def target(name, part2 = nil)
52
- if part2.nil?
53
- {controller: implied_controller_name, name: js_name(name)}
54
- else
55
- {controller: stimulize_path(name), name: js_name(part2)}
56
- end
57
- end
58
-
59
- def target_data_attribute(name)
60
- build_target_data_attributes([target(name)])
61
- end
62
-
63
- # Getter for a named classes list so can be used in view to set initial state on SSR
64
- # Returns a String of classes that can be used in a `class` attribute.
65
- def named_classes(*names)
66
- names.map { |name| convert_classes_list_to_string(@named_classes[name]) }.join(" ")
67
- end
68
-
69
- # Helpers for generating the Stimulus data-* attributes directly
70
-
71
- # Return the HTML `data-controller` attribute for the given controllers
72
- def with_controllers(*controllers_to_set)
73
- "data-controller=\"#{controller_list(controllers_to_set)}\"".html_safe
74
- end
75
-
76
- # Return the HTML `data-target` attribute for the given targets
77
- def as_targets(*targets)
78
- attrs = build_target_data_attributes(parse_targets(targets))
79
- attrs.map { |dt, n| "data-#{dt}=\"#{n}\"" }.join(" ").html_safe
80
- end
81
- alias_method :as_target, :as_targets
82
-
83
- # Return the HTML `data-action` attribute for the given actions
84
- def with_actions(*actions_to_set)
85
- "data-action='#{parse_actions(actions_to_set).join(" ")}'".html_safe
86
- end
87
- alias_method :with_action, :with_actions
88
-
89
- private
90
-
91
- # An implicit Stimulus controller name is built from the implicit controller path
92
- def implied_controller_name
93
- stimulize_path(implied_controller_path)
94
- end
95
-
96
- # When using the DSL if you dont specify, the first controller is implied
97
- def implied_controller_path
98
- @controllers&.first || raise(StandardError, "No controllers have been specified")
99
- end
100
-
101
- # A complete list of Stimulus controllers for this component
102
- def controller_list(controllers_to_set)
103
- controllers_to_set&.map { |c| stimulize_path(c) }&.join(" ")
104
- end
105
-
106
- # Complete list of actions ready to be use in the data-action attribute
107
- def action_list(actions_to_parse)
108
- return nil unless actions_to_parse&.size&.positive?
109
- parse_actions(actions_to_parse).join(" ")
110
- end
111
-
112
- # Complete list of targets ready to be use in the data attributes
113
- def target_list
114
- return {} unless @targets&.size&.positive?
115
- build_target_data_attributes(parse_targets(@targets))
116
- end
117
-
118
- def named_classes_list
119
- return {} unless @named_classes&.size&.positive?
120
- build_named_classes_data_attributes(@named_classes)
121
- end
122
-
123
- # stimulus "data-*" attributes map for this component
124
- def tag_data_attributes
125
- {controller: controller_list(@controllers), action: action_list(@actions)}
126
- .merge!(target_list)
127
- .merge!(named_classes_list)
128
- .merge!(data_map_attributes)
129
- .compact_blank!
130
- end
131
-
132
- # Actions can be specified as a symbol, in which case they imply an action on the primary
133
- # controller, or as a string in which case it implies an action that is already fully qualified
134
- # stimulus action.
135
- # 1 Symbol: :my_action => "my_controller#myAction"
136
- # 1 String: "my_controller#myAction"
137
- # 2 Symbols: [:click, :my_action] => "click->my_controller#myAction"
138
- # 1 String, 1 Symbol: ["path/to/controller", :my_action] => "path--to--controller#myAction"
139
- # 1 Symbol, 1 String, 1 Symbol: [:hover, "path/to/controller", :my_action] => "hover->path--to--controller#myAction"
140
-
141
- def parse_action_arg(part1)
142
- if part1.is_a?(Symbol)
143
- # 1 symbol arg, name of method on this controller
144
- "#{implied_controller_name}##{js_name(part1)}"
145
- elsif part1.is_a?(String)
146
- # 1 string arg, fully qualified action
147
- part1
148
- end
149
- end
150
-
151
- def parse_multiple_action_args(part1, part2, part3)
152
- if part3.nil? && part1.is_a?(Symbol)
153
- # 2 symbol args = event + action
154
- "#{part1}->#{implied_controller_name}##{js_name(part2)}"
155
- elsif part3.nil?
156
- # 1 string arg, 1 symbol = controller + action
157
- "#{stimulize_path(part1)}##{js_name(part2)}"
158
- else
159
- # 1 symbol, 1 string, 1 symbol = as above but with event
160
- "#{part1}->#{stimulize_path(part2)}##{js_name(part3)}"
161
- end
162
- end
163
-
164
- # Parse actions, targets and attributes that are passed in as symbols or strings
165
-
166
- def parse_targets(targets)
167
- targets.map { |n| parse_target(n) }
168
- end
169
-
170
- def parse_target(raw_target)
171
- return raw_target if raw_target.is_a?(String)
172
- return raw_target if raw_target.is_a?(Hash)
173
- target(raw_target)
174
- end
175
-
176
- def build_target_data_attributes(targets)
177
- targets.map { |t| ["#{t[:controller]}-target".to_sym, t[:name]] }.to_h
178
- end
179
-
180
- def parse_actions(actions)
181
- actions.map! { |a| a.is_a?(String) ? a : action(*a) }
182
- end
183
-
184
- def parse_attributes(attrs, controller = nil)
185
- attrs.transform_keys { |k| "#{controller || implied_controller_name}-#{k}" }
186
- end
187
-
188
- def data_map_attributes
189
- return {} unless @data_maps
190
- @data_maps.each_with_object({}) do |m, obj|
191
- if m.is_a?(Hash)
192
- obj.merge!(parse_attributes(m))
193
- elsif m.is_a?(Array)
194
- controller_path = m.first
195
- data = m.last
196
- obj.merge!(parse_attributes(data, stimulize_path(controller_path)))
197
- end
198
- end
199
- end
200
-
201
- def parse_named_classes_hash(named_classes)
202
- named_classes.map do |name, classes|
203
- logical_name = name.to_s.dasherize
204
- classes_str = convert_classes_list_to_string(classes)
205
- if classes.is_a?(Hash)
206
- {controller: stimulize_path(classes[:controller_path]), name: logical_name, classes: classes_str}
207
- else
208
- {controller: implied_controller_name, name: logical_name, classes: classes_str}
209
- end
210
- end
211
- end
212
-
213
- def build_named_classes_data_attributes(named_classes)
214
- parse_named_classes_hash(named_classes)
215
- .map { |c| ["#{c[:controller]}-#{c[:name]}-class", c[:classes]] }
216
- .to_h
217
- end
218
-
219
- def convert_classes_list_to_string(classes)
220
- return "" if classes.nil?
221
- return classes if classes.is_a?(String)
222
- return classes.join(" ") if classes.is_a?(Array)
223
- classes[:classes].is_a?(Array) ? classes[:classes].join(" ") : classes[:classes]
224
- end
225
-
226
- # Convert a file path to a stimulus controller name
227
- def stimulize_path(path)
228
- path.split("/").map { |p| p.to_s.dasherize }.join("--")
229
- end
230
-
231
- # Convert a Ruby 'snake case' string to a JavaScript camel case strings
232
- def js_name(name)
233
- name.to_s.camelize(:lower)
234
- end
235
- end
236
- end
237
- end