vident 0.6.3 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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