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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.contexts/code_comment.rb +5 -8
  3. data/.contexts/lib.rb +0 -2
  4. data/CHANGES.md +12 -0
  5. data/README.md +158 -6
  6. data/Rakefile +19 -16
  7. data/examples/let.rb +8 -40
  8. data/examples/mail.rb +0 -1
  9. data/examples/turing.rb +3 -1
  10. data/lib/tins/alias.rb +1 -0
  11. data/lib/tins/annotate.rb +37 -27
  12. data/lib/tins/ask_and_send.rb +41 -0
  13. data/lib/tins/attempt.rb +39 -0
  14. data/lib/tins/bijection.rb +34 -0
  15. data/lib/tins/case_predicate.rb +21 -0
  16. data/lib/tins/complete.rb +16 -0
  17. data/lib/tins/concern.rb +64 -0
  18. data/lib/tins/date_dummy.rb +36 -4
  19. data/lib/tins/date_time_dummy.rb +34 -2
  20. data/lib/tins/deep_dup.rb +9 -2
  21. data/lib/tins/deprecate.rb +12 -0
  22. data/lib/tins/dslkit.rb +559 -83
  23. data/lib/tins/duration.rb +120 -5
  24. data/lib/tins/expose.rb +54 -5
  25. data/lib/tins/extract_last_argument_options.rb +9 -0
  26. data/lib/tins/file_binary.rb +104 -21
  27. data/lib/tins/find.rb +114 -11
  28. data/lib/tins/generator.rb +10 -2
  29. data/lib/tins/go.rb +81 -4
  30. data/lib/tins/hash_bfs.rb +4 -2
  31. data/lib/tins/hash_symbolize_keys_recursive.rb +62 -4
  32. data/lib/tins/hash_union.rb +47 -2
  33. data/lib/tins/if_predicate.rb +31 -0
  34. data/lib/tins/implement.rb +50 -0
  35. data/lib/tins/limited.rb +54 -5
  36. data/lib/tins/lines_file.rb +81 -2
  37. data/lib/tins/lru_cache.rb +54 -17
  38. data/lib/tins/memoize.rb +86 -58
  39. data/lib/tins/method_description.rb +87 -4
  40. data/lib/tins/minimize.rb +39 -11
  41. data/lib/tins/module_group.rb +27 -2
  42. data/lib/tins/named_set.rb +20 -0
  43. data/lib/tins/null.rb +86 -15
  44. data/lib/tins/once.rb +61 -4
  45. data/lib/tins/p.rb +44 -8
  46. data/lib/tins/partial_application.rb +66 -7
  47. data/lib/tins/proc_compose.rb +58 -1
  48. data/lib/tins/proc_prelude.rb +97 -10
  49. data/lib/tins/range_plus.rb +30 -2
  50. data/lib/tins/require_maybe.rb +36 -0
  51. data/lib/tins/responding.rb +39 -0
  52. data/lib/tins/secure_write.rb +24 -4
  53. data/lib/tins/sexy_singleton.rb +45 -48
  54. data/lib/tins/string_byte_order_mark.rb +33 -2
  55. data/lib/tins/string_camelize.rb +31 -2
  56. data/lib/tins/string_underscore.rb +33 -2
  57. data/lib/tins/string_version.rb +179 -10
  58. data/lib/tins/subhash.rb +35 -10
  59. data/lib/tins/temp_io.rb +7 -0
  60. data/lib/tins/temp_io_enum.rb +19 -0
  61. data/lib/tins/terminal.rb +31 -9
  62. data/lib/tins/thread_local.rb +67 -5
  63. data/lib/tins/time_dummy.rb +46 -21
  64. data/lib/tins/to.rb +15 -0
  65. data/lib/tins/to_proc.rb +17 -4
  66. data/lib/tins/token.rb +56 -1
  67. data/lib/tins/unit.rb +288 -149
  68. data/lib/tins/version.rb +1 -1
  69. data/lib/tins/write.rb +14 -3
  70. data/lib/tins/xt/blank.rb +81 -2
  71. data/lib/tins/xt/concern.rb +51 -0
  72. data/lib/tins/xt/full.rb +56 -11
  73. data/lib/tins/xt/irb.rb +46 -2
  74. data/lib/tins/xt/method_description.rb +0 -12
  75. data/lib/tins/xt/minimize.rb +7 -0
  76. data/lib/tins/xt/named.rb +71 -16
  77. data/lib/tins/xt/proc_compose.rb +4 -0
  78. data/lib/tins/xt/subhash.rb +11 -0
  79. data/lib/tins/xt/time_freezer.rb +43 -6
  80. data/lib/tins/xt.rb +1 -3
  81. data/lib/tins.rb +16 -3
  82. data/tests/duration_test.rb +4 -0
  83. data/tests/from_module_test.rb +30 -2
  84. data/tests/implement_test.rb +6 -8
  85. data/tests/lines_file_test.rb +2 -0
  86. data/tests/lru_cache_test.rb +12 -0
  87. data/tests/method_description_test.rb +14 -20
  88. data/tests/partial_application_test.rb +4 -0
  89. data/tests/proc_prelude_test.rb +1 -1
  90. data/tests/scope_test.rb +1 -1
  91. data/tests/string_version_test.rb +2 -0
  92. data/tests/to_test.rb +6 -6
  93. data/tins.gemspec +9 -9
  94. metadata +23 -41
  95. data/lib/tins/count_by.rb +0 -21
  96. data/lib/tins/deep_const_get.rb +0 -64
  97. data/lib/tins/timed_cache.rb +0 -51
  98. data/lib/tins/uniq_by.rb +0 -23
  99. data/lib/tins/xt/count_by.rb +0 -7
  100. data/lib/tins/xt/deep_const_get.rb +0 -7
  101. data/lib/tins/xt/uniq_by.rb +0 -25
  102. data/tests/count_by_test.rb +0 -17
  103. data/tests/deep_const_get_test.rb +0 -37
  104. data/tests/uniq_by_test.rb +0 -31
  105. /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
- # Calls the associated block with the name of every path and directory
126
- # listed as arguments, then recursively on their subdirectories, and so on.
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
- # See the +Find+ module documentation for an example.
222
+ # @example Basic usage
223
+ # Tins::Find.find('/path/to/directory') do |path|
224
+ # puts path
225
+ # end
129
226
  #
130
- def find(*paths, &block) # :yield: path
131
- opts = Hash === paths.last ? paths.pop : {}
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. If the current path is a directory, that directory will not be
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
@@ -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 true or the
29
- # found option argument.
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 Hash === object
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 Array === object
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'
@@ -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'
@@ -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
@@ -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'\