classy-yaml 1.7.2 → 1.7.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 +4 -4
- data/README.md +7 -3
- data/lib/classy/yaml/component_helpers.rb +3 -10
- data/lib/classy/yaml/engine.rb +13 -0
- data/lib/classy/yaml/helpers.rb +2 -139
- data/lib/classy/yaml/lookup.rb +181 -0
- data/lib/classy/yaml/result_cache.rb +68 -0
- data/lib/classy/yaml/version.rb +1 -1
- data/lib/classy/yaml.rb +261 -78
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 177a550a2cc9c2678b05d5ccfc29aeb690949e1e99f604bc78f4a728fac96fc9
|
|
4
|
+
data.tar.gz: 1afe8baf1045c0bc23e60916a0f4dba19cd8d39f0562ed60bd58e3a914613238
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec10f751c02f2e49cd8aa176a0b0361a82c0a7feb0cc7a68518cadf94c6008b39efd5b1c3b210c699c01cd4f6517d3af3e97231f3d115d98313fe7ba1ce20eea
|
|
7
|
+
data.tar.gz: 6b9e997a94be56439b79765aef5117b54cac23e40681634bc56666413ca68a29debdce78cab0ccf83a0b77f4d7c8c3260dbdb99419fb0b0fe5dbdd02d87ae2e2
|
data/README.md
CHANGED
|
@@ -144,11 +144,14 @@ Classy YAML detects if [tailwind_merge](https://github.com/gjtorikian/tailwind_m
|
|
|
144
144
|
|
|
145
145
|
## Caching and development reloads
|
|
146
146
|
|
|
147
|
-
Classy YAML
|
|
147
|
+
Classy YAML needs no configuration for its caches. The railtie chooses their behavior from `Rails.application.config.cache_classes`.
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
- **Results.** `yass` remembers the result for each set of arguments (including `add:`, `skip_base:` and `classy_files:`) in a bounded least-recently-used cache of `Classy::Yaml.cache_size` entries (5,000 by default). A repeated call does no YAML lookup and no class merge. A call that logs an invalid key warning is not cached, so the warning appears on every call. Set `Classy::Yaml.cache_size = 0` to turn the result cache off.
|
|
150
|
+
- **YAML files.** Each file is parsed once and compiled to frozen lookup data. A missing or invalid file is also remembered.
|
|
151
|
+
- **Classes cached (test, production).** Classy YAML never checks the files again. Edits take effect after a restart or `Classy::Yaml.setup`. At boot, Classy YAML parses the default, engine, and extra files and builds the `tailwind_merge` merger, so forked workers (Puma `preload_app!`, parallel tests) share them.
|
|
152
|
+
- **Classes reloaded (development).** At the start of each request or job (the Rails executor), Classy YAML marks its files for a check. The next `yass` call checks each YAML file it has read once, including missing component files. An edit, addition, deletion, or replacement takes effect on the next request. Outside the executor, for example in a console, call `reload!`.
|
|
150
153
|
|
|
151
|
-
When `tailwind_merge` is available, Classy YAML reuses one merger per process in all environments. A lock protects the merger cache during concurrent calls.
|
|
154
|
+
`Classy::Yaml.setup` and the file setters clear all caches. When `tailwind_merge` is available, Classy YAML reuses one merger per process in all environments. A lock protects the merger cache during concurrent calls.
|
|
152
155
|
|
|
153
156
|
## Configuration Options
|
|
154
157
|
|
|
@@ -158,6 +161,7 @@ When `tailwind_merge` is available, Classy YAML reuses one merger per process in
|
|
|
158
161
|
| `extra_files` | `[]` | Array of additional YAML files (above default and engine files; below component files) |
|
|
159
162
|
| `engine_files` | `[]` | Array of engine YAML files (lowest priority) |
|
|
160
163
|
| `override_tag_helpers` | `false` | Automatically process class symbols/hashes in Rails tag helpers |
|
|
164
|
+
| `cache_size` | `5000` | Maximum number of cached `yass` results; `0` turns the result cache off |
|
|
161
165
|
|
|
162
166
|
## Contributing
|
|
163
167
|
|
|
@@ -2,19 +2,12 @@ module Classy
|
|
|
2
2
|
module Yaml
|
|
3
3
|
module ComponentHelpers
|
|
4
4
|
def yass(*args)
|
|
5
|
-
|
|
6
|
-
calling_path = File.dirname(source_file)
|
|
7
|
-
calling_file = File.basename(source_file).split(".").first
|
|
8
|
-
component_name = self.class.name.underscore.split("/").last.split(".").first
|
|
9
|
-
|
|
10
|
-
classy_files = [ "#{calling_path}/#{component_name}.yml",
|
|
11
|
-
"#{calling_path}/#{calling_file}/#{calling_file}.yml",
|
|
12
|
-
"#{calling_path}/#{calling_file}/#{component_name}.yml" ]
|
|
5
|
+
classy_files = Classy::Yaml.component_classy_files(self.class)
|
|
13
6
|
|
|
14
7
|
if args.first.is_a?(Hash)
|
|
15
|
-
args[0] = args.first.merge(classy_files: classy_files
|
|
8
|
+
args[0] = args.first.merge(classy_files: classy_files)
|
|
16
9
|
else
|
|
17
|
-
args << { classy_files: classy_files
|
|
10
|
+
args << { classy_files: classy_files }
|
|
18
11
|
end
|
|
19
12
|
|
|
20
13
|
helpers.yass(*args)
|
data/lib/classy/yaml/engine.rb
CHANGED
|
@@ -3,9 +3,22 @@ require "rails"
|
|
|
3
3
|
module Classy
|
|
4
4
|
module Yaml
|
|
5
5
|
class Engine < Rails::Railtie
|
|
6
|
+
# Each request or job starts with one check of the YAML files while Rails
|
|
7
|
+
# reloads code, instead of a check on every lookup.
|
|
8
|
+
initializer "classy_yaml.file_check" do |app|
|
|
9
|
+
app.executor.to_run { Classy::Yaml.files_may_have_changed }
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
config.to_prepare do
|
|
13
|
+
Classy::Yaml.clear_component_files
|
|
14
|
+
Classy::Yaml.files_may_have_changed
|
|
7
15
|
ApplicationController.helper(Classy::Yaml::Helpers)
|
|
8
16
|
end
|
|
17
|
+
|
|
18
|
+
# With classes cached, parse the YAML before workers fork.
|
|
19
|
+
config.after_initialize do |app|
|
|
20
|
+
Classy::Yaml.warm if app.config.cache_classes
|
|
21
|
+
end
|
|
9
22
|
end
|
|
10
23
|
end
|
|
11
24
|
end
|
data/lib/classy/yaml/helpers.rb
CHANGED
|
@@ -11,154 +11,17 @@ module Classy
|
|
|
11
11
|
# @param args [Array] Array of keys to look up in the YAML files
|
|
12
12
|
# @return [String] Space-separated list of CSS classes
|
|
13
13
|
def yass(*args)
|
|
14
|
-
|
|
15
|
-
classy_yamls = Classy::Yaml.cached_engine_yamls.dup
|
|
16
|
-
|
|
17
|
-
# Add default YAML (next priority)
|
|
18
|
-
default_yaml = Classy::Yaml.cached_default_yaml
|
|
19
|
-
classy_yamls << default_yaml if default_yaml
|
|
20
|
-
|
|
21
|
-
# Add extra files (highest priority)
|
|
22
|
-
Classy::Yaml.extra_files.each do |file_path|
|
|
23
|
-
load_yaml_file(file_path, classy_yamls, "extra")
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Add classy_files (highest priority)
|
|
27
|
-
classy_files_hash = args.find { |arg| arg.is_a?(Hash) && arg.key?(:classy_files) } || { classy_files: [] }
|
|
28
|
-
classy_files_hash[:classy_files].each do |file_path|
|
|
29
|
-
load_yaml_file(file_path, classy_yamls, "classy")
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
skip_base_hash = args.find { |arg| arg.is_a?(Hash) && arg.key?(:skip_base) } || {}
|
|
33
|
-
keys, add_classes = flatten_args(values: args)
|
|
34
|
-
classes = fetch_classes(keys.uniq, classy_yamls: classy_yamls, skip_base: skip_base_hash[:skip_base])
|
|
35
|
-
classes += add_classes
|
|
36
|
-
|
|
37
|
-
# Use tailwind_merge if available, otherwise fall back to simple join
|
|
38
|
-
merge_classes(classes.flatten.uniq)
|
|
14
|
+
Classy::Yaml.classes_for(args, tailwind_merge_available?)
|
|
39
15
|
end
|
|
40
16
|
|
|
41
17
|
private
|
|
42
18
|
|
|
43
|
-
# Merges CSS classes using tailwind_merge if available, otherwise uses simple join
|
|
44
|
-
# @param classes [Array] Array of CSS class strings
|
|
45
|
-
# @return [String] Merged CSS classes
|
|
46
|
-
def merge_classes(classes)
|
|
47
|
-
return "" if classes.blank?
|
|
48
|
-
|
|
49
|
-
if tailwind_merge_available?
|
|
50
|
-
# Use tailwind_merge for intelligent class merging
|
|
51
|
-
Classy::Yaml.merge_classes(classes)
|
|
52
|
-
else
|
|
53
|
-
# Fall back to simple space-joined classes
|
|
54
|
-
classes.join(" ")
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
19
|
# Checks if tailwind_merge gem is available
|
|
59
20
|
# @return [Boolean] True if tailwind_merge is available
|
|
60
21
|
def tailwind_merge_available?
|
|
61
22
|
return @tailwind_merge_available if defined?(@tailwind_merge_available)
|
|
62
|
-
@tailwind_merge_available = begin
|
|
63
|
-
require "tailwind_merge"
|
|
64
|
-
true
|
|
65
|
-
rescue LoadError
|
|
66
|
-
false
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Loads a YAML file and adds its contents to the classy_yamls array
|
|
71
|
-
# @param file_path [String, Pathname] Path to the YAML file
|
|
72
|
-
# @param classy_yamls [Array] Array to add the parsed YAML to
|
|
73
|
-
# @param file_type [String] Type of file being loaded (for error messages)
|
|
74
|
-
def load_yaml_file(file_path, classy_yamls, file_type)
|
|
75
|
-
parsed_yaml = Classy::Yaml.cached_yaml_file(file_path, file_type)
|
|
76
|
-
classy_yamls << parsed_yaml if parsed_yaml
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Flattens the arguments into keys and classes
|
|
80
|
-
# @param root [Array] Current root path in the argument tree
|
|
81
|
-
# @param values [Array] Values to process
|
|
82
|
-
# @param keys [Array] Array to store found keys
|
|
83
|
-
# @param added_classes [Array] Array to store found classes
|
|
84
|
-
# @return [Array] Tuple of [keys, added_classes]
|
|
85
|
-
def flatten_args(root: [], values: [], keys: [], added_classes: [])
|
|
86
|
-
parent_keys = []
|
|
87
|
-
values.each do |value|
|
|
88
|
-
case value
|
|
89
|
-
when Hash
|
|
90
|
-
added_classes << value[:add] if value.key?(:add)
|
|
91
|
-
value.each do |key, child|
|
|
92
|
-
next if key == :add
|
|
93
|
-
next if root.empty? && [ :skip_base, :classy_files ].include?(key)
|
|
94
|
-
|
|
95
|
-
path = root + [ key.to_s ]
|
|
96
|
-
parent_keys << path
|
|
97
|
-
flatten_args(root: path, values: [ child ], keys: keys, added_classes: added_classes)
|
|
98
|
-
end
|
|
99
|
-
when Array
|
|
100
|
-
flatten_args(root: root, values: value, keys: keys, added_classes: added_classes)
|
|
101
|
-
else
|
|
102
|
-
keys << (root + [ value.to_s ])
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
keys.concat(parent_keys)
|
|
106
|
-
[ keys, added_classes ]
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Fetches classes from the YAML files based on the provided keys
|
|
110
|
-
# @param keys [Array] Array of keys to look up
|
|
111
|
-
# @param classy_yamls [Array] Array of YAML files to search in
|
|
112
|
-
# @param skip_base [Boolean] Whether to skip base classes
|
|
113
|
-
# @return [Array] Array of found classes
|
|
114
|
-
def fetch_classes(keys, classy_yamls: [], skip_base: false)
|
|
115
|
-
classes = []
|
|
116
|
-
|
|
117
|
-
keys.each do |key|
|
|
118
|
-
base_classes = nil
|
|
119
|
-
fetched_classes = nil
|
|
120
|
-
|
|
121
|
-
classy_yamls.reverse_each do |classy_yaml|
|
|
122
|
-
begin
|
|
123
|
-
value = classy_yaml.dig(*key)
|
|
124
|
-
unless skip_base == true || base_classes
|
|
125
|
-
base_value = if value.is_a?(Hash)
|
|
126
|
-
value["base"]
|
|
127
|
-
elsif key.length > 1
|
|
128
|
-
classy_yaml.dig(*(key[0...-1] + [ "base" ]))
|
|
129
|
-
end
|
|
130
|
-
base_classes = normalize_original(base_value).presence
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
unless fetched_classes || value.is_a?(Hash)
|
|
134
|
-
fetched_classes = normalize_original(value).presence
|
|
135
|
-
end
|
|
136
|
-
rescue StandardError
|
|
137
|
-
Rails.logger.warn(Classy::Yaml::InvalidKeyError.new(data: key))
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
break if fetched_classes && (skip_base == true || base_classes)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
classes << base_classes unless base_classes.blank?
|
|
144
|
-
classes << fetched_classes unless fetched_classes.blank?
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
classes.flatten.uniq
|
|
148
|
-
end
|
|
149
23
|
|
|
150
|
-
|
|
151
|
-
# @param value [String, Array, nil] Value to normalize
|
|
152
|
-
# @return [Array, nil] Array of classes or nil if value is invalid
|
|
153
|
-
def normalize_original(value)
|
|
154
|
-
case value
|
|
155
|
-
when String
|
|
156
|
-
value.split(" ").reject(&:blank?)
|
|
157
|
-
when Array
|
|
158
|
-
value.flatten.map(&:to_s).reject(&:blank?)
|
|
159
|
-
else
|
|
160
|
-
nil
|
|
161
|
-
end
|
|
24
|
+
Classy::Yaml.tailwind_merge_available?
|
|
162
25
|
end
|
|
163
26
|
end
|
|
164
27
|
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
module Classy
|
|
2
|
+
module Yaml
|
|
3
|
+
# Turns yass arguments into classes. The YAML layers are compiled once at load
|
|
4
|
+
# (see Classy::Yaml.compile), so a lookup only walks frozen hashes and arrays.
|
|
5
|
+
module Lookup
|
|
6
|
+
EMPTY = [].freeze
|
|
7
|
+
# Returned by walk when a key path goes through a value that is not a hash.
|
|
8
|
+
INVALID = Object.new.freeze
|
|
9
|
+
# A compiled YAML array that refers to itself. 1.7.2 raised on it while
|
|
10
|
+
# normalizing classes, so using it logs a warning and skips the layer.
|
|
11
|
+
RECURSIVE = Object.new.freeze
|
|
12
|
+
# Cache key markers. Hash order changes the output, so a key lists hash
|
|
13
|
+
# entries in order instead of using Hash#eql?, which ignores order.
|
|
14
|
+
HASH_START = Object.new.freeze
|
|
15
|
+
ARRAY_START = Object.new.freeze
|
|
16
|
+
CLOSE = Object.new.freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Returns an order-sensitive cache key for the arguments, or nil when an
|
|
21
|
+
# argument is not a plain value. Only the exact core classes qualify: a
|
|
22
|
+
# subclass can override #to_s, #each or #key? and change the result.
|
|
23
|
+
def cache_key(args, tailwind)
|
|
24
|
+
key = [ tailwind ]
|
|
25
|
+
append_key(args, key) ? key : nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def append_key(value, key)
|
|
29
|
+
case value
|
|
30
|
+
when Symbol, Integer, Float, nil, true, false
|
|
31
|
+
key << value
|
|
32
|
+
when String, Pathname
|
|
33
|
+
return false unless value.instance_of?(String) || value.instance_of?(Pathname)
|
|
34
|
+
|
|
35
|
+
key << value
|
|
36
|
+
when Array
|
|
37
|
+
return false unless value.instance_of?(Array)
|
|
38
|
+
|
|
39
|
+
key << ARRAY_START
|
|
40
|
+
value.each { |child| return false unless append_key(child, key) }
|
|
41
|
+
key << CLOSE
|
|
42
|
+
when Hash
|
|
43
|
+
return false unless value.instance_of?(Hash)
|
|
44
|
+
|
|
45
|
+
key << HASH_START
|
|
46
|
+
value.each { |name, child| return false unless append_key(name, key) && append_key(child, key) }
|
|
47
|
+
key << CLOSE
|
|
48
|
+
else
|
|
49
|
+
return false
|
|
50
|
+
end
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Freezes a cache key built by cache_key. Unfrozen strings are replaced by
|
|
55
|
+
# frozen copies, so a caller that later mutates its strings cannot change it.
|
|
56
|
+
def freeze_key(key)
|
|
57
|
+
key.each_with_index do |part, index|
|
|
58
|
+
key[index] = part.dup.freeze if part.is_a?(String) && !part.frozen?
|
|
59
|
+
end
|
|
60
|
+
key.freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Collects the lookup key paths and add: values in the order 1.7.2 used:
|
|
64
|
+
# leaf paths first, then the parent paths of each list of values.
|
|
65
|
+
def collect_list(values, root, keys, adds)
|
|
66
|
+
parents = nil
|
|
67
|
+
values.each { |value| parents = collect(value, root, keys, adds, parents) }
|
|
68
|
+
keys.concat(parents) if parents
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def collect(value, root, keys, adds, parents)
|
|
72
|
+
case value
|
|
73
|
+
when Hash
|
|
74
|
+
adds << value[:add] if value.key?(:add)
|
|
75
|
+
value.each do |name, child|
|
|
76
|
+
next if name == :add
|
|
77
|
+
next if root.empty? && (name == :skip_base || name == :classy_files)
|
|
78
|
+
|
|
79
|
+
path = [ *root, segment(name) ]
|
|
80
|
+
(parents ||= []) << path
|
|
81
|
+
nested = collect(child, path, keys, adds, nil)
|
|
82
|
+
keys.concat(nested) if nested
|
|
83
|
+
end
|
|
84
|
+
when Array
|
|
85
|
+
collect_list(value, root, keys, adds)
|
|
86
|
+
else
|
|
87
|
+
keys << [ *root, segment(value) ]
|
|
88
|
+
end
|
|
89
|
+
parents
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def segment(value)
|
|
93
|
+
value.is_a?(Symbol) ? value.name : value.to_s
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Resolves each key path against the layers (lowest priority first) and
|
|
97
|
+
# appends base and specific classes. Returns false when a path went through
|
|
98
|
+
# a value that is not a hash; that call logged a warning.
|
|
99
|
+
def resolve(keys, layers, skip_base, classes)
|
|
100
|
+
clean = true
|
|
101
|
+
skip_base = skip_base == true
|
|
102
|
+
keys.each do |path|
|
|
103
|
+
clean = false unless resolve_path(path, layers, skip_base, classes)
|
|
104
|
+
end
|
|
105
|
+
clean
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def resolve_path(path, layers, skip_base, classes)
|
|
109
|
+
clean = true
|
|
110
|
+
base_classes = nil
|
|
111
|
+
fetched_classes = nil
|
|
112
|
+
last = path.size - 1
|
|
113
|
+
index = layers.size - 1
|
|
114
|
+
while index >= 0
|
|
115
|
+
layer = layers[index]
|
|
116
|
+
index -= 1
|
|
117
|
+
parent = walk(layer, path, last)
|
|
118
|
+
value = case parent
|
|
119
|
+
when Hash then parent[path[last]]
|
|
120
|
+
when nil then nil
|
|
121
|
+
else INVALID
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if value.equal?(INVALID)
|
|
125
|
+
clean = false
|
|
126
|
+
Rails.logger.warn(Classy::Yaml::InvalidKeyError.new(data: path))
|
|
127
|
+
else
|
|
128
|
+
# 1.7.2 computed the base, then the classes, and a recursive array
|
|
129
|
+
# raised in either step, which skipped the rest of this layer.
|
|
130
|
+
unless skip_base || base_classes
|
|
131
|
+
base_value = if value.is_a?(Hash)
|
|
132
|
+
value["base"]
|
|
133
|
+
elsif last.positive? && parent.is_a?(Hash)
|
|
134
|
+
parent["base"]
|
|
135
|
+
end
|
|
136
|
+
base_classes = classes_of(base_value)
|
|
137
|
+
end
|
|
138
|
+
unless fetched_classes || value.is_a?(Hash) || base_classes.equal?(RECURSIVE)
|
|
139
|
+
fetched_classes = classes_of(value)
|
|
140
|
+
end
|
|
141
|
+
if base_classes.equal?(RECURSIVE) || fetched_classes.equal?(RECURSIVE)
|
|
142
|
+
base_classes = nil if base_classes.equal?(RECURSIVE)
|
|
143
|
+
fetched_classes = nil if fetched_classes.equal?(RECURSIVE)
|
|
144
|
+
clean = false
|
|
145
|
+
Rails.logger.warn(Classy::Yaml::InvalidKeyError.new(data: path))
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
break if fetched_classes && (skip_base || base_classes)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
classes.concat(base_classes) if base_classes
|
|
153
|
+
classes.concat(fetched_classes) if fetched_classes
|
|
154
|
+
clean
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Follows path[0...depth] from node. Returns nil when a key is missing, and
|
|
158
|
+
# INVALID when the path continues through a value that is not a hash.
|
|
159
|
+
def walk(node, path, depth)
|
|
160
|
+
index = 0
|
|
161
|
+
while index < depth
|
|
162
|
+
case node
|
|
163
|
+
when Hash then node = node[path[index]]
|
|
164
|
+
when nil then return nil
|
|
165
|
+
else return INVALID
|
|
166
|
+
end
|
|
167
|
+
index += 1
|
|
168
|
+
end
|
|
169
|
+
node
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# A compiled leaf is a frozen array of classes; anything else has none.
|
|
173
|
+
# A recursive array is returned as RECURSIVE for the caller to report.
|
|
174
|
+
def classes_of(value)
|
|
175
|
+
return value if value.equal?(RECURSIVE)
|
|
176
|
+
|
|
177
|
+
value if value.is_a?(Array) && !value.empty?
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Classy
|
|
2
|
+
module Yaml
|
|
3
|
+
# A bounded, thread-safe least-recently-used cache of yass results.
|
|
4
|
+
#
|
|
5
|
+
# Entries are indexed by the key's hash, computed once per call, and keep the
|
|
6
|
+
# key itself so a lookup only hits when the keys are eql?. A store carries the
|
|
7
|
+
# generation its result was computed under; a store from an older generation
|
|
8
|
+
# is dropped, so a lookup that raced a reload or a `setup` never caches an
|
|
9
|
+
# answer built from the old YAML.
|
|
10
|
+
class ResultCache
|
|
11
|
+
Entry = Struct.new(:key, :value)
|
|
12
|
+
|
|
13
|
+
attr_reader :max_size
|
|
14
|
+
|
|
15
|
+
def initialize(max_size)
|
|
16
|
+
@max_size = max_size
|
|
17
|
+
@entries = {}
|
|
18
|
+
@generation = 0
|
|
19
|
+
@lock = Mutex.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generation
|
|
23
|
+
@generation
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def max_size=(value)
|
|
27
|
+
@lock.synchronize do
|
|
28
|
+
@max_size = value
|
|
29
|
+
@entries.shift while @entries.size > @max_size
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def size
|
|
34
|
+
@entries.size
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get(key, digest = key.hash)
|
|
38
|
+
@lock.synchronize do
|
|
39
|
+
entry = @entries.delete(digest)
|
|
40
|
+
return unless entry
|
|
41
|
+
|
|
42
|
+
# Move the entry to the newest end; a colliding key is a miss and its
|
|
43
|
+
# entry is replaced by the next store.
|
|
44
|
+
@entries[digest] = entry
|
|
45
|
+
entry.value if entry.key.eql?(key)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def store(key, value, generation, digest = key.hash)
|
|
50
|
+
@lock.synchronize do
|
|
51
|
+
return unless generation == @generation && @max_size.positive?
|
|
52
|
+
|
|
53
|
+
@entries.delete(digest)
|
|
54
|
+
@entries[digest] = Entry.new(key, value).freeze
|
|
55
|
+
@entries.shift if @entries.size > @max_size
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Drops every result and invalidates stores already in progress.
|
|
60
|
+
def invalidate
|
|
61
|
+
@lock.synchronize do
|
|
62
|
+
@generation += 1
|
|
63
|
+
@entries.clear
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/classy/yaml/version.rb
CHANGED
data/lib/classy/yaml.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "concurrent/map"
|
|
1
2
|
require "classy/yaml/version"
|
|
2
3
|
require "classy/yaml/engine"
|
|
3
4
|
|
|
@@ -20,29 +21,42 @@ module Classy
|
|
|
20
21
|
autoload :Helpers, "classy/yaml/helpers"
|
|
21
22
|
autoload :ComponentHelpers, "classy/yaml/component_helpers"
|
|
22
23
|
autoload :InvalidKeyError, "classy/yaml/invalid_key_error"
|
|
24
|
+
autoload :Lookup, "classy/yaml/lookup"
|
|
25
|
+
autoload :ResultCache, "classy/yaml/result_cache"
|
|
23
26
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
# The number of yass results kept for reuse. Set to 0 to turn the result cache off.
|
|
28
|
+
DEFAULT_CACHE_SIZE = 5_000
|
|
29
|
+
|
|
30
|
+
FileEntry = Struct.new(:signature, :layer)
|
|
31
|
+
|
|
32
|
+
# -- Caches --
|
|
33
|
+
# Parsed and compiled YAML files, keyed by full path. While Rails reloads code,
|
|
34
|
+
# each entry keeps the file signature that check_for_changes compares.
|
|
28
35
|
@file_cache = {}
|
|
36
|
+
# Configured and component paths resolved against Rails.root.
|
|
37
|
+
@full_paths = {}
|
|
29
38
|
@file_lock = Mutex.new
|
|
39
|
+
# The engine, default and extra layers, compiled for the current generation.
|
|
40
|
+
@static_layers = nil
|
|
41
|
+
@results = ResultCache.new(DEFAULT_CACHE_SIZE)
|
|
42
|
+
@component_files = Concurrent::Map.new
|
|
43
|
+
@check_pending = false
|
|
30
44
|
@merger_lock = Mutex.new
|
|
31
45
|
|
|
32
46
|
# -- Configuration Setters with Path Resolution --
|
|
33
47
|
def self.engine_files=(value)
|
|
34
48
|
@@engine_files = Array.wrap(value).reject(&:blank?).map { |file| Rails.root.join(file) }
|
|
35
|
-
|
|
49
|
+
reset_caches
|
|
36
50
|
end
|
|
37
51
|
|
|
38
52
|
def self.extra_files=(value)
|
|
39
53
|
@@extra_files = Array.wrap(value).reject(&:blank?).map { |file| Rails.root.join(file) }
|
|
40
|
-
|
|
54
|
+
reset_caches
|
|
41
55
|
end
|
|
42
56
|
|
|
43
57
|
def self.default_file=(value)
|
|
44
58
|
@@default_file = value
|
|
45
|
-
|
|
59
|
+
reset_caches
|
|
46
60
|
end
|
|
47
61
|
|
|
48
62
|
def self.override_tag_helpers=(value)
|
|
@@ -50,109 +64,167 @@ module Classy
|
|
|
50
64
|
apply_tag_helper_override if value
|
|
51
65
|
end
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# Check file metadata only while Rails reloads code
|
|
56
|
-
return load_engine_yamls if reloading?
|
|
57
|
-
|
|
58
|
-
# Classes are cached: parse once
|
|
59
|
-
return @cached_engine_yamls if @cached_engine_yamls
|
|
60
|
-
|
|
61
|
-
@load_lock.synchronize do
|
|
62
|
-
# Double-check idiom to ensure loading happens only once
|
|
63
|
-
return @cached_engine_yamls if @cached_engine_yamls
|
|
64
|
-
@cached_engine_yamls = load_engine_yamls
|
|
65
|
-
end
|
|
67
|
+
def self.cache_size
|
|
68
|
+
@results.max_size
|
|
66
69
|
end
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
# Accepts an Integer, or a String holding one (for example from ENV), of 0 or more.
|
|
72
|
+
# Integer() would truncate a Float such as -0.5 to 0, so floats are rejected.
|
|
73
|
+
def self.cache_size=(value)
|
|
74
|
+
size = case value
|
|
75
|
+
when Integer then value
|
|
76
|
+
when String then Integer(value, 10, exception: false)
|
|
77
|
+
end
|
|
78
|
+
unless size && !size.negative?
|
|
79
|
+
raise ArgumentError, "Classy::Yaml.cache_size must be an integer of 0 or more, got #{value.inspect}"
|
|
80
|
+
end
|
|
71
81
|
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
@results.max_size = size
|
|
83
|
+
end
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
end
|
|
85
|
+
# Increases on every configuration change and every YAML file change.
|
|
86
|
+
def self.generation
|
|
87
|
+
@results.generation
|
|
79
88
|
end
|
|
80
89
|
|
|
81
90
|
# -- Setup Method --
|
|
82
91
|
def self.setup
|
|
83
92
|
yield self
|
|
84
93
|
# Clear all caches when configuration changes
|
|
85
|
-
|
|
86
|
-
@cached_default_yaml = nil
|
|
87
|
-
@file_lock.synchronize { @file_cache.clear }
|
|
94
|
+
reset_caches(files: true)
|
|
88
95
|
# Apply tag helper override if enabled
|
|
89
96
|
apply_tag_helper_override if @@override_tag_helpers
|
|
90
97
|
end
|
|
91
98
|
|
|
92
|
-
|
|
99
|
+
# Returns the classes for yass arguments. Results are reused until a
|
|
100
|
+
# configuration change or (while Rails reloads code) a YAML file change.
|
|
101
|
+
def self.classes_for(args, tailwind)
|
|
102
|
+
check_for_changes if @check_pending
|
|
103
|
+
key = Lookup.cache_key(args, tailwind) if @results.max_size.positive?
|
|
104
|
+
if key
|
|
105
|
+
digest = key.hash
|
|
106
|
+
cached = @results.get(key, digest)
|
|
107
|
+
return cached.is_a?(String) ? cached : cached.first.dup if cached
|
|
108
|
+
end
|
|
93
109
|
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
generation = @results.generation
|
|
111
|
+
result, cacheable = build_classes(args, tailwind)
|
|
112
|
+
if key && cacheable
|
|
113
|
+
# A frozen result is shared as 1.7.2 shared tailwind_merge's cached string;
|
|
114
|
+
# any other result is returned as a new unfrozen copy each time.
|
|
115
|
+
value = result.frozen? ? result : [ result.dup.freeze ].freeze
|
|
116
|
+
@results.store(Lookup.freeze_key(key), value, generation, digest)
|
|
117
|
+
end
|
|
118
|
+
result
|
|
119
|
+
end
|
|
96
120
|
|
|
97
|
-
|
|
98
|
-
|
|
121
|
+
# Parses the YAML files and builds the merger before Puma or the test runner
|
|
122
|
+
# forks, so workers share them copy-on-write and the first request pays nothing.
|
|
123
|
+
def self.warm
|
|
124
|
+
static_layers
|
|
125
|
+
@merger_lock.synchronize { @merger ||= TailwindMerge::Merger.new } if tailwind_merge_available?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Called at the start of each request or job (the Rails executor) and after
|
|
129
|
+
# each code reload (to_prepare, which also covers `reload!` in a console).
|
|
130
|
+
# While Rails reloads code, the next lookup compares each YAML file it has
|
|
131
|
+
# read. With classes cached it does nothing, so no file is ever checked.
|
|
132
|
+
def self.files_may_have_changed
|
|
133
|
+
@check_pending = true if reloading?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Stats every YAML file read so far (also missing ones) once. A change drops
|
|
137
|
+
# that file and every cached result (a new generation).
|
|
138
|
+
#
|
|
139
|
+
# Not ActiveSupport::FileUpdateChecker, for two reasons:
|
|
140
|
+
# - It detects an edit only when the newest mtime of all watched files grows
|
|
141
|
+
# (or the number of existing files changes). An edit that keeps the mtime,
|
|
142
|
+
# or a replacement by an older file, is missed.
|
|
143
|
+
# - Its files and directories are fixed when it is built, but component YAML
|
|
144
|
+
# paths are discovered at the first render, so each new path would need a
|
|
145
|
+
# new checker.
|
|
146
|
+
# The signature here is mtime, ctime, size, inode and device of each file
|
|
147
|
+
# looked up so far, and nil for a missing one.
|
|
148
|
+
def self.check_for_changes
|
|
149
|
+
@file_lock.synchronize do
|
|
150
|
+
return unless @check_pending
|
|
151
|
+
|
|
152
|
+
@check_pending = false
|
|
153
|
+
changed = @file_cache.select { |path, entry| entry.signature != file_signature(path) }
|
|
154
|
+
next if changed.empty?
|
|
155
|
+
|
|
156
|
+
changed.each_key { |path| @file_cache.delete(path) }
|
|
157
|
+
invalidate_results
|
|
99
158
|
end
|
|
159
|
+
end
|
|
100
160
|
|
|
101
|
-
|
|
161
|
+
def self.reset_caches(files: false)
|
|
162
|
+
@file_lock.synchronize do
|
|
163
|
+
if files
|
|
164
|
+
@file_cache.clear
|
|
165
|
+
@full_paths.clear
|
|
166
|
+
end
|
|
167
|
+
@component_files = Concurrent::Map.new
|
|
168
|
+
invalidate_results
|
|
169
|
+
end
|
|
102
170
|
end
|
|
103
171
|
|
|
104
|
-
|
|
105
|
-
|
|
172
|
+
# Must be called with @file_lock held.
|
|
173
|
+
def self.invalidate_results
|
|
174
|
+
@static_layers = nil
|
|
175
|
+
@results.invalidate
|
|
176
|
+
end
|
|
106
177
|
|
|
107
|
-
|
|
178
|
+
def self.tailwind_merge_available?
|
|
179
|
+
return @tailwind_merge_available if defined?(@tailwind_merge_available)
|
|
108
180
|
|
|
109
|
-
|
|
181
|
+
@tailwind_merge_available = begin
|
|
182
|
+
require "tailwind_merge"
|
|
183
|
+
true
|
|
184
|
+
rescue LoadError
|
|
185
|
+
false
|
|
186
|
+
end
|
|
110
187
|
end
|
|
111
188
|
|
|
112
|
-
|
|
113
|
-
|
|
189
|
+
# The YAML candidates next to a component's source file, computed once per class.
|
|
190
|
+
def self.component_classy_files(component_class)
|
|
191
|
+
@component_files.compute_if_absent(component_class) do
|
|
192
|
+
source_file = Object.const_source_location(component_class.name).first
|
|
193
|
+
calling_path = File.dirname(source_file)
|
|
194
|
+
calling_file = File.basename(source_file).split(".").first
|
|
195
|
+
component_name = component_class.name.underscore.split("/").last.split(".").first
|
|
196
|
+
|
|
197
|
+
[ "#{calling_path}/#{component_name}.yml",
|
|
198
|
+
"#{calling_path}/#{calling_file}/#{calling_file}.yml",
|
|
199
|
+
"#{calling_path}/#{calling_file}/#{component_name}.yml" ].uniq.each(&:freeze).freeze
|
|
200
|
+
end
|
|
114
201
|
end
|
|
115
202
|
|
|
116
|
-
|
|
117
|
-
|
|
203
|
+
# Reloaded component classes are new objects, so drop the old ones.
|
|
204
|
+
def self.clear_component_files
|
|
205
|
+
@component_files = Concurrent::Map.new
|
|
118
206
|
end
|
|
119
207
|
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
208
|
+
# Engine files (lowest priority), the default file, then extra files.
|
|
209
|
+
def self.static_layers
|
|
210
|
+
@static_layers || begin
|
|
211
|
+
generation = @results.generation
|
|
212
|
+
layers = [
|
|
213
|
+
*engine_files.map { |path| cached_yaml_file(path, "engine") },
|
|
214
|
+
cached_yaml_file(default_file, "default"),
|
|
215
|
+
*extra_files.map { |path| cached_yaml_file(path, "extra") }
|
|
216
|
+
].compact.freeze
|
|
217
|
+
@file_lock.synchronize { @static_layers = layers if generation == @results.generation }
|
|
218
|
+
layers
|
|
219
|
+
end
|
|
124
220
|
end
|
|
125
221
|
|
|
126
|
-
#
|
|
222
|
+
# Returns the compiled YAML of a file, or nil when it is missing or invalid.
|
|
127
223
|
def self.cached_yaml_file(file_path, file_type)
|
|
128
224
|
@file_lock.synchronize do
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return cached[:yaml] if cached && !reloading?
|
|
133
|
-
|
|
134
|
-
stat = File.stat(path)
|
|
135
|
-
signature = [ stat.mtime, stat.ctime, stat.size, stat.ino, stat.dev ]
|
|
136
|
-
return cached[:yaml] if cached && cached[:signature] == signature
|
|
137
|
-
|
|
138
|
-
content = File.read(path, encoding: "UTF-8")
|
|
139
|
-
parsed = YAML.safe_load(content, permitted_classes: [ Symbol, String, Array, Hash ], aliases: true)
|
|
140
|
-
yaml = parsed.is_a?(Hash) ? parsed : nil
|
|
141
|
-
@file_cache[path] = { signature: signature, yaml: yaml }
|
|
142
|
-
yaml
|
|
143
|
-
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
144
|
-
# A missing file stays missing until the next restart when classes are cached.
|
|
145
|
-
reloading? ? @file_cache.delete(path) : @file_cache[path] = { signature: nil, yaml: nil }
|
|
146
|
-
nil
|
|
147
|
-
rescue Psych::SyntaxError => e
|
|
148
|
-
@file_cache.delete(path)
|
|
149
|
-
Rails.logger.error "Classy::Yaml: Failed to parse #{file_type} YAML file #{file_path}: #{e.message}"
|
|
150
|
-
nil
|
|
151
|
-
rescue => e
|
|
152
|
-
@file_cache.delete(path)
|
|
153
|
-
Rails.logger.error "Classy::Yaml: Error loading #{file_type} YAML file #{file_path}: #{e.message}"
|
|
154
|
-
nil
|
|
155
|
-
end
|
|
225
|
+
path = @full_paths[file_path] ||= Rails.root.join(file_path).to_s.freeze
|
|
226
|
+
entry = @file_cache[path] ||= load_yaml_file(path, file_path, file_type)
|
|
227
|
+
entry.layer
|
|
156
228
|
end
|
|
157
229
|
end
|
|
158
230
|
|
|
@@ -163,5 +235,116 @@ module Classy
|
|
|
163
235
|
@merger.merge(classes)
|
|
164
236
|
end
|
|
165
237
|
end
|
|
238
|
+
|
|
239
|
+
# Edits are picked up without a restart only while Rails reloads code.
|
|
240
|
+
# With classes cached (test, production) a parsed file is never stat'ed again.
|
|
241
|
+
def self.reloading?
|
|
242
|
+
!Rails.application.config.cache_classes
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Turns parsed YAML into frozen lookup data: a string becomes its classes, an
|
|
246
|
+
# array its flattened classes. Other values stay as they are and give no classes.
|
|
247
|
+
def self.compile(value, compiled_hashes = {}.compare_by_identity)
|
|
248
|
+
case value
|
|
249
|
+
when Hash
|
|
250
|
+
compiled_hashes[value] || begin
|
|
251
|
+
compiled = compiled_hashes[value] = {}
|
|
252
|
+
value.each { |key, child| compiled[key] = compile(child, compiled_hashes) }
|
|
253
|
+
compiled.freeze
|
|
254
|
+
end
|
|
255
|
+
when String
|
|
256
|
+
value.split(" ").reject(&:blank?).each(&:freeze).freeze
|
|
257
|
+
when Array
|
|
258
|
+
begin
|
|
259
|
+
value.flatten.map(&:to_s).reject(&:blank?).each(&:freeze).freeze
|
|
260
|
+
rescue ArgumentError
|
|
261
|
+
# flatten raises on a recursive array; lookups report it as 1.7.2 did.
|
|
262
|
+
Lookup::RECURSIVE
|
|
263
|
+
end
|
|
264
|
+
else
|
|
265
|
+
value
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def self.apply_tag_helper_override
|
|
270
|
+
require "classy/yaml/tag_helper"
|
|
271
|
+
|
|
272
|
+
ActiveSupport.on_load(:action_view) do
|
|
273
|
+
ActionView::Helpers::TagHelper::TagBuilder.prepend(Classy::Yaml::TagHelper)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
apply_icon_helper_override
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def self.apply_icon_helper_override
|
|
280
|
+
return unless defined?(RailsIcons::Helpers::IconHelper)
|
|
281
|
+
|
|
282
|
+
require "classy/yaml/icon_helper"
|
|
283
|
+
|
|
284
|
+
RailsIcons::Helpers::IconHelper.prepend(Classy::Yaml::IconHelper)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
class << self
|
|
288
|
+
private
|
|
289
|
+
|
|
290
|
+
def build_classes(args, tailwind)
|
|
291
|
+
layers = static_layers
|
|
292
|
+
classy_files = args.find { |arg| arg.is_a?(Hash) && arg.key?(:classy_files) }
|
|
293
|
+
if classy_files
|
|
294
|
+
layers = layers.dup
|
|
295
|
+
classy_files[:classy_files].each do |file_path|
|
|
296
|
+
layer = cached_yaml_file(file_path, "classy")
|
|
297
|
+
layers << layer if layer
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
skip_base = args.find { |arg| arg.is_a?(Hash) && arg.key?(:skip_base) }
|
|
301
|
+
|
|
302
|
+
keys = []
|
|
303
|
+
adds = []
|
|
304
|
+
Lookup.collect_list(args, Lookup::EMPTY, keys, adds)
|
|
305
|
+
keys.uniq!
|
|
306
|
+
classes = []
|
|
307
|
+
clean = Lookup.resolve(keys, layers, skip_base && skip_base[:skip_base], classes)
|
|
308
|
+
classes.uniq!
|
|
309
|
+
unless adds.empty?
|
|
310
|
+
classes.concat(adds)
|
|
311
|
+
classes.flatten!
|
|
312
|
+
classes.uniq!
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
result = if classes.empty?
|
|
316
|
+
+""
|
|
317
|
+
elsif tailwind
|
|
318
|
+
merge_classes(classes)
|
|
319
|
+
else
|
|
320
|
+
classes.join(" ")
|
|
321
|
+
end
|
|
322
|
+
[ result, clean ]
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def load_yaml_file(path, file_path, file_type)
|
|
326
|
+
signature = file_signature(path)
|
|
327
|
+
return FileEntry.new(nil, nil).freeze unless signature
|
|
328
|
+
|
|
329
|
+
content = File.read(path, encoding: "UTF-8")
|
|
330
|
+
parsed = YAML.safe_load(content, permitted_classes: [ Symbol, String, Array, Hash ], aliases: true)
|
|
331
|
+
FileEntry.new(signature, parsed.is_a?(Hash) ? compile(parsed) : nil).freeze
|
|
332
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
333
|
+
FileEntry.new(nil, nil).freeze
|
|
334
|
+
rescue Psych::SyntaxError => e
|
|
335
|
+
Rails.logger.error "Classy::Yaml: Failed to parse #{file_type} YAML file #{file_path}: #{e.message}"
|
|
336
|
+
FileEntry.new(signature, nil).freeze
|
|
337
|
+
rescue => e
|
|
338
|
+
Rails.logger.error "Classy::Yaml: Error loading #{file_type} YAML file #{file_path}: #{e.message}"
|
|
339
|
+
FileEntry.new(signature, nil).freeze
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def file_signature(path)
|
|
343
|
+
stat = File.stat(path)
|
|
344
|
+
[ stat.mtime, stat.ctime, stat.size, stat.ino, stat.dev ]
|
|
345
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
346
|
+
nil
|
|
347
|
+
end
|
|
348
|
+
end
|
|
166
349
|
end
|
|
167
350
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: classy-yaml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.7.
|
|
4
|
+
version: 1.7.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tonksthebear
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -152,6 +152,8 @@ files:
|
|
|
152
152
|
- lib/classy/yaml/helpers.rb
|
|
153
153
|
- lib/classy/yaml/icon_helper.rb
|
|
154
154
|
- lib/classy/yaml/invalid_key_error.rb
|
|
155
|
+
- lib/classy/yaml/lookup.rb
|
|
156
|
+
- lib/classy/yaml/result_cache.rb
|
|
155
157
|
- lib/classy/yaml/tag_helper.rb
|
|
156
158
|
- lib/classy/yaml/version.rb
|
|
157
159
|
- lib/tasks/classy/yaml_tasks.rake
|