classy-yaml 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afbd609fcea95d0b745fe2aa147dc38e2f34b22d18fe315ef905d75988d21555
4
- data.tar.gz: 7862d4e6f955b973052b53ebeeca9fa879101594e47673a8940ea10baaeb4634
3
+ metadata.gz: 94e216b419acc7e44aa7a69c78c127319dcfa402c15104c529f68ea1b841ab93
4
+ data.tar.gz: 335441bc701254faa6a92bda5e682f57234761908ec4c4af2d3209f6115f8b70
5
5
  SHA512:
6
- metadata.gz: d42aa732b1e4c1feeff22ffc8cb3b0f47ea31a48be7b1bf5b26141e6e8f3a9c9047cd6c9aaba8f302e2eb61a42cc3c0dc50f0e0f8d5b942755207fb842f7545a
7
- data.tar.gz: cd52524424ae4e255044a54de0f6b94cf76e67dea5ffc823a3829b7b897f6b4ff32d90c576e5d76f3afa90cc8054328e6734916f14ca5a3da980d15dece785d4
6
+ metadata.gz: 006251bb6ddbd941c5bfe7f08a75cbafe7400178ac1550c316acf1499b100764f26ae9255d2acf14731db4028750c3b67af814cec277fb0907fd9d4e52ff305b
7
+ data.tar.gz: 4a04bb0454b7e3ad8d1165b41923b87ff49df5c52051295c032396ceb13b91d986dc18466e731ca06617530a81cea7b51317444aa8a1ab5cdabde8443c9adf0a
data/README.md CHANGED
@@ -101,6 +101,10 @@ In your views or components:
101
101
  <div class="px-4 py-2 rounded font-medium bg-blue-500 text-white hover:bg-blue-600 m-4"></div>
102
102
  ```
103
103
 
104
+ The helper reads argument hashes and arrays without changing them. You can reuse frozen arguments across calls. The `add` option also works when no YAML file exists.
105
+
106
+ Classy YAML checks component files, extra files, the default file, and engine files in that priority order. Base classes and specific classes fall back independently. Control options such as `skip_base` and `classy_files` are not YAML lookup keys.
107
+
104
108
  ### ViewComponent Integration
105
109
 
106
110
  Include the component helpers in your ViewComponent:
@@ -138,12 +142,20 @@ btn:
138
142
  Classy YAML detects if [tailwind_merge](https://github.com/gjtorikian/tailwind_merge) is installed and will leverage it when fetching classes with `yass`. Please read their documentation for installation and benefits.
139
143
 
140
144
 
145
+ ## Caching and development reloads
146
+
147
+ Classy YAML reuses parsed YAML when file metadata has not changed. In development and tests, each call checks the default, engine, extra, and component files. Edits, additions, and deletions take effect on the next call without a restart.
148
+
149
+ In production, the default and engine YAML remain cached until configuration resets or the process restarts. Extra and component files still receive metadata checks. `Classy::Yaml.setup` clears the YAML caches.
150
+
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.
152
+
141
153
  ## Configuration Options
142
154
 
143
155
  | Option | Default | Description |
144
156
  |--------|---------|-------------|
145
157
  | `default_file` | `"config/utility_classes.yml"` | Path to the main YAML file |
146
- | `extra_files` | `[]` | Array of additional YAML files (highest priority) |
158
+ | `extra_files` | `[]` | Array of additional YAML files (above default and engine files; below component files) |
147
159
  | `engine_files` | `[]` | Array of engine YAML files (lowest priority) |
148
160
  | `override_tag_helpers` | `false` | Automatically process class symbols/hashes in Rails tag helpers |
149
161
 
@@ -2,8 +2,9 @@ module Classy
2
2
  module Yaml
3
3
  module ComponentHelpers
4
4
  def yass(*args)
5
- calling_path = Object.const_source_location(self.class.name).first.split("/")[0...-1].join("/")
6
- calling_file = Object.const_source_location(self.class.name).first.split("/").last.split(".").first
5
+ source_file = Object.const_source_location(self.class.name).first
6
+ calling_path = File.dirname(source_file)
7
+ calling_file = File.basename(source_file).split(".").first
7
8
  component_name = self.class.name.underscore.split("/").last.split(".").first
8
9
 
9
10
  classy_files = [ "#{calling_path}/#{component_name}.yml",
@@ -11,7 +12,7 @@ module Classy
11
12
  "#{calling_path}/#{calling_file}/#{component_name}.yml" ]
12
13
 
13
14
  if args.first.is_a?(Hash)
14
- args.first.merge!({ classy_files: classy_files.uniq })
15
+ args[0] = args.first.merge(classy_files: classy_files.uniq)
15
16
  else
16
17
  args << { classy_files: classy_files.uniq }
17
18
  end
@@ -3,14 +3,15 @@ module Classy
3
3
  module Helpers
4
4
  # Fetches utility classes from YAML files based on the provided keys.
5
5
  # The method follows a priority order:
6
- # 1. Extra files (highest priority)
7
- # 2. Default YAML
8
- # 3. ViewComponent YAMLs (lowest priority)
6
+ # 1. Component files (highest priority)
7
+ # 2. Extra files
8
+ # 3. Default YAML
9
+ # 4. Engine files (lowest priority)
9
10
  #
10
11
  # @param args [Array] Array of keys to look up in the YAML files
11
12
  # @return [String] Space-separated list of CSS classes
12
13
  def yass(*args)
13
- # Start with ViewComponent YAMLs (lowest priority)
14
+ # Start with engine YAMLs (lowest priority)
14
15
  classy_yamls = Classy::Yaml.cached_engine_yamls.dup
15
16
 
16
17
  # Add default YAML (next priority)
@@ -23,16 +24,14 @@ module Classy
23
24
  end
24
25
 
25
26
  # Add classy_files (highest priority)
26
- classy_files_hash = args.find { |arg| arg.is_a?(Hash) && arg.keys.include?(:classy_files) } || { classy_files: [] }
27
+ classy_files_hash = args.find { |arg| arg.is_a?(Hash) && arg.key?(:classy_files) } || { classy_files: [] }
27
28
  classy_files_hash[:classy_files].each do |file_path|
28
29
  load_yaml_file(file_path, classy_yamls, "classy")
29
30
  end
30
31
 
31
- return "" if classy_yamls.blank?
32
-
33
- skip_base_hash = args.find { |arg| arg.is_a?(Hash) && arg.keys.include?(:skip_base) } || {}
32
+ skip_base_hash = args.find { |arg| arg.is_a?(Hash) && arg.key?(:skip_base) } || {}
34
33
  keys, add_classes = flatten_args(values: args)
35
- classes = fetch_classes(keys, classy_yamls: classy_yamls, skip_base: skip_base_hash[:skip_base])
34
+ classes = fetch_classes(keys.uniq, classy_yamls: classy_yamls, skip_base: skip_base_hash[:skip_base])
36
35
  classes += add_classes
37
36
 
38
37
  # Use tailwind_merge if available, otherwise fall back to simple join
@@ -49,7 +48,7 @@ module Classy
49
48
 
50
49
  if tailwind_merge_available?
51
50
  # Use tailwind_merge for intelligent class merging
52
- TailwindMerge::Merger.new.merge(classes)
51
+ Classy::Yaml.merge_classes(classes)
53
52
  else
54
53
  # Fall back to simple space-joined classes
55
54
  classes.join(" ")
@@ -73,18 +72,8 @@ module Classy
73
72
  # @param classy_yamls [Array] Array to add the parsed YAML to
74
73
  # @param file_type [String] Type of file being loaded (for error messages)
75
74
  def load_yaml_file(file_path, classy_yamls, file_type)
76
- begin
77
- path_obj = file_path.is_a?(Pathname) ? file_path : Rails.root.join(file_path)
78
- if File.exist?(path_obj)
79
- content = File.read(path_obj, encoding: "UTF-8")
80
- parsed_yaml = YAML.safe_load(content, permitted_classes: [ Symbol, String, Array, Hash ], aliases: true)
81
- classy_yamls << parsed_yaml if parsed_yaml && parsed_yaml.is_a?(Hash)
82
- end
83
- rescue Psych::SyntaxError => e
84
- Rails.logger.error "Classy::Yaml (yass helper): Failed to parse #{file_type} YAML file #{file_path}: #{e.message}"
85
- rescue => e
86
- Rails.logger.error "Classy::Yaml (yass helper): Error loading #{file_type} YAML file #{file_path}: #{e.message}"
87
- end
75
+ parsed_yaml = Classy::Yaml.cached_yaml_file(file_path, file_type)
76
+ classy_yamls << parsed_yaml if parsed_yaml
88
77
  end
89
78
 
90
79
  # Flattens the arguments into keys and classes
@@ -94,27 +83,27 @@ module Classy
94
83
  # @param added_classes [Array] Array to store found classes
95
84
  # @return [Array] Tuple of [keys, added_classes]
96
85
  def flatten_args(root: [], values: [], keys: [], added_classes: [])
86
+ parent_keys = []
97
87
  values.each do |value|
98
- if value.is_a?(Hash)
99
- if value.has_key? :add
100
- added_classes << value[:add]
101
- value.except! :add
102
- end
103
-
104
- value.keys.each do |key|
105
- values << (root + [ key.to_s ])
106
- flatten_args(root: root + [ key.to_s ], values: [ value[key] ], keys: keys, added_classes: added_classes)
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)
107
98
  end
99
+ when Array
100
+ flatten_args(root: root, values: value, keys: keys, added_classes: added_classes)
108
101
  else
109
- if value.is_a?(Array)
110
- flatten_args(root: root, values: value, keys: keys, added_classes: added_classes)
111
- else
112
- keys << (root + [ value.to_s ])
113
- end
102
+ keys << (root + [ value.to_s ])
114
103
  end
115
104
  end
116
-
117
- return keys, added_classes
105
+ keys.concat(parent_keys)
106
+ [ keys, added_classes ]
118
107
  end
119
108
 
120
109
  # Fetches classes from the YAML files based on the provided keys
@@ -125,42 +114,30 @@ module Classy
125
114
  def fetch_classes(keys, classy_yamls: [], skip_base: false)
126
115
  classes = []
127
116
 
128
- keys.map do |key|
117
+ keys.each do |key|
129
118
  base_classes = nil
130
119
  fetched_classes = nil
131
120
 
132
121
  classy_yamls.reverse_each do |classy_yaml|
133
- # Base Class Lookup
134
- unless skip_base == true
135
- begin
136
- value_at_key = classy_yaml.send(:dig, *key)
137
- base_value = if value_at_key.is_a?(Hash)
138
- classy_yaml.send(:dig, *(key + [ "base" ]))
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"]
139
127
  elsif key.length > 1
140
- classy_yaml.send(:dig, *(key[0...-1] + [ "base" ]))
141
- else
142
- nil
128
+ classy_yaml.dig(*(key[0...-1] + [ "base" ]))
143
129
  end
144
- normalized_base = normalize_original(base_value)
145
- base_classes ||= normalized_base
146
- rescue
147
- Rails.logger.warn(Classy::Yaml::InvalidKeyError.new(data: key))
130
+ base_classes = normalize_original(base_value).presence
148
131
  end
149
- end
150
132
 
151
- # Specific Key Class Lookup
152
- begin
153
- value = classy_yaml.send(:dig, *key)
154
- unless value.is_a?(Hash)
155
- normalized_fetched = normalize_original(value)
156
- fetched_classes ||= normalized_fetched
133
+ unless fetched_classes || value.is_a?(Hash)
134
+ fetched_classes = normalize_original(value).presence
157
135
  end
158
- rescue
136
+ rescue StandardError
159
137
  Rails.logger.warn(Classy::Yaml::InvalidKeyError.new(data: key))
160
138
  end
161
139
 
162
- base_classes = nil if base_classes.blank?
163
- fetched_classes = nil if fetched_classes.blank?
140
+ break if fetched_classes && (skip_base == true || base_classes)
164
141
  end
165
142
 
166
143
  classes << base_classes unless base_classes.blank?
@@ -1,5 +1,5 @@
1
1
  module Classy
2
2
  module Yaml
3
- VERSION = "1.7.0"
3
+ VERSION = "1.7.1"
4
4
  end
5
5
  end
data/lib/classy/yaml.rb CHANGED
@@ -25,6 +25,9 @@ module Classy
25
25
  @cached_engine_yamls = nil
26
26
  @cached_default_yaml = nil
27
27
  @load_lock = Mutex.new # Prevent race conditions during lazy loading
28
+ @file_cache = {}
29
+ @file_lock = Mutex.new
30
+ @merger_lock = Mutex.new
28
31
 
29
32
  # -- Configuration Setters with Path Resolution --
30
33
  def self.engine_files=(value)
@@ -34,7 +37,7 @@ module Classy
34
37
 
35
38
  def self.extra_files=(value)
36
39
  @@extra_files = Array.wrap(value).reject(&:blank?).map { |file| Rails.root.join(file) }
37
- # Note: extra_files are not cached globally by default
40
+ # Parsed files use the shared file cache.
38
41
  end
39
42
 
40
43
  def self.default_file=(value)
@@ -49,7 +52,7 @@ module Classy
49
52
 
50
53
  # -- Cached Data Accessors (Lazy Loading) --
51
54
  def self.cached_engine_yamls
52
- # Bypass cache in development and test environments
55
+ # Check file metadata in development and test environments
53
56
  return load_engine_yamls if Rails.env.development? || Rails.env.test?
54
57
 
55
58
  # Use cache in other environments
@@ -63,7 +66,7 @@ module Classy
63
66
  end
64
67
 
65
68
  def self.cached_default_yaml
66
- # Bypass cache in development and test environments
69
+ # Check file metadata in development and test environments
67
70
  return load_default_yaml if Rails.env.development? || Rails.env.test?
68
71
 
69
72
  # Use cache in other environments
@@ -81,6 +84,7 @@ module Classy
81
84
  # Clear all caches when configuration changes
82
85
  @cached_engine_yamls = nil
83
86
  @cached_default_yaml = nil
87
+ @file_lock.synchronize { @file_cache.clear }
84
88
  # Apply tag helper override if enabled
85
89
  apply_tag_helper_override if @@override_tag_helpers
86
90
  end
@@ -106,37 +110,49 @@ module Classy
106
110
  end
107
111
 
108
112
  def self.load_engine_yamls
109
- yamls = []
110
- self.engine_files.each do |file_path|
113
+ engine_files.map { |path| cached_yaml_file(path, "engine") }.compact
114
+ end
115
+
116
+ def self.load_default_yaml
117
+ cached_yaml_file(default_file, "default")
118
+ end
119
+
120
+ # Check metadata on each access so edits do not require a Rails reload.
121
+ def self.cached_yaml_file(file_path, file_type)
122
+ @file_lock.synchronize do
111
123
  begin
112
- if File.exist?(file_path)
113
- content = File.read(file_path, encoding: "UTF-8")
114
- parsed_yaml = YAML.safe_load(content, permitted_classes: [ Symbol, String, Array, Hash ], aliases: true)
115
- yamls << parsed_yaml if parsed_yaml && parsed_yaml.is_a?(Hash)
116
- end
124
+ path = Rails.root.join(file_path).to_s
125
+ stat = File.stat(path)
126
+ signature = [ stat.mtime, stat.ctime, stat.size, stat.ino, stat.dev ]
127
+ cached = @file_cache[path]
128
+ return cached[:yaml] if cached && cached[:signature] == signature
129
+
130
+ content = File.read(path, encoding: "UTF-8")
131
+ parsed = YAML.safe_load(content, permitted_classes: [ Symbol, String, Array, Hash ], aliases: true)
132
+ yaml = parsed.is_a?(Hash) ? parsed : nil
133
+ @file_cache[path] = { signature: signature, yaml: yaml }
134
+ yaml
135
+ rescue Errno::ENOENT, Errno::ENOTDIR
136
+ @file_cache.delete(path)
137
+ nil
117
138
  rescue Psych::SyntaxError => e
118
- Rails.logger.error "Classy::Yaml: Failed to parse engine YAML file #{file_path}: #{e.message}"
139
+ @file_cache.delete(path)
140
+ Rails.logger.error "Classy::Yaml: Failed to parse #{file_type} YAML file #{file_path}: #{e.message}"
141
+ nil
119
142
  rescue => e
120
- Rails.logger.error "Classy::Yaml: Error loading engine YAML file #{file_path}: #{e.message}"
143
+ @file_cache.delete(path)
144
+ Rails.logger.error "Classy::Yaml: Error loading #{file_type} YAML file #{file_path}: #{e.message}"
145
+ nil
121
146
  end
122
147
  end
123
- yamls
124
148
  end
125
149
 
126
- def self.load_default_yaml
127
- default_path = Rails.root.join(self.default_file)
128
- begin
129
- if File.exist?(default_path)
130
- content = File.read(default_path, encoding: "UTF-8")
131
- parsed_yaml = YAML.safe_load(content, permitted_classes: [ Symbol, String, Array, Hash ], aliases: true)
132
- return parsed_yaml if parsed_yaml && parsed_yaml.is_a?(Hash)
133
- end
134
- rescue Psych::SyntaxError => e
135
- Rails.logger.error "Classy::Yaml: Failed to parse default YAML file #{default_path}: #{e.message}"
136
- rescue => e
137
- Rails.logger.error "Classy::Yaml: Error loading default YAML file #{default_path}: #{e.message}"
150
+ # The merger cache is mutable, so concurrent calls must use the same lock.
151
+ def self.merge_classes(classes)
152
+ @merger_lock.synchronize do
153
+ @merger ||= TailwindMerge::Merger.new
154
+ @merger.merge(classes)
138
155
  end
139
- nil # Return nil if file doesn't exist or fails to load/parse
140
156
  end
141
157
  end
142
158
  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.0
4
+ version: 1.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tonksthebear
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-31 00:00:00.000000000 Z
11
+ date: 2026-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport