devex 0.3.5
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 +7 -0
- data/.obsidian/app.json +6 -0
- data/.obsidian/appearance.json +4 -0
- data/.obsidian/community-plugins.json +5 -0
- data/.obsidian/core-plugins.json +33 -0
- data/.obsidian/plugins/obsidian-minimal-settings/data.json +34 -0
- data/.obsidian/plugins/obsidian-minimal-settings/main.js +8 -0
- data/.obsidian/plugins/obsidian-minimal-settings/manifest.json +11 -0
- data/.obsidian/plugins/obsidian-style-settings/data.json +15 -0
- data/.obsidian/plugins/obsidian-style-settings/main.js +165 -0
- data/.obsidian/plugins/obsidian-style-settings/manifest.json +10 -0
- data/.obsidian/plugins/obsidian-style-settings/styles.css +243 -0
- data/.obsidian/plugins/table-editor-obsidian/data.json +6 -0
- data/.obsidian/plugins/table-editor-obsidian/main.js +236 -0
- data/.obsidian/plugins/table-editor-obsidian/manifest.json +17 -0
- data/.obsidian/plugins/table-editor-obsidian/styles.css +78 -0
- data/.obsidian/themes/AnuPpuccin/manifest.json +7 -0
- data/.obsidian/themes/AnuPpuccin/theme.css +9080 -0
- data/.obsidian/themes/Minimal/manifest.json +8 -0
- data/.obsidian/themes/Minimal/theme.css +2251 -0
- data/.rubocop.yml +231 -0
- data/CHANGELOG.md +97 -0
- data/LICENSE +21 -0
- data/README.md +314 -0
- data/Rakefile +13 -0
- data/devex-logo.jpg +0 -0
- data/docs/developing-tools.md +1000 -0
- data/docs/ref/agent-mode.md +46 -0
- data/docs/ref/cli-interface.md +60 -0
- data/docs/ref/configuration.md +46 -0
- data/docs/ref/design-philosophy.md +17 -0
- data/docs/ref/error-handling.md +38 -0
- data/docs/ref/io-handling.md +88 -0
- data/docs/ref/signals.md +141 -0
- data/docs/ref/temporal-software-theory.md +790 -0
- data/exe/dx +52 -0
- data/lib/devex/builtins/.index.rb +10 -0
- data/lib/devex/builtins/debug.rb +43 -0
- data/lib/devex/builtins/format.rb +44 -0
- data/lib/devex/builtins/gem.rb +77 -0
- data/lib/devex/builtins/lint.rb +61 -0
- data/lib/devex/builtins/test.rb +76 -0
- data/lib/devex/builtins/version.rb +156 -0
- data/lib/devex/cli.rb +340 -0
- data/lib/devex/context.rb +433 -0
- data/lib/devex/core/configuration.rb +136 -0
- data/lib/devex/core.rb +79 -0
- data/lib/devex/dirs.rb +210 -0
- data/lib/devex/dsl.rb +100 -0
- data/lib/devex/exec/controller.rb +245 -0
- data/lib/devex/exec/result.rb +229 -0
- data/lib/devex/exec.rb +662 -0
- data/lib/devex/loader.rb +136 -0
- data/lib/devex/output.rb +257 -0
- data/lib/devex/project_paths.rb +309 -0
- data/lib/devex/support/ansi.rb +437 -0
- data/lib/devex/support/core_ext.rb +560 -0
- data/lib/devex/support/global.rb +68 -0
- data/lib/devex/support/path.rb +357 -0
- data/lib/devex/support.rb +71 -0
- data/lib/devex/template_helpers.rb +136 -0
- data/lib/devex/templates/debug.erb +24 -0
- data/lib/devex/tool.rb +374 -0
- data/lib/devex/version.rb +5 -0
- data/lib/devex/working_dir.rb +99 -0
- data/lib/devex.rb +158 -0
- data/ruby-project-template/.gitignore +0 -0
- data/ruby-project-template/Gemfile +0 -0
- data/ruby-project-template/README.md +0 -0
- data/ruby-project-template/docs/README.md +0 -0
- data/sig/devex.rbs +4 -0
- metadata +122 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Devex
|
|
6
|
+
module Support
|
|
7
|
+
# Core extensions as refinements.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# using Devex::Support::CoreExt
|
|
11
|
+
#
|
|
12
|
+
# Or load globally (for CLI tools):
|
|
13
|
+
# require "devex/support/global"
|
|
14
|
+
#
|
|
15
|
+
module CoreExt
|
|
16
|
+
# ─────────────────────────────────────────────────────────────
|
|
17
|
+
# Implementation Modules
|
|
18
|
+
# These define the actual methods and can be included in both
|
|
19
|
+
# refinements and monkey-patched classes.
|
|
20
|
+
# ─────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
module ObjectMethods
|
|
23
|
+
def blank? = respond_to?(:empty?) ? empty? : !self
|
|
24
|
+
|
|
25
|
+
def present? = !blank?
|
|
26
|
+
|
|
27
|
+
def presence() self if present? end
|
|
28
|
+
|
|
29
|
+
def numeric?
|
|
30
|
+
true if Float(self)
|
|
31
|
+
rescue StandardError
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def in?(collection) = collection.include?(self)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module NilMethods
|
|
39
|
+
def blank? = true
|
|
40
|
+
def present? = false
|
|
41
|
+
def presence = nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module FalseMethods
|
|
45
|
+
def blank? = true
|
|
46
|
+
def present? = false
|
|
47
|
+
def presence = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
module TrueMethods
|
|
51
|
+
def blank? = false
|
|
52
|
+
def present? = true
|
|
53
|
+
def presence = self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
module NumericMethods
|
|
57
|
+
def blank? = false
|
|
58
|
+
def present? = true
|
|
59
|
+
def presence = self
|
|
60
|
+
def numeric? = true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
module ArrayBlankMethods
|
|
64
|
+
def blank? = empty?
|
|
65
|
+
def present? = !blank?
|
|
66
|
+
|
|
67
|
+
def presence() self if present? end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
module HashBlankMethods
|
|
71
|
+
def blank? = empty?
|
|
72
|
+
def present? = !blank?
|
|
73
|
+
|
|
74
|
+
def presence() self if present? end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
module StringMethods
|
|
78
|
+
def blank? = empty? || !match?(/[^[:space:]]/)
|
|
79
|
+
|
|
80
|
+
# Override present? to use String's blank? (import_methods copies bytecode,
|
|
81
|
+
# so ObjectMethods#present? would call Object's blank?)
|
|
82
|
+
def present? = !blank?
|
|
83
|
+
|
|
84
|
+
def presence() self if present? end
|
|
85
|
+
|
|
86
|
+
def to_p = Devex::Support::Path.new(self)
|
|
87
|
+
|
|
88
|
+
def wrap(indent = :first, width = 90)
|
|
89
|
+
ind = case indent
|
|
90
|
+
when :first then self[/^[[:space:]]*/] || ""
|
|
91
|
+
when ::String then indent
|
|
92
|
+
when ::Integer then " " * indent.abs
|
|
93
|
+
else ""
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
ind_size = (ind.count("\t") * 8) + ind.length - ind.count("\t")
|
|
97
|
+
effective_width = [width - ind_size, 1].max
|
|
98
|
+
|
|
99
|
+
paragraphs = strip.split(/\n[ \t]*\n/m)
|
|
100
|
+
paragraphs.map do |p|
|
|
101
|
+
p.gsub(/[[:space:]]+/, " ")
|
|
102
|
+
.strip
|
|
103
|
+
.scan(/.{1,#{effective_width}}(?: |$)/)
|
|
104
|
+
.map { |row| ind + row.strip }
|
|
105
|
+
.join("\n")
|
|
106
|
+
end.join("\n\n")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def sentences = gsub(/\s+/, " ").scan(/[^.!?]+[.!?]+(?:\s+|$)|[^.!?]+$/).map(&:strip).reject(&:empty?)
|
|
110
|
+
|
|
111
|
+
def to_sh
|
|
112
|
+
return "''" if empty?
|
|
113
|
+
|
|
114
|
+
gsub(%r{([^A-Za-z0-9_\-.,:/@\n])}, '\\\\\\\\\\1').gsub("\n", "'\n'")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def squish = gsub(/[[:space:]]+/, " ").strip
|
|
118
|
+
|
|
119
|
+
def fnv32 = bytes.reduce(0x811c9dc5) { |h, b| ((h ^ b) * 0x01000193) % (1 << 32) }
|
|
120
|
+
|
|
121
|
+
def fnv64 = bytes.reduce(0xcbf29ce484222325) { |h, b| ((h ^ b) * 0x100000001b3) % (1 << 64) }
|
|
122
|
+
|
|
123
|
+
def base64url
|
|
124
|
+
require "base64"
|
|
125
|
+
Base64.urlsafe_encode64(self, padding: false)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def truncate(length, omission: "...")
|
|
129
|
+
return self if self.length <= length
|
|
130
|
+
|
|
131
|
+
stop = length - omission.length
|
|
132
|
+
stop = 0 if stop < 0
|
|
133
|
+
self[0, stop] + omission
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def truncate_words(count, omission: "...")
|
|
137
|
+
words = split
|
|
138
|
+
return self if words.length <= count
|
|
139
|
+
|
|
140
|
+
words.first(count).join(" ") + omission
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def indent(amount, indent_char = " ")
|
|
144
|
+
prefix = indent_char * amount
|
|
145
|
+
gsub(/^/, prefix)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def remove(pattern) = gsub(pattern, "")
|
|
149
|
+
|
|
150
|
+
# ─────────────────────────────────────────────────────────────
|
|
151
|
+
# Case Transforms
|
|
152
|
+
# ─────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
# ALL UPPER CASE
|
|
155
|
+
def up_case = upcase
|
|
156
|
+
|
|
157
|
+
# all lower case
|
|
158
|
+
def down_case = downcase
|
|
159
|
+
|
|
160
|
+
# snake_case
|
|
161
|
+
# Converts CamelCase, kebab-case, spaces to snake_case
|
|
162
|
+
def snake_case = gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').gsub(/[\s-]+/, "_").downcase
|
|
163
|
+
|
|
164
|
+
# SCREAM_CASE (screaming snake case / constant case)
|
|
165
|
+
def scream_case = snake_case.upcase
|
|
166
|
+
|
|
167
|
+
# kebab-case
|
|
168
|
+
def kebab_case = snake_case.tr("_", "-")
|
|
169
|
+
|
|
170
|
+
# PascalCase (first letter uppercase)
|
|
171
|
+
def pascal_case = snake_case.split("_").map(&:capitalize).join
|
|
172
|
+
|
|
173
|
+
# camelCase (first letter lowercase)
|
|
174
|
+
def camel_case
|
|
175
|
+
result = pascal_case
|
|
176
|
+
return result if result.empty?
|
|
177
|
+
|
|
178
|
+
result[0] = result[0].downcase
|
|
179
|
+
result
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Title Case With Proper Rules
|
|
183
|
+
# - Always capitalize first/last word
|
|
184
|
+
# - Lowercase: articles, coord conjunctions, short prepositions
|
|
185
|
+
# - Capitalize after hyphens (unless minor word)
|
|
186
|
+
def title_case
|
|
187
|
+
# Words that should be lowercase (unless first/last)
|
|
188
|
+
# Articles, coordinating conjunctions, short prepositions
|
|
189
|
+
# Note: verbs (is), pronouns (it), subordinating conj (if) should be capitalized
|
|
190
|
+
minor_words = Set.new(%w[
|
|
191
|
+
a an the
|
|
192
|
+
for and nor but or yet so
|
|
193
|
+
at by in to of on up as
|
|
194
|
+
])
|
|
195
|
+
|
|
196
|
+
# Split keeping delimiters (spaces and hyphens)
|
|
197
|
+
tokens = split(/(\s+|-+)/)
|
|
198
|
+
return "" if tokens.empty?
|
|
199
|
+
|
|
200
|
+
# Find actual words (not delimiters)
|
|
201
|
+
word_indices = tokens.each_index.select { |i| !tokens[i].match?(/^[\s-]+$/) }
|
|
202
|
+
return self if word_indices.empty?
|
|
203
|
+
|
|
204
|
+
first_word_idx = word_indices.first
|
|
205
|
+
last_word_idx = word_indices.last
|
|
206
|
+
|
|
207
|
+
tokens.each_with_index.map do |token, idx|
|
|
208
|
+
if token.match?(/^[\s-]+$/)
|
|
209
|
+
# Delimiter - keep as-is
|
|
210
|
+
token
|
|
211
|
+
elsif idx == first_word_idx || idx == last_word_idx
|
|
212
|
+
# First or last word - always capitalize
|
|
213
|
+
token.capitalize
|
|
214
|
+
elsif minor_words.include?(token.downcase)
|
|
215
|
+
# Minor word - lowercase
|
|
216
|
+
token.downcase
|
|
217
|
+
else
|
|
218
|
+
# Regular word - capitalize
|
|
219
|
+
token.capitalize
|
|
220
|
+
end
|
|
221
|
+
end.join
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Aliases without underscores
|
|
225
|
+
# Note: upcase and downcase are Ruby native - don't override
|
|
226
|
+
def snakecase = snake_case
|
|
227
|
+
def screamcase = scream_case
|
|
228
|
+
def kebabcase = kebab_case
|
|
229
|
+
def pascalcase = pascal_case
|
|
230
|
+
def camelcase = camel_case
|
|
231
|
+
def titlecase = title_case
|
|
232
|
+
|
|
233
|
+
# Additional common aliases
|
|
234
|
+
def upper = up_case
|
|
235
|
+
def uppercase = up_case
|
|
236
|
+
def upper_case = up_case
|
|
237
|
+
def caps = up_case
|
|
238
|
+
|
|
239
|
+
def lower = down_case
|
|
240
|
+
def lowercase = down_case
|
|
241
|
+
def lower_case = down_case
|
|
242
|
+
|
|
243
|
+
def var_case = snake_case
|
|
244
|
+
def varcase = snake_case
|
|
245
|
+
|
|
246
|
+
def const_case = scream_case
|
|
247
|
+
def constcase = scream_case
|
|
248
|
+
|
|
249
|
+
def mod_case = pascal_case
|
|
250
|
+
def modcase = pascal_case
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
module EnumerableMethods
|
|
254
|
+
def average
|
|
255
|
+
return 0.0 if respond_to?(:empty?) && empty?
|
|
256
|
+
|
|
257
|
+
arr = to_a
|
|
258
|
+
return 0.0 if arr.empty?
|
|
259
|
+
|
|
260
|
+
arr.sum.to_f / arr.size
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def mean = average
|
|
264
|
+
|
|
265
|
+
def median
|
|
266
|
+
arr = to_a.sort
|
|
267
|
+
return nil if arr.empty?
|
|
268
|
+
|
|
269
|
+
mid = arr.size / 2
|
|
270
|
+
arr.size.odd? ? arr[mid] : (arr[mid - 1] + arr[mid]) / 2.0
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def sample_variance
|
|
274
|
+
arr = to_a
|
|
275
|
+
return 0.0 if arr.size < 2
|
|
276
|
+
|
|
277
|
+
avg = arr.sum.to_f / arr.size
|
|
278
|
+
arr.sum { |x| (x - avg) ** 2 } / (arr.size - 1).to_f
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def variance = sample_variance
|
|
282
|
+
|
|
283
|
+
def standard_deviation = Math.sqrt(sample_variance)
|
|
284
|
+
|
|
285
|
+
def stddev = standard_deviation
|
|
286
|
+
|
|
287
|
+
def percentile(p)
|
|
288
|
+
arr = to_a.sort
|
|
289
|
+
return nil if arr.empty?
|
|
290
|
+
|
|
291
|
+
k = (p / 100.0) * (arr.size - 1)
|
|
292
|
+
f = k.floor
|
|
293
|
+
c = k.ceil
|
|
294
|
+
return arr[f] if f == c
|
|
295
|
+
|
|
296
|
+
(arr[f] * (c - k)) + (arr[c] * (k - f))
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def q20 = percentile(20)
|
|
300
|
+
def q80 = percentile(80)
|
|
301
|
+
|
|
302
|
+
def robust_average
|
|
303
|
+
arr = to_a
|
|
304
|
+
return nil if arr.empty?
|
|
305
|
+
|
|
306
|
+
(q20.to_f + median.to_f + q80.to_f) / 3.0
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def amap(method, *args, &block) = map { |item| item.send(method, *args, &block) }
|
|
310
|
+
|
|
311
|
+
def summarize_runs
|
|
312
|
+
arr = to_a
|
|
313
|
+
return [] if arr.empty?
|
|
314
|
+
|
|
315
|
+
arr.chunk_while { |a, b| a == b }.map { |run| [run.size, run.first] }
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def many?
|
|
319
|
+
count = 0
|
|
320
|
+
if block_given?
|
|
321
|
+
each do |e|
|
|
322
|
+
count += 1 if yield(e)
|
|
323
|
+
return true if count > 1
|
|
324
|
+
end
|
|
325
|
+
else
|
|
326
|
+
each do
|
|
327
|
+
count += 1
|
|
328
|
+
return true if count > 1
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
false
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def index_by = each_with_object({}) { |e, h| h[yield(e)] = e }
|
|
335
|
+
|
|
336
|
+
def index_with(default = nil)
|
|
337
|
+
if block_given?
|
|
338
|
+
each_with_object({}) { |e, h| h[e] = yield(e) }
|
|
339
|
+
else
|
|
340
|
+
each_with_object({}) { |e, h| h[e] = default }
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def excluding(*elements) = reject { |e| elements.include?(e) }
|
|
345
|
+
|
|
346
|
+
def without(*elements) = excluding(*elements)
|
|
347
|
+
|
|
348
|
+
def including(*elements) = to_a + elements
|
|
349
|
+
|
|
350
|
+
def pluck(*keys)
|
|
351
|
+
if keys.one?
|
|
352
|
+
key = keys.first
|
|
353
|
+
map { |e| e.respond_to?(key) ? e.send(key) : e[key] }
|
|
354
|
+
else
|
|
355
|
+
map { |e| keys.map { |k| e.respond_to?(k) ? e.send(k) : e[k] } }
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
module ArrayMethods
|
|
361
|
+
def second = self[1]
|
|
362
|
+
def third = self[2]
|
|
363
|
+
def fourth = self[3]
|
|
364
|
+
def fifth = self[4]
|
|
365
|
+
def second_to_last = self[-2]
|
|
366
|
+
def third_to_last = self[-3]
|
|
367
|
+
|
|
368
|
+
def to_sentence(connector: ", ", last_connector: ", and ")
|
|
369
|
+
case size
|
|
370
|
+
when 0 then ""
|
|
371
|
+
when 1 then first.to_s
|
|
372
|
+
when 2 then "#{first}#{last_connector.sub(/^, /, ' ')}#{second}"
|
|
373
|
+
else
|
|
374
|
+
"#{self[0..-2].join(connector)}#{last_connector}#{last}"
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def in_groups_of(n, fill_with = nil)
|
|
379
|
+
arr = dup
|
|
380
|
+
if fill_with && (remainder = arr.size % n) > 0
|
|
381
|
+
arr.concat(Array.new(n - remainder, fill_with))
|
|
382
|
+
end
|
|
383
|
+
arr.each_slice(n).to_a
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def in_groups(n, fill_with = nil)
|
|
387
|
+
division = size.div(n)
|
|
388
|
+
modulo = size % n
|
|
389
|
+
groups = []
|
|
390
|
+
start = 0
|
|
391
|
+
|
|
392
|
+
n.times do |i|
|
|
393
|
+
length = division + (modulo > 0 && modulo > i ? 1 : 0)
|
|
394
|
+
groups << slice(start, length)
|
|
395
|
+
groups.last << fill_with if fill_with && groups.last.size < division + 1
|
|
396
|
+
start += length
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
groups
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def extract_options! = last.is_a?(Hash) ? pop : {}
|
|
403
|
+
|
|
404
|
+
def deep_dup
|
|
405
|
+
map do |e|
|
|
406
|
+
e.respond_to?(:deep_dup) ? e.deep_dup : e.dup
|
|
407
|
+
rescue StandardError
|
|
408
|
+
e
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
module HashMethods
|
|
414
|
+
def deep_dup
|
|
415
|
+
each_with_object({}) do |(k, v), h|
|
|
416
|
+
h[if k.respond_to?(:deep_dup)
|
|
417
|
+
k.deep_dup
|
|
418
|
+
else
|
|
419
|
+
begin
|
|
420
|
+
k.dup
|
|
421
|
+
rescue StandardError
|
|
422
|
+
k
|
|
423
|
+
end
|
|
424
|
+
end] =
|
|
425
|
+
if v.respond_to?(:deep_dup)
|
|
426
|
+
v.deep_dup
|
|
427
|
+
else
|
|
428
|
+
begin
|
|
429
|
+
v.dup
|
|
430
|
+
rescue StandardError
|
|
431
|
+
v
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def deep_merge(other, &) = dup.deep_merge!(other, &)
|
|
438
|
+
|
|
439
|
+
def deep_merge!(other, &block)
|
|
440
|
+
other.each do |k, v|
|
|
441
|
+
self[k] = if self[k].is_a?(Hash) && v.is_a?(Hash)
|
|
442
|
+
self[k].deep_merge(v, &block)
|
|
443
|
+
elsif block_given?
|
|
444
|
+
yield(k, self[k], v)
|
|
445
|
+
else
|
|
446
|
+
v
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
self
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def deep_stringify_keys = transform_keys_recursive(&:to_s)
|
|
453
|
+
|
|
454
|
+
def deep_symbolize_keys = transform_keys_recursive { |k| k.respond_to?(:to_sym) ? k.to_sym : k }
|
|
455
|
+
|
|
456
|
+
def assert_valid_keys(*valid_keys)
|
|
457
|
+
valid_keys = valid_keys.flatten
|
|
458
|
+
each_key do |k|
|
|
459
|
+
unless valid_keys.include?(k)
|
|
460
|
+
raise ArgumentError, "Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}"
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
self
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def stable_compact
|
|
467
|
+
compact
|
|
468
|
+
.transform_values do |v|
|
|
469
|
+
case v
|
|
470
|
+
when Hash then v.stable_compact
|
|
471
|
+
when Array then v.map { |e| e.respond_to?(:stable_compact) ? e.stable_compact : e }
|
|
472
|
+
else v
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
.sort_by { |k, _| k.to_s }
|
|
476
|
+
.to_h
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def to_sig
|
|
480
|
+
require "digest"
|
|
481
|
+
Digest::SHA1.hexdigest(stable_compact.inspect)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def transform_keys_recursive(&block)
|
|
485
|
+
each_with_object({}) do |(k, v), h|
|
|
486
|
+
new_key = yield(k)
|
|
487
|
+
h[new_key] = case v
|
|
488
|
+
when Hash then v.transform_keys_recursive(&block)
|
|
489
|
+
when Array then v.map { |e| e.is_a?(Hash) ? e.transform_keys_recursive(&block) : e }
|
|
490
|
+
else v
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Ordinal suffixes (defined outside refinement to avoid warning)
|
|
497
|
+
ORDINALS = { 1 => "st", 2 => "nd", 3 => "rd" }.freeze
|
|
498
|
+
|
|
499
|
+
module IntegerMethods
|
|
500
|
+
def ordinal
|
|
501
|
+
abs_mod_100 = abs % 100
|
|
502
|
+
if (11..13).cover?(abs_mod_100)
|
|
503
|
+
"th"
|
|
504
|
+
else
|
|
505
|
+
CoreExt::ORDINALS.fetch(abs % 10, "th")
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def ordinalize = "#{self}#{ordinal}"
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# ─────────────────────────────────────────────────────────────
|
|
513
|
+
# Refinements
|
|
514
|
+
# Use import_methods (Ruby 3.1+) instead of include (removed in 3.2)
|
|
515
|
+
# ─────────────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
refine Object do
|
|
518
|
+
import_methods ObjectMethods
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
refine NilClass do
|
|
522
|
+
import_methods NilMethods
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
refine FalseClass do
|
|
526
|
+
import_methods FalseMethods
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
refine TrueClass do
|
|
530
|
+
import_methods TrueMethods
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
refine Numeric do
|
|
534
|
+
import_methods NumericMethods
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
refine Array do
|
|
538
|
+
import_methods ArrayBlankMethods
|
|
539
|
+
import_methods ArrayMethods
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
refine Hash do
|
|
543
|
+
import_methods HashBlankMethods
|
|
544
|
+
import_methods HashMethods
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
refine String do
|
|
548
|
+
import_methods StringMethods
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
refine Enumerable do
|
|
552
|
+
import_methods EnumerableMethods
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
refine Integer do
|
|
556
|
+
import_methods IntegerMethods
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Global loading of Devex support extensions.
|
|
4
|
+
#
|
|
5
|
+
# This file monkey-patches core classes with the extensions from CoreExt.
|
|
6
|
+
# Use this in CLI tools where you want the extensions available everywhere.
|
|
7
|
+
#
|
|
8
|
+
# For library code, prefer refinements:
|
|
9
|
+
# using Devex::Support::CoreExt
|
|
10
|
+
#
|
|
11
|
+
# For CLI tools:
|
|
12
|
+
# require "devex/support/global"
|
|
13
|
+
#
|
|
14
|
+
|
|
15
|
+
require_relative "../support"
|
|
16
|
+
require_relative "core_ext"
|
|
17
|
+
|
|
18
|
+
module Devex
|
|
19
|
+
module Support
|
|
20
|
+
module Global
|
|
21
|
+
class << self
|
|
22
|
+
def load!
|
|
23
|
+
return if @loaded
|
|
24
|
+
|
|
25
|
+
# Include the shared implementation modules into core classes
|
|
26
|
+
Object.include CoreExt::ObjectMethods
|
|
27
|
+
NilClass.include CoreExt::NilMethods
|
|
28
|
+
FalseClass.include CoreExt::FalseMethods
|
|
29
|
+
TrueClass.include CoreExt::TrueMethods
|
|
30
|
+
Numeric.include CoreExt::NumericMethods
|
|
31
|
+
|
|
32
|
+
Array.include CoreExt::ArrayBlankMethods
|
|
33
|
+
Array.include CoreExt::ArrayMethods
|
|
34
|
+
|
|
35
|
+
Hash.include CoreExt::HashBlankMethods
|
|
36
|
+
Hash.include CoreExt::HashMethods
|
|
37
|
+
|
|
38
|
+
String.include CoreExt::StringMethods
|
|
39
|
+
|
|
40
|
+
# Enumerable goes on the module, affects all including classes
|
|
41
|
+
Enumerable.module_eval { include CoreExt::EnumerableMethods }
|
|
42
|
+
|
|
43
|
+
Integer.include CoreExt::IntegerMethods
|
|
44
|
+
|
|
45
|
+
# Also add ANSI string methods
|
|
46
|
+
add_ansi_string_methods!
|
|
47
|
+
|
|
48
|
+
@loaded = true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def add_ansi_string_methods!
|
|
54
|
+
String.class_eval do
|
|
55
|
+
def ansi(*styles, bg: nil) = Devex::Support::ANSI[self, *styles, bg: bg]
|
|
56
|
+
|
|
57
|
+
def strip_ansi = Devex::Support::ANSI.strip(self)
|
|
58
|
+
|
|
59
|
+
def visible_length = Devex::Support::ANSI.visible_length(self)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Auto-load when required
|
|
68
|
+
Devex::Support::Global.load!
|