tins 1.43.0 → 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.contexts/code_comment.rb +5 -8
- data/.contexts/lib.rb +0 -2
- data/CHANGES.md +12 -0
- data/README.md +158 -6
- data/Rakefile +19 -16
- data/examples/let.rb +8 -40
- data/examples/mail.rb +0 -1
- data/examples/turing.rb +3 -1
- data/lib/tins/alias.rb +1 -0
- data/lib/tins/annotate.rb +37 -27
- data/lib/tins/ask_and_send.rb +41 -0
- data/lib/tins/attempt.rb +39 -0
- data/lib/tins/bijection.rb +34 -0
- data/lib/tins/case_predicate.rb +21 -0
- data/lib/tins/complete.rb +16 -0
- data/lib/tins/concern.rb +64 -0
- data/lib/tins/date_dummy.rb +36 -4
- data/lib/tins/date_time_dummy.rb +34 -2
- data/lib/tins/deep_dup.rb +9 -2
- data/lib/tins/deprecate.rb +12 -0
- data/lib/tins/dslkit.rb +559 -83
- data/lib/tins/duration.rb +120 -5
- data/lib/tins/expose.rb +54 -5
- data/lib/tins/extract_last_argument_options.rb +9 -0
- data/lib/tins/file_binary.rb +104 -21
- data/lib/tins/find.rb +114 -11
- data/lib/tins/generator.rb +10 -2
- data/lib/tins/go.rb +81 -4
- data/lib/tins/hash_bfs.rb +4 -2
- data/lib/tins/hash_symbolize_keys_recursive.rb +62 -4
- data/lib/tins/hash_union.rb +47 -2
- data/lib/tins/if_predicate.rb +31 -0
- data/lib/tins/implement.rb +50 -0
- data/lib/tins/limited.rb +54 -5
- data/lib/tins/lines_file.rb +81 -2
- data/lib/tins/lru_cache.rb +54 -17
- data/lib/tins/memoize.rb +86 -58
- data/lib/tins/method_description.rb +87 -4
- data/lib/tins/minimize.rb +39 -11
- data/lib/tins/module_group.rb +27 -2
- data/lib/tins/named_set.rb +20 -0
- data/lib/tins/null.rb +86 -15
- data/lib/tins/once.rb +61 -4
- data/lib/tins/p.rb +44 -8
- data/lib/tins/partial_application.rb +66 -7
- data/lib/tins/proc_compose.rb +58 -1
- data/lib/tins/proc_prelude.rb +97 -10
- data/lib/tins/range_plus.rb +30 -2
- data/lib/tins/require_maybe.rb +36 -0
- data/lib/tins/responding.rb +39 -0
- data/lib/tins/secure_write.rb +24 -4
- data/lib/tins/sexy_singleton.rb +45 -48
- data/lib/tins/string_byte_order_mark.rb +33 -2
- data/lib/tins/string_camelize.rb +31 -2
- data/lib/tins/string_underscore.rb +33 -2
- data/lib/tins/string_version.rb +179 -10
- data/lib/tins/subhash.rb +35 -10
- data/lib/tins/temp_io.rb +7 -0
- data/lib/tins/temp_io_enum.rb +19 -0
- data/lib/tins/terminal.rb +31 -9
- data/lib/tins/thread_local.rb +67 -5
- data/lib/tins/time_dummy.rb +46 -21
- data/lib/tins/to.rb +15 -0
- data/lib/tins/to_proc.rb +17 -4
- data/lib/tins/token.rb +56 -1
- data/lib/tins/unit.rb +288 -149
- data/lib/tins/version.rb +1 -1
- data/lib/tins/write.rb +14 -3
- data/lib/tins/xt/blank.rb +81 -2
- data/lib/tins/xt/concern.rb +51 -0
- data/lib/tins/xt/full.rb +56 -11
- data/lib/tins/xt/irb.rb +46 -2
- data/lib/tins/xt/method_description.rb +0 -12
- data/lib/tins/xt/minimize.rb +7 -0
- data/lib/tins/xt/named.rb +71 -16
- data/lib/tins/xt/proc_compose.rb +4 -0
- data/lib/tins/xt/subhash.rb +11 -0
- data/lib/tins/xt/time_freezer.rb +43 -6
- data/lib/tins/xt.rb +1 -3
- data/lib/tins.rb +16 -3
- data/tests/duration_test.rb +4 -0
- data/tests/from_module_test.rb +30 -2
- data/tests/implement_test.rb +6 -8
- data/tests/lines_file_test.rb +2 -0
- data/tests/lru_cache_test.rb +12 -0
- data/tests/method_description_test.rb +14 -20
- data/tests/partial_application_test.rb +4 -0
- data/tests/proc_prelude_test.rb +1 -1
- data/tests/scope_test.rb +1 -1
- data/tests/string_version_test.rb +2 -0
- data/tests/to_test.rb +6 -6
- data/tins.gemspec +9 -9
- metadata +23 -41
- data/lib/tins/count_by.rb +0 -21
- data/lib/tins/deep_const_get.rb +0 -64
- data/lib/tins/timed_cache.rb +0 -51
- data/lib/tins/uniq_by.rb +0 -23
- data/lib/tins/xt/count_by.rb +0 -7
- data/lib/tins/xt/deep_const_get.rb +0 -7
- data/lib/tins/xt/uniq_by.rb +0 -25
- data/tests/count_by_test.rb +0 -17
- data/tests/deep_const_get_test.rb +0 -37
- data/tests/uniq_by_test.rb +0 -31
- /data/{COPYING → LICENSE} +0 -0
data/lib/tins/find.rb
CHANGED
@@ -3,57 +3,116 @@ require 'pathname'
|
|
3
3
|
require 'tins/module_group'
|
4
4
|
|
5
5
|
module Tins
|
6
|
+
# This module provides file system traversal functionality with support for
|
7
|
+
# filtering by file type, handling of hidden files, and error management.
|
8
|
+
#
|
9
|
+
# The Find module implements a depth-first search algorithm that traverses
|
10
|
+
# directory trees, yielding each path to the provided block. It handles
|
11
|
+
# various edge cases including symbolic links, permission errors, and
|
12
|
+
# circular references.
|
13
|
+
#
|
14
|
+
# @example Basic usage
|
15
|
+
# Tins::Find.find('/path/to/directory') do |path|
|
16
|
+
# puts path
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @example Find files with specific extension
|
20
|
+
# Tins::Find.find('/path/to/directory', suffix: 'rb') do |path|
|
21
|
+
# puts path
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# @example Skip directories and files
|
25
|
+
# Tins::Find.find('/path/to/directory') do |path|
|
26
|
+
# if File.directory?(path)
|
27
|
+
# Tins::Find.prune # Skip this directory and its contents
|
28
|
+
# else
|
29
|
+
# puts path
|
30
|
+
# end
|
31
|
+
# end
|
6
32
|
module Find
|
33
|
+
# Standard errors that are expected during file system operations
|
34
|
+
# and will be silently handled unless raise_errors is enabled
|
7
35
|
EXPECTED_STANDARD_ERRORS = ModuleGroup[
|
8
36
|
Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP,
|
9
37
|
Errno::ENAMETOOLONG
|
10
38
|
]
|
11
39
|
|
40
|
+
# The Finder class implements the core file system traversal logic.
|
41
|
+
# It handles path processing, error management, and directory traversal.
|
12
42
|
class Finder
|
43
|
+
# Extension module that adds convenience methods to Pathname objects
|
44
|
+
# during the find operation. These methods provide access to finder
|
45
|
+
# functionality and handle errors gracefully.
|
13
46
|
module PathExtension
|
14
47
|
attr_accessor :finder
|
15
48
|
|
49
|
+
# Gets the stat information for this path, handling errors appropriately
|
50
|
+
# @return [File::Stat, nil] File statistics or nil on error
|
16
51
|
def finder_stat
|
17
52
|
finder.protect_from_errors do
|
18
53
|
finder.follow_symlinks ? File.stat(self) : File.lstat(self)
|
19
54
|
end
|
20
55
|
end
|
21
56
|
|
57
|
+
# Opens the file if it's a regular file
|
58
|
+
# @return [File, nil] File object or nil if not a file
|
22
59
|
def file
|
23
60
|
finder.protect_from_errors do
|
24
61
|
File.new(self) if file?
|
25
62
|
end
|
26
63
|
end
|
27
64
|
|
65
|
+
# Checks if this path represents a regular file
|
66
|
+
# @return [Boolean] true if the path is a regular file
|
28
67
|
def file?
|
29
68
|
finder.protect_from_errors { s = finder_stat and s.file? }
|
30
69
|
end
|
31
70
|
|
71
|
+
# Checks if this path represents a directory
|
72
|
+
# @return [Boolean] true if the path is a directory
|
32
73
|
def directory?
|
33
74
|
finder.protect_from_errors { s = finder_stat and s.directory? }
|
34
75
|
end
|
35
76
|
|
77
|
+
# Checks if this path exists
|
78
|
+
# @return [Boolean] true if the path exists
|
36
79
|
def exist?
|
37
80
|
finder.protect_from_errors { File.exist?(self) }
|
38
81
|
end
|
39
82
|
|
83
|
+
# Gets the stat information for this path (follows symlinks)
|
84
|
+
# @return [File::Stat, nil] File statistics or nil on error
|
40
85
|
def stat
|
41
86
|
finder.protect_from_errors { File.stat(self) }
|
42
87
|
end
|
43
88
|
|
89
|
+
# Gets the stat information for this path (does not follow symlinks)
|
90
|
+
# @return [File::Stat, nil] File statistics or nil on error
|
44
91
|
def lstat
|
45
92
|
finder.protect_from_errors { File.lstat(self) }
|
46
93
|
end
|
47
94
|
|
95
|
+
# Creates a Pathname object from this path
|
96
|
+
# @return [Pathname] Pathname object for this path
|
48
97
|
def pathname
|
49
98
|
Pathname.new(self)
|
50
99
|
end
|
51
100
|
|
101
|
+
# Gets the file extension without the leading dot
|
102
|
+
# @return [String] File extension or empty string if no extension
|
52
103
|
def suffix
|
53
104
|
pathname.extname[1..-1] || ''
|
54
105
|
end
|
55
106
|
end
|
56
107
|
|
108
|
+
# Initializes a new Finder instance with specified options
|
109
|
+
#
|
110
|
+
# @param opts [Hash] Configuration options
|
111
|
+
# @option opts [Boolean] :show_hidden (true) Whether to include hidden files/directories
|
112
|
+
# @option opts [Boolean] :raise_errors (false) Whether to raise exceptions on errors
|
113
|
+
# @option opts [Boolean] :follow_symlinks (true) Whether to follow symbolic links
|
114
|
+
# @option opts [Array<String, Symbol>] :suffix (nil) Filter by file extension(s)
|
115
|
+
# @option opts [Proc] :visit (nil) Custom filter predicate
|
57
116
|
def initialize(opts = {})
|
58
117
|
@show_hidden = opts.fetch(:show_hidden) { true }
|
59
118
|
@raise_errors = opts.fetch(:raise_errors) { false }
|
@@ -68,14 +127,26 @@ module Tins
|
|
68
127
|
end
|
69
128
|
end
|
70
129
|
|
130
|
+
# Controls whether hidden files and directories are included in the search
|
131
|
+
# @return [Boolean]
|
71
132
|
attr_accessor :show_hidden
|
72
133
|
|
134
|
+
# Controls whether errors during file system operations should be raised
|
135
|
+
# @return [Boolean]
|
73
136
|
attr_accessor :raise_errors
|
74
137
|
|
138
|
+
# Controls whether symbolic links should be followed during traversal
|
139
|
+
# @return [Boolean]
|
75
140
|
attr_accessor :follow_symlinks
|
76
141
|
|
142
|
+
# The file suffix filter, if specified
|
143
|
+
# @return [Array<String>] Array of allowed file extensions
|
77
144
|
attr_accessor :suffix
|
78
145
|
|
146
|
+
# Determines if a path should be visited based on the configured filters
|
147
|
+
#
|
148
|
+
# @param path [String] The path to check
|
149
|
+
# @return [Boolean] true if the path should be visited
|
79
150
|
def visit_path?(path)
|
80
151
|
if !defined?(@visit) || @visit.nil?
|
81
152
|
true
|
@@ -84,6 +155,11 @@ module Tins
|
|
84
155
|
end
|
85
156
|
end
|
86
157
|
|
158
|
+
# Performs a depth-first search of the specified paths
|
159
|
+
#
|
160
|
+
# @param paths [Array<String>] The root paths to search
|
161
|
+
# @yield [String] Each path that matches the criteria
|
162
|
+
# @return [Enumerator] If no block is given, returns an enumerator
|
87
163
|
def find(*paths)
|
88
164
|
block_given? or return enum_for(__method__, *paths)
|
89
165
|
paths.collect! { |d| d.dup }
|
@@ -106,6 +182,10 @@ module Tins
|
|
106
182
|
end
|
107
183
|
end
|
108
184
|
|
185
|
+
# Prepares a path for processing by extending it with PathExtension
|
186
|
+
#
|
187
|
+
# @param path [String] The path to prepare
|
188
|
+
# @return [String] The prepared path object
|
109
189
|
def prepare_path(path)
|
110
190
|
path = path.dup
|
111
191
|
path.extend PathExtension
|
@@ -113,6 +193,11 @@ module Tins
|
|
113
193
|
path
|
114
194
|
end
|
115
195
|
|
196
|
+
# Executes a block while protecting against expected standard errors
|
197
|
+
#
|
198
|
+
# @param errors [Array<Class>] Array of error classes to catch
|
199
|
+
# @yield [] the block to be protected
|
200
|
+
# @return [Object, nil] The result of the block or nil on error
|
116
201
|
def protect_from_errors(errors = Find::EXPECTED_STANDARD_ERRORS)
|
117
202
|
yield
|
118
203
|
rescue errors
|
@@ -121,25 +206,43 @@ module Tins
|
|
121
206
|
end
|
122
207
|
end
|
123
208
|
|
209
|
+
# Performs a depth-first search of the specified paths, yielding each
|
210
|
+
# matching path to the block
|
124
211
|
#
|
125
|
-
#
|
126
|
-
#
|
212
|
+
# @param paths [Array<String>] The root paths to search
|
213
|
+
# @param opts [Hash] Configuration options
|
214
|
+
# @option opts [Boolean] :show_hidden (true) Whether to include hidden files/directories
|
215
|
+
# @option opts [Boolean] :raise_errors (false) Whether to raise exceptions on errors
|
216
|
+
# @option opts [Boolean] :follow_symlinks (true) Whether to follow symbolic links
|
217
|
+
# @option opts [Array<String, Symbol>] :suffix (nil) Filter by file extension(s)
|
218
|
+
# @option opts [Proc] :visit (nil) Custom filter predicate
|
219
|
+
# @yield [String] Each path that matches the criteria
|
220
|
+
# @return [Enumerator] If no block is given, returns an enumerator
|
127
221
|
#
|
128
|
-
#
|
222
|
+
# @example Basic usage
|
223
|
+
# Tins::Find.find('/path/to/directory') do |path|
|
224
|
+
# puts path
|
225
|
+
# end
|
129
226
|
#
|
130
|
-
|
131
|
-
|
227
|
+
# @example Find only Ruby files
|
228
|
+
# Tins::Find.find('/path/to/directory', suffix: 'rb') do |path|
|
229
|
+
# puts path
|
230
|
+
# end
|
231
|
+
def find(*paths, **opts, &block)
|
132
232
|
Finder.new(opts).find(*paths, &block)
|
133
233
|
end
|
134
234
|
|
135
|
-
#
|
136
235
|
# Skips the current path or directory, restarting the loop with the next
|
137
|
-
# entry.
|
138
|
-
# recursively entered. Meaningful only within the block associated with
|
139
|
-
# Find::find.
|
140
|
-
#
|
141
|
-
# See the +Find+ module documentation for an example.
|
236
|
+
# entry. Meaningful only within the block associated with Find.find.
|
142
237
|
#
|
238
|
+
# @example Skip directories
|
239
|
+
# Tins::Find.find('/path/to/directory') do |path|
|
240
|
+
# if path.count(?/) < 3
|
241
|
+
# Tins::Find.prune # Skip all paths deeper than 2
|
242
|
+
# else
|
243
|
+
# puts path
|
244
|
+
# end
|
245
|
+
# end
|
143
246
|
def prune
|
144
247
|
throw :prune
|
145
248
|
end
|
data/lib/tins/generator.rb
CHANGED
@@ -35,6 +35,16 @@ module Tins
|
|
35
35
|
self
|
36
36
|
end
|
37
37
|
|
38
|
+
# Recurses through nested enumerators to yield all combinations
|
39
|
+
#
|
40
|
+
# This method performs a recursive traversal of nested enumerators,
|
41
|
+
# building tuples by iterating through each enumerator at its respective
|
42
|
+
# level and yielding complete combinations when the deepest level is
|
43
|
+
# reached
|
44
|
+
#
|
45
|
+
# @param tuple [ Array ] the current tuple being built during recursion
|
46
|
+
# @param i [ Integer ] the current index/level in the recursion
|
47
|
+
# @yield [ Array ] yields a duplicate of the completed tuple
|
38
48
|
def recurse(tuple = [ nil ] * @n, i = 0, &block)
|
39
49
|
if i < @n - 1 then
|
40
50
|
@enums[i].__send__(@iterators[i]) do |x|
|
@@ -64,5 +74,3 @@ module Tins
|
|
64
74
|
end
|
65
75
|
end
|
66
76
|
end
|
67
|
-
|
68
|
-
require 'tins/alias'
|
data/lib/tins/go.rb
CHANGED
@@ -1,17 +1,62 @@
|
|
1
1
|
module Tins
|
2
|
+
# A command-line option parsing library that provides a flexible way to
|
3
|
+
# parse single-character options with optional arguments. It supports
|
4
|
+
# multiple values for the same flag and provides a clean API for handling
|
5
|
+
# command-line interfaces.
|
6
|
+
#
|
7
|
+
# @example Basic usage
|
8
|
+
# # Parse options with pattern 'xy:z'
|
9
|
+
# options = Tins::GO.go('xy:z', ARGV, defaults: { x: true, y: 'default' })
|
10
|
+
# # Handles: -x -y value -z
|
11
|
+
#
|
12
|
+
# @example Multiple values for same option
|
13
|
+
# # Handle: -f foo -f bar -f baz
|
14
|
+
# options = Tins::GO.go('f:', ARGV)
|
15
|
+
# # options['f'] will contain an EnumerableExtension collection with all
|
16
|
+
# values, see `option['f'].to_a`
|
2
17
|
module GO
|
18
|
+
# An extension module that provides Enumerable behavior for collecting
|
19
|
+
# multiple values associated with the same command-line option.
|
20
|
+
#
|
21
|
+
# This extension enables command-line flags like `-f foo -f bar` to be
|
22
|
+
# collected into a single collection that can be queried via #to_a or #each.
|
3
23
|
module EnumerableExtension
|
24
|
+
# Adds an element to the collection.
|
25
|
+
#
|
26
|
+
# This method allows for chaining operations and collects multiple
|
27
|
+
# values for the same command-line option.
|
28
|
+
#
|
29
|
+
# @param argument [Object] The element to add to the collection
|
30
|
+
# @return [self] Returns self to enable method chaining
|
4
31
|
def push(argument)
|
5
32
|
@arguments ||= []
|
6
33
|
@arguments.push argument
|
7
34
|
self
|
8
35
|
end
|
36
|
+
|
37
|
+
# Alias for {#push}
|
38
|
+
#
|
39
|
+
# Enables intuitive syntax for adding elements: `collection << item`
|
40
|
+
#
|
41
|
+
# @see #push
|
9
42
|
alias << push
|
10
43
|
|
44
|
+
# Iterates over each element in the collection.
|
45
|
+
#
|
46
|
+
# Implements the Enumerable interface, allowing the use of all Enumerable
|
47
|
+
# methods like map, select, find, etc.
|
48
|
+
#
|
49
|
+
# @yield [element] Yields each element in the collection
|
50
|
+
# @yieldparam element [Object] Each element in the collection
|
51
|
+
# @return [self] Returns self to enable method chaining
|
11
52
|
def each(&block)
|
12
53
|
@arguments.each(&block)
|
13
54
|
self
|
14
55
|
end
|
56
|
+
|
57
|
+
# Includes the Enumerable module to provide rich iteration capabilities.
|
58
|
+
#
|
59
|
+
# This enables the use of methods like map, select, find, etc.
|
15
60
|
include Enumerable
|
16
61
|
end
|
17
62
|
|
@@ -25,8 +70,42 @@ module Tins
|
|
25
70
|
#
|
26
71
|
# The _defaults_ argument specifies default values for the options.
|
27
72
|
#
|
28
|
-
# An option hash is returned with all found options set to
|
29
|
-
#
|
73
|
+
# An option hash is returned with all found options set to a truthy value
|
74
|
+
# representing the number of times they were encountered, or `false` if not
|
75
|
+
# present. When a default value is specified and the flag is not present,
|
76
|
+
# the default value is used instead.
|
77
|
+
#
|
78
|
+
# @param s [String] Option pattern string where each character represents
|
79
|
+
# an option, and ':' indicates the option requires an argument
|
80
|
+
# @param args [Array<String>] Array of arguments to parse (defaults to ARGV)
|
81
|
+
# @param defaults [Hash{String => Object}] Default values for options
|
82
|
+
# @return [Hash{String => Object}] Hash mapping option names to their values
|
83
|
+
#
|
84
|
+
# @example Basic usage
|
85
|
+
# # Parse options with pattern 'xy:z'
|
86
|
+
# options = Tins::GO.go('xy:z', ARGV, defaults: { x: true, y: 'default' })
|
87
|
+
# # Handles: -x -y value -z
|
88
|
+
#
|
89
|
+
# @example Multiple values for same option
|
90
|
+
# # Handle: -f foo -f bar -f baz
|
91
|
+
# options = Tins::GO.go('f:', ARGV)
|
92
|
+
# # options['f'] will contain an EnumerableExtension collection with
|
93
|
+
# # all values, see options['f'].to_a
|
94
|
+
#
|
95
|
+
# @example Boolean flag counting
|
96
|
+
# # Handle: -x -x -x
|
97
|
+
# options = Tins::GO.go('x', ARGV)
|
98
|
+
# # options['x'] will be 3 (truthy numeric value)
|
99
|
+
#
|
100
|
+
# @example Boolean flag not present
|
101
|
+
# # Handle: no -x flag
|
102
|
+
# options = Tins::GO.go('x', ARGV)
|
103
|
+
# # options['x'] will be false
|
104
|
+
#
|
105
|
+
# @example Disabling options with default values
|
106
|
+
# # Handle: ~x (disables -x option) when x has a default value
|
107
|
+
# options = Tins::GO.go('x', ARGV, defaults: { x: true })
|
108
|
+
# # options['x'] will be false if no ~x flag is present
|
30
109
|
def go(s, args = ARGV, defaults: {})
|
31
110
|
d = defaults || {}
|
32
111
|
b, v = s.scan(/(.)(:?)/).inject([ {}, {} ]) { |t, (o, a)|
|
@@ -92,5 +171,3 @@ module Tins
|
|
92
171
|
end
|
93
172
|
end
|
94
173
|
end
|
95
|
-
|
96
|
-
require 'tins/alias'
|
data/lib/tins/hash_bfs.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
require 'tins/thread_local'
|
2
2
|
|
3
3
|
module Tins
|
4
|
+
# HashBFS module for breadth-first traversal of hash structures
|
5
|
+
#
|
6
|
+
# Provides methods to traverse hash structures in a breadth-first manner,
|
7
|
+
# visiting all keys and values while maintaining the order of traversal.
|
4
8
|
module HashBFS
|
5
9
|
extend Tins::ThreadLocal
|
6
10
|
|
@@ -63,5 +67,3 @@ module Tins
|
|
63
67
|
end
|
64
68
|
end
|
65
69
|
end
|
66
|
-
|
67
|
-
require 'tins/alias'
|
@@ -1,11 +1,52 @@
|
|
1
1
|
require 'tins/thread_local'
|
2
2
|
|
3
3
|
module Tins
|
4
|
+
# This module provides recursive symbolization of hash keys. It handles
|
5
|
+
# nested structures including hashes and arrays, with special handling for
|
6
|
+
# circular references to prevent infinite recursion.
|
7
|
+
#
|
8
|
+
# @example Basic usage
|
9
|
+
# hash = { "name" => "John", "address" => { "street" => "123 Main St" } }
|
10
|
+
# hash.symbolize_keys_recursive
|
11
|
+
# # => { name: "John", address: { street: "123 Main St" } }
|
12
|
+
#
|
13
|
+
# @example Handling circular references
|
14
|
+
# hash = { "name" => "John" }
|
15
|
+
# hash["self"] = hash # Circular reference
|
16
|
+
# hash.symbolize_keys_recursive(circular: "[Circular Reference]")
|
17
|
+
# # => { name: "John", self: "[Circular Reference]" }
|
4
18
|
module HashSymbolizeKeysRecursive
|
5
19
|
extend Tins::ThreadLocal
|
6
20
|
|
21
|
+
# Thread-local storage for tracking visited objects to handle circular
|
22
|
+
# references
|
7
23
|
thread_local :seen
|
8
24
|
|
25
|
+
# Recursively converts all string keys in a hash (and nested hashes/arrays)
|
26
|
+
# to symbols. This method does not modify the original hash.
|
27
|
+
#
|
28
|
+
# @param circular [Object] The value to use when encountering circular references.
|
29
|
+
# Defaults to nil, which means circular references will be ignored.
|
30
|
+
# @return [Hash, Array, Object] A new hash/array with symbolized keys
|
31
|
+
#
|
32
|
+
# @example Basic usage
|
33
|
+
# { "name" => "John", "age" => 30 }.symbolize_keys_recursive
|
34
|
+
# # => { name: "John", age: 30 }
|
35
|
+
#
|
36
|
+
# @example Nested structures
|
37
|
+
# {
|
38
|
+
# "user" => {
|
39
|
+
# "name" => "John",
|
40
|
+
# "hobbies" => ["reading", "swimming"]
|
41
|
+
# }
|
42
|
+
# }.symbolize_keys_recursive
|
43
|
+
# # => { user: { name: "John", hobbies: ["reading", "swimming"] } }
|
44
|
+
#
|
45
|
+
# @example Circular reference handling
|
46
|
+
# hash = { "name" => "John" }
|
47
|
+
# hash["self"] = hash
|
48
|
+
# hash.symbolize_keys_recursive(circular: "[Circular]")
|
49
|
+
# # => { name: "John", self: "[Circular]" }
|
9
50
|
def symbolize_keys_recursive(circular: nil)
|
10
51
|
self.seen = {}
|
11
52
|
_symbolize_keys_recursive(self, circular: circular)
|
@@ -13,17 +54,35 @@ module Tins
|
|
13
54
|
self.seen = nil
|
14
55
|
end
|
15
56
|
|
57
|
+
# Recursively converts all string keys in a hash (and nested hashes/arrays)
|
58
|
+
# to symbols. This method modifies the original hash in place.
|
59
|
+
#
|
60
|
+
# @param circular [Object] The value to use when encountering circular references.
|
61
|
+
# Defaults to nil, which means circular references will be ignored.
|
62
|
+
# @return [Hash, Array, Object] The same hash/array with symbolized keys
|
63
|
+
#
|
64
|
+
# @example Basic usage
|
65
|
+
# hash = { "name" => "John", "age" => 30 }
|
66
|
+
# hash.symbolize_keys_recursive!
|
67
|
+
# # => { name: "John", age: 30 }
|
68
|
+
# # hash is now modified in place
|
16
69
|
def symbolize_keys_recursive!(circular: nil)
|
17
70
|
replace symbolize_keys_recursive(circular: circular)
|
18
71
|
end
|
19
72
|
|
20
73
|
private
|
21
74
|
|
75
|
+
# Performs the actual recursive symbolization work
|
76
|
+
#
|
77
|
+
# @param object [Object] The object to process
|
78
|
+
# @param circular [Object] The value to return for circular references
|
79
|
+
# @return [Object] The processed object with symbolized keys
|
22
80
|
def _symbolize_keys_recursive(object, circular: nil)
|
23
81
|
case
|
24
82
|
when seen[object.__id__]
|
25
83
|
object = circular
|
26
|
-
when
|
84
|
+
when object.respond_to?(:to_hash)
|
85
|
+
object = object.to_hash
|
27
86
|
seen[object.__id__] = true
|
28
87
|
new_object = object.class.new
|
29
88
|
seen[new_object.__id__] = true
|
@@ -31,7 +90,8 @@ module Tins
|
|
31
90
|
new_object[k.to_s.to_sym] = _symbolize_keys_recursive(v, circular: circular)
|
32
91
|
end
|
33
92
|
object = new_object
|
34
|
-
when
|
93
|
+
when object.respond_to?(:to_ary)
|
94
|
+
object = object.to_ary
|
35
95
|
seen[object.__id__] = true
|
36
96
|
new_object = object.class.new(object.size)
|
37
97
|
seen[new_object.__id__] = true
|
@@ -44,5 +104,3 @@ module Tins
|
|
44
104
|
end
|
45
105
|
end
|
46
106
|
end
|
47
|
-
|
48
|
-
require 'tins/alias'
|
data/lib/tins/hash_union.rb
CHANGED
@@ -1,5 +1,52 @@
|
|
1
1
|
module Tins
|
2
|
+
# A module that provides union functionality for hash-like objects
|
3
|
+
#
|
4
|
+
# This module implements the | (pipe) operator for hashes, allowing them to be
|
5
|
+
# merged with other hash-like objects. The merge gives precedence to values from
|
6
|
+
# the other object, making it useful for configuration merging where default
|
7
|
+
# values should be overridden by user-provided options.
|
2
8
|
module HashUnion
|
9
|
+
# Implements the | (union) operator for hash-like objects.
|
10
|
+
#
|
11
|
+
# Merges another hash-like object into this object, with the other taking
|
12
|
+
# precedence over self. This is useful for configuration merging where
|
13
|
+
# default values should be overridden by user-provided options.
|
14
|
+
#
|
15
|
+
# @example Basic usage
|
16
|
+
# h1 = { a: 1, b: 2 }
|
17
|
+
# h2 = { b: 3, c: 4 }
|
18
|
+
# result = h1 | h2
|
19
|
+
# # => { a: 1, b: 3, c: 4 } # h2 values take precedence
|
20
|
+
#
|
21
|
+
# @example Configuration merging
|
22
|
+
# default_options = {
|
23
|
+
# format: :json,
|
24
|
+
# timeout: 30,
|
25
|
+
# retries: 3
|
26
|
+
# }
|
27
|
+
#
|
28
|
+
# user_options = { timeout: 60 }
|
29
|
+
# options = user_options | default_options
|
30
|
+
# # => { format: :json, timeout: 60, retries: 3 }
|
31
|
+
#
|
32
|
+
# @example With objects that respond to to_hash
|
33
|
+
# class CustomHash
|
34
|
+
# def to_hash
|
35
|
+
# { x: 10, y: 20 }
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# custom = CustomHash.new
|
40
|
+
# result = { a: 1 } | custom
|
41
|
+
# # => { a: 1, x: 10, y: 20 }
|
42
|
+
#
|
43
|
+
# @param other [Hash, Object] Another hash-like object to merge with.
|
44
|
+
# Can be a Hash, or any object that responds to either `to_hash` or `to_h`.
|
45
|
+
# @return [Hash] A new hash containing the merged key-value pairs
|
46
|
+
#
|
47
|
+
# @note The merge operation preserves the original hashes and returns a new
|
48
|
+
# hash. In case of duplicate keys, values from `other` will overwrite
|
49
|
+
# values from `self`.
|
3
50
|
def |(other)
|
4
51
|
case
|
5
52
|
when other.respond_to?(:to_hash)
|
@@ -11,5 +58,3 @@ module Tins
|
|
11
58
|
end
|
12
59
|
end
|
13
60
|
end
|
14
|
-
|
15
|
-
require 'tins/alias'
|
data/lib/tins/if_predicate.rb
CHANGED
@@ -1,5 +1,36 @@
|
|
1
1
|
module Tins
|
2
|
+
# A module that provides a predicate method for checking if a value is
|
3
|
+
# truthy.
|
4
|
+
#
|
5
|
+
# The IfPredicate module adds a #if? method to objects that returns true if
|
6
|
+
# the object is truthy, and false if it is falsy. This is useful for
|
7
|
+
# conditional logic where you want to check if
|
8
|
+
# an object evaluates to true in a boolean context.
|
2
9
|
module IfPredicate
|
10
|
+
# A predicate method that returns the receiver if it's truthy,
|
11
|
+
# or nil if it's falsy.
|
12
|
+
#
|
13
|
+
# This method is designed to work in conditional expressions
|
14
|
+
# where you want to check if a value is truthy and either
|
15
|
+
# return it or handle the falsy case.
|
16
|
+
#
|
17
|
+
# @example Basic usage
|
18
|
+
# true.if? # => true
|
19
|
+
# false.if? # => nil
|
20
|
+
# nil.if? # => nil
|
21
|
+
# "hello".if? # => "hello"
|
22
|
+
# "".if? # => ""
|
23
|
+
#
|
24
|
+
# @example With default values
|
25
|
+
# user = nil
|
26
|
+
# name = user.if? || "Anonymous"
|
27
|
+
# # => "Anonymous"
|
28
|
+
#
|
29
|
+
# user = "John"
|
30
|
+
# name = user.if? || "Anonymous"
|
31
|
+
# # => "John"
|
32
|
+
#
|
33
|
+
# @return [Object, nil] The receiver if truthy, nil if falsy
|
3
34
|
def if?
|
4
35
|
self ? self : nil
|
5
36
|
end
|
data/lib/tins/implement.rb
CHANGED
@@ -1,5 +1,32 @@
|
|
1
1
|
module Tins
|
2
|
+
# Provides methods for defining abstract method implementations that raise
|
3
|
+
# NotImplementedError. Useful for creating interface-like modules or base
|
4
|
+
# classes where certain methods must be implemented by subclasses.
|
5
|
+
#
|
6
|
+
# @example Basic usage
|
7
|
+
# module MyInterface
|
8
|
+
# include Tins::Implement
|
9
|
+
# implement :process
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# class MyClass
|
13
|
+
# include MyInterface
|
14
|
+
# # Must implement process method or it will raise NotImplementedError
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example With custom messages
|
18
|
+
# module ApiInterface
|
19
|
+
# include Tins::Implement
|
20
|
+
# implement :get_data, :subclass
|
21
|
+
# implement :post_data, :submodule
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# @see Tins::MethodDescription For method description integration
|
2
25
|
module Implement
|
26
|
+
# Predefined error message templates for different implementation contexts.
|
27
|
+
#
|
28
|
+
# These templates provide context-specific error messages that help
|
29
|
+
# developers understand where and how methods should be implemented.
|
3
30
|
MESSAGES = {
|
4
31
|
default: 'method %{method_name} not implemented in module %{module}',
|
5
32
|
subclass: 'method %{method_name} has to be implemented in '\
|
@@ -8,6 +35,21 @@ module Tins
|
|
8
35
|
'submodules of %{module}',
|
9
36
|
}
|
10
37
|
|
38
|
+
# Defines an implementation for a method that raises NotImplementedError.
|
39
|
+
#
|
40
|
+
# @param method_name [Symbol, String] The name of the method to implement
|
41
|
+
# @param msg [Symbol, String, Hash] The error message template or options
|
42
|
+
# @option msg [Symbol] :in When msg is a Hash, specifies which message key to use
|
43
|
+
#
|
44
|
+
# @example Basic usage
|
45
|
+
# implement :process
|
46
|
+
# # => Defines process method that raises NotImplementedError
|
47
|
+
#
|
48
|
+
# @example Custom message
|
49
|
+
# implement :calculate, 'Custom error message'
|
50
|
+
#
|
51
|
+
# @example Using predefined message template
|
52
|
+
# implement :validate, :subclass
|
11
53
|
def implement(method_name, msg = :default)
|
12
54
|
method_name.nil? and return
|
13
55
|
case msg
|
@@ -30,6 +72,14 @@ module Tins
|
|
30
72
|
end
|
31
73
|
end
|
32
74
|
|
75
|
+
# Defines an implementation for a method that raises NotImplementedError
|
76
|
+
# specifically for submodule implementations.
|
77
|
+
#
|
78
|
+
# @param method_name [Symbol, String] The name of the method to implement
|
79
|
+
#
|
80
|
+
# @example Usage
|
81
|
+
# implement_in_submodule :render
|
82
|
+
# # => Defines render method with submodule-specific error message
|
33
83
|
def implement_in_submodule(method_name)
|
34
84
|
implement method_name,
|
35
85
|
'method %{method_name} has to be implemented in submodules of'\
|