emanlib 0.1.0 → 0.1.2

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: 1325271b5892d5c0195b9169f09416790ea4887f680e194afb2445f33bb57b75
4
- data.tar.gz: effa861c1070d969e2ab86287c101ba90bcd52ad9f82cceb87616d79f3b5a554
3
+ metadata.gz: fd662dbc1918087da06994da20c822e4e3697a07db7f5681602c26c46839ed07
4
+ data.tar.gz: d1e82edbb74f7d2f60dbe906f5715cddc94a28639266107d713744517de52007
5
5
  SHA512:
6
- metadata.gz: 2a5248da79b6d73c42c62932c9a2473d10dee603aa9c40ba880cbe708f5ae450d1fa0861805c507051fa5dd3d777031dbfecd6a2120ac0fe688bc5743ee0eaf5
7
- data.tar.gz: 77a89f7f87e7003134b581f272c4892f61c3cc861371cc7758b21a022463caa8468064b16adafd88b0d70f7b0bf1d431290b90bfe2dda22c16d6ff619d3d0389
6
+ metadata.gz: 3b5a86384a130d687ef886a075b55d1d3ea892ccf656356df78cfee9c9949dbcdaa36691b92733506956b687c5af66baba8fcf6455e3176af3eaac7bdb80c181
7
+ data.tar.gz: 67b7d61d4cf0ad053c3c8e9121c734fe5c03b8a899c17f2b5b08d5c181142f70a494008820fe365f772dbcebb468ea99d14d3d2e2e3d9ea7198ad9b8c9f59482
data/lib/emanlib.rb CHANGED
@@ -1,73 +1,8 @@
1
- require_relative "patch/define"
1
+ require_relative "patch/enum"
2
2
  require_relative "patch/foobar"
3
3
  require_relative "patch/lambda"
4
+ require_relative "patch/let"
4
5
 
5
6
  module EmanLib
6
- # The identity Lambda object (`_`).
7
- # Store in a short variable, and use it as a building block for anonymous functions.
8
- #
9
- # _ = EmanLib.LAMBDA
10
- # [1, 2, 3].map(&_.succ) => [2, 3, 4]
11
- # [[1, 2], [3, 4]].map(&(_ + _).lift) => [3, 7]
12
- LAMBDA = Lambda.new
13
-
14
- # Helper method to create definitions.
15
- # A convenient shorthand for `Object.new.define(...)`.
16
- #
17
- # @param args [Array<Hash, Array>] A list of Hashes or "hashy" Arrays.
18
- # @param block [Proc] If provided, its local variables are used to define methods.
19
- # @return [Object] A new object with dynamically defined methods.
20
- #
21
- # @see [Object#define]
22
- #
23
- # @example
24
- # point = let(x: 10, y: 20)
25
- # puts point.x # => 10
26
- #
27
- # settings = let do
28
- # theme = "dark"
29
- # font_size = 12
30
- # binding
31
- # end
32
- # puts settings.theme # => "dark"
33
- #
34
- # complex_data = let([[:id, "item1"]], name: "Test Item") do
35
- # details = { color: "red", size: "large" }
36
- # binding
37
- # end
38
- #
39
- # puts complex_data.id # => "item1"
40
- # puts complex_data.name # => "Test Item"
41
- # puts complex_data.details.color # => "red"
42
- def let(*args, &block)
43
- Object.new.define(*args, &block)
44
- end
45
-
46
- module_function :let
47
-
48
- # Support for using a `_` as the second operand with operators.
49
- # WARN: This method will MODIFY the standard library classes.
50
- # In particular, the operators: `- * / % ** & | ^ << >> <=> == === != > < >= <=`
51
- # in the classes: `Integer, Float, Rational, Complex, Array, String, Hash, Range, Set`
52
- def support_lambda
53
- [[Integer, Float, Rational, Complex, Array, String, Hash, Range, Set],
54
- %i[- * / % ** & | ^ << >> <=> == === != > < >= <=]].op(:product)
55
- .each do |klass, op|
56
- next unless klass.instance_methods(false).include?(op)
57
-
58
- original = klass.instance_method(op)
59
- klass.define_method(op) do |other|
60
- if other.is_a?(Lambda)
61
- repr = [self]
62
- repr.concat(other.repr)
63
- repr << Lambda::Function.new(op)
64
- Lambda.new(repr)
65
- else
66
- original.bind(self).call(other)
67
- end
68
- end
69
- end
70
- end
71
-
72
- module_function :support_lambda
7
+ VERSION = "0.1.2"
73
8
  end
data/lib/patch/enum.rb ADDED
@@ -0,0 +1,115 @@
1
+ module EmanLib
2
+ # The Enum module provides a factory method `[]` to dynamically create
3
+ # simple, lightweight enum-like classes. These classes allow defining a set of
4
+ # named constants with associated numeric values.
5
+ #
6
+ # Enums are defined by passing symbols, strings, or hashes to the `[]` method.
7
+ #
8
+ # Usage examples:
9
+ # OS = Enum[:Arch, :BSD] # OS.Arch => 0, OS.BSD => 1
10
+ # Bool = Enum[nil: 1, :t 10] # Bool.nil => 1, Bool.t => 10
11
+ # Way = Enum["↑", { ↓: 50 }, :→ ] # Way.↑ => 0, Way.↓ => 50, Way.→ => 51
12
+ # Pet = Enum[:Dog, :Cat] { |v| 0.5 * v } # Pet.Dog => 0.0, Pet.Cat => 0.5
13
+ #
14
+ # The generated enum class provides:
15
+ # * Class methods to access each constant's value (e.g., `Way.↑`).
16
+ # * A `size` method to get the number of defined constants.
17
+ # * An `each` class method to iterate over value-name pairs.
18
+ # * A `to_h` class method to get a hash of name-value pairs.
19
+ module Enum
20
+ def self.[](*args, &block)
21
+ # At least one argument should be provided
22
+ raise ArgumentError, "Enums must have at least one constant" if args.empty?
23
+
24
+ # Initialize tracking variables
25
+ pairs = {}
26
+ current = 0
27
+
28
+ args.flatten!(1)
29
+
30
+ # Process all arguments to extract name-value pairs
31
+ args.each do |arg|
32
+ current = case arg
33
+ when Symbol, String
34
+ # Single enum value - assign current value and increment
35
+ pairs[arg.to_sym] = current
36
+ current + 1
37
+ when Hash
38
+ # Hash with explicit key-value mapping
39
+ arg.each do |key, value|
40
+ unless key.is_a?(Symbol) || key.is_a?(String)
41
+ raise ArgumentError, "Enum names must be Symbol|String: #{key.inspect}"
42
+ end
43
+ raise ArgumentError, "Enum values must be Numeric: #{value.inspect}" unless value.is_a?(Numeric)
44
+ pairs[key.to_sym] = value
45
+ current = value + 1
46
+ end
47
+
48
+ current
49
+ else
50
+ raise ArgumentError, "Invalid enum argument: #{arg.inspect}"
51
+ end
52
+ end
53
+
54
+ # Apply block transformation if provided
55
+ if block_given?
56
+ values = Set.new
57
+ pairs.each do |prop, value|
58
+ hash = block.call(value, prop)
59
+
60
+ # Make sure block result is Numeric
61
+ unless hash.is_a?(Numeric)
62
+ raise ArgumentError, "Block must return a Numeric, got #{hash.class}: #{hash.inspect}"
63
+ end
64
+
65
+ # Check that result is unique
66
+ if values.include?(hash)
67
+ raise ArgumentError, "Block must return unique values, duplicate: #{hash}"
68
+ end
69
+
70
+ values.add(hash)
71
+ pairs[prop] = hash
72
+ end
73
+ end
74
+
75
+ # Create enum class and include this module
76
+ klass = Class.new
77
+ klass.include(Enum)
78
+
79
+ # Store enums data for instance methods
80
+ klass.instance_variable_set(:@pairs, pairs.freeze)
81
+
82
+ # Define getter methods for each constant
83
+ pairs.each do |prop, value|
84
+ begin
85
+ klass.define_singleton_method(prop) { value }
86
+ rescue => e
87
+ raise ArgumentError, "Invalid const name '#{prop}': #{e.message}"
88
+ end
89
+ end
90
+
91
+ klass.define_method(:initialize) do
92
+ raise RuntimeError, "Enums are not meant to be instantiated"
93
+ end
94
+
95
+ # Define utility methods directly on the class
96
+ klass.define_singleton_method(:size) do
97
+ @pairs.size
98
+ end
99
+
100
+ klass.define_singleton_method(:to_h) do
101
+ @pairs.dup
102
+ end
103
+
104
+ klass.define_singleton_method(:each) do |&block|
105
+ return enum_for(:each) unless block
106
+
107
+ @pairs.each do |prop, value|
108
+ block.call(value, prop)
109
+ end
110
+ end
111
+
112
+ klass
113
+ end
114
+ end
115
+ end
data/lib/patch/foobar.rb CHANGED
@@ -121,22 +121,24 @@ class Object
121
121
  end
122
122
 
123
123
  # Asserts a condition about `self`.
124
- # If a block is given, it asserts that the block, when called with `self`, returns a truthy value.
124
+ # If a block is given, it asserts that the block returns a truthy value.
125
+ # The block is passed `self` as an argument.
125
126
  # If no block is given, it asserts that `n === self` is true.
126
127
  # If the assertion fails, it performs a non-local exit by `raise`.
127
128
  #
128
- # @param `n` ([Object]) The object to compare with `self` if no block is given. Defaults to `self`.
129
+ # @param `n` ([Object]) The object to compare with `self` if no block is given.
129
130
  # @param `error` ([Class]) The class of error to raise if the assertion fails.
130
- # Defaults to `StandardError`.
131
+ # @param `message` (String?) An optional message to include in the raised error.
131
132
  # @yield [self] Optional block whose truthiness is asserted.
132
133
  # @return [self] The original object if assertion passes.
133
134
  # @throw `error` (or a related symbol) if the assertion fails.
134
135
  #
135
136
  # @example
136
137
  # 5.assert(Integer) # Passes
137
- # "string".assert(error: ArgumentError) { |s| s.length > 5 } # Passes
138
- def assert(n = self, error: StandardError)
139
- tap { (block_given? ? yield(self) : (n === self)) || raise(error) }
138
+ # "string".assert(error: ArgumentError) { |s| s.size > 5 } # Passes
139
+ # "".assert(message: "String too short") {|s| s.size > 5 } # Raises error with message
140
+ def assert(n = self, error: StandardError, message: nil)
141
+ tap { (block_given? ? yield(self) : (n === self)) || (message ? raise(error, message) : raise(error)) }
140
142
  end
141
143
 
142
144
  # Prints the `inspect` representation of `self` to standard output.
@@ -281,8 +283,8 @@ class Array
281
283
  # ["a","b"].third # => nil
282
284
  %i[second third fourth fifth sixth seventh eighth ninth tenth]
283
285
  .zip(1..) # 1-based index for human-readable Nth
284
- .each do |method_name, index|
285
- define_method(method_name) do |n = 1|
286
+ .each do |method, index|
287
+ define_method(method) do |n = 1|
286
288
  # `index` is 1 for 'second', 2 for 'third', etc.
287
289
  # So, for 'second' (index 1), drop 1. For 'third' (index 2), drop 2.
288
290
  drop(index).take(n).simplify
data/lib/patch/lambda.rb CHANGED
@@ -19,131 +19,168 @@ class Array
19
19
  end
20
20
  end
21
21
 
22
- # A Lambda object (`_`) is building block for anonymous functions.
23
- # For instance, (`_ + _`) represents f(x,y) = x + y
24
- # (`_ * 2`) represents f(x) = 2x.
25
- # You can use any method or operator on a `_`, and it will work:
26
- # - `[1, 2].map(&_.succ ** 3) => [8, 27]`
27
- # - `[[1,2], [3,4]].map(&_.sum / 2) => [1.5, 3.5]`
28
- #
29
- # The `lift` method allows a `_` to be used like so:
30
- # - `[[1, 2], [3, 4]].map(&(_ + _).lift) => [3, 7]`
31
- #
32
- # i.e. it treats the first arg (that is an array) as the actual arguments to be used
33
- # WARN: "lift" state is contagious (e.g. `(_ + _.lift) <=> (_ + _).lift`).
34
- #
35
- # You can similarly use [unlift] to convert a lifted `_` back to a normal.
36
- # [support_lambda] will allow for a _ to be used as the second operand (e.g. `2 - _`)
37
- #
38
- class Lambda < BasicObject
39
- class Arg; end
40
-
41
- class Block
42
- def initialize(block)
43
- @proc = block
44
- end
22
+ module EmanLib
23
+
24
+ # A Lambda object (`_`) is building block for anonymous functions.
25
+ # For instance, (`_ + _`) represents f(x,y) = x + y
26
+ # (`_ * 2`) represents f(x) = 2x.
27
+ # You can use any method or operator on a `_`, and it will work:
28
+ # - `[1, 2].map(&_.succ ** 3) => [8, 27]`
29
+ # - `[[1,2], [3,4]].map(&_.sum / 2) => [1.5, 3.5]`
30
+ #
31
+ # The `lift` method allows a `_` to be used like so:
32
+ # - `[[1, 2], [3, 4]].map(&(_ + _).lift) => [3, 7]`
33
+ #
34
+ # i.e. it treats the first arg (that is an array) as the actual arguments to be used
35
+ # WARN: "lift" state is contagious (e.g. `(_ + _.lift) <=> (_ + _).lift`).
36
+ #
37
+ # You can similarly use [unlift] to convert a lifted `_` back to a normal.
38
+ # [support_lambda] will allow for a _ to be used as the second operand (e.g. `2 - _`)
39
+ #
40
+ class Lambda < BasicObject
41
+ class Arg; end
45
42
 
46
- def to_proc
47
- @proc
48
- end
49
- end
43
+ class Block
44
+ def initialize(block)
45
+ @proc = block
46
+ end
50
47
 
51
- class Function
52
- def initialize(method)
53
- @method = method
48
+ def to_proc
49
+ @proc
50
+ end
54
51
  end
55
52
 
56
- def to_proc
57
- @method.to_proc
53
+ class Function
54
+ def initialize(method)
55
+ @method = method
56
+ end
57
+
58
+ def to_proc
59
+ @method.to_proc
60
+ end
58
61
  end
59
- end
60
62
 
61
- def __repr__; @__repr__ end
62
- def __tuply__; @__tuply__ end
63
+ def __repr__; @__repr__ end
64
+ def __tuply__; @__tuply__ end
63
65
 
64
- def initialize(repr = [Arg.new], *args)
65
- @__tuply__ = args.include?(:lift)
66
- @__repr__ = repr
66
+ def initialize(repr = [Arg.new], *args)
67
+ @__tuply__ = args.include?(:lift)
68
+ @__repr__ = repr
67
69
 
68
- return if repr != :lift
70
+ return if repr != :lift
69
71
 
70
- @__repr__ = [Arg.new]
71
- @__tuply__ = true
72
- end
72
+ @__repr__ = [Arg.new]
73
+ @__tuply__ = true
74
+ end
73
75
 
74
- def to_proc
75
- ::Proc.new do |*args|
76
- args = args.first if @__tuply__ && args.first.is_a?(::Array)
77
- stack = []
78
- index = 0
79
-
80
- @__repr__.each do |element|
81
- case element
82
- when Arg
83
- stack << args[index]
84
- index += 1
85
- when Function
86
- f = element.to_proc
87
- operands, block = stack.partition { |e| !e.is_a?(Block) }
88
- empty = ::Object.new
89
- xy = [operands.qoq(empty), operands.qoq(empty)]
90
- .reject { |x| x.equal?(empty) }.reverse
91
- stack = operands
92
-
93
- if block.empty?
94
- stack << f.call(*xy)
76
+ def to_proc
77
+ ::Proc.new do |*args|
78
+ args = args.first if @__tuply__ && args.first.is_a?(::Array)
79
+ stack = []
80
+ index = 0
81
+
82
+ @__repr__.each do |element|
83
+ case element
84
+ when Arg
85
+ stack << args[index]
86
+ index += 1
87
+ when Function
88
+ f = element.to_proc
89
+ operands, block = stack.partition { |e| !e.is_a?(Block) }
90
+ empty = ::Object.new
91
+ xy = [operands.qoq(empty), operands.qoq(empty)]
92
+ .reject { |x| x.equal?(empty) }.reverse
93
+ stack = operands
94
+
95
+ if block.empty?
96
+ stack << f.call(*xy)
97
+ else
98
+ stack << f.call(*xy, &block[0].to_proc)
99
+ end
95
100
  else
96
- stack << f.call(*xy, &block[0].to_proc)
101
+ stack << element
97
102
  end
98
- else
99
- stack << element
100
103
  end
101
- end
102
104
 
103
- stack.first
105
+ stack.first
106
+ end
104
107
  end
105
- end
106
108
 
107
- # Temporary wrapper for @__repr__
108
- class Repr
109
- attr_reader :repr
109
+ # Temporary wrapper for @__repr__
110
+ class Repr
111
+ attr_reader :repr
110
112
 
111
- def initialize(repr)
112
- @repr = repr
113
+ def initialize(repr)
114
+ @repr = repr
115
+ end
113
116
  end
114
- end
115
117
 
116
- def method_missing(method, *args, &block)
117
- extra = [Function.new(method)]
118
- extra.unshift Block.new(block) if block
119
- tuply = @__tuply__
118
+ def method_missing(method, *args, &block)
119
+ extra = [Function.new(method)]
120
+ extra.unshift Block.new(block) if block
121
+ tuply = @__tuply__
120
122
 
121
- args.map do |arg|
122
- if arg.is_a?(::Lambda)
123
- tuply ||= arg.__tuply__
124
- Repr.new(arg.__repr__)
125
- else
126
- arg
123
+ args.map do |arg|
124
+ if arg.is_a?(::Lambda)
125
+ tuply ||= arg.__tuply__
126
+ Repr.new(arg.__repr__)
127
+ else
128
+ arg
129
+ end
130
+ end.each_with_index do |arg, i|
131
+ if arg.is_a? Repr
132
+ args.insert(i, *arg.repr)
133
+ args.delete_at(i + arg.repr.size)
134
+ end
127
135
  end
128
- end.each_with_index do |arg, i|
129
- if arg.is_a? Repr
130
- args.insert(i, *arg.repr)
131
- args.delete_at(i + arg.repr.size)
136
+
137
+ if tuply
138
+ ::Lambda.new(@__repr__ + args + extra, :lift)
139
+ else
140
+ ::Lambda.new(@__repr__ + args + extra)
132
141
  end
133
142
  end
134
143
 
135
- if tuply
136
- ::Lambda.new(@__repr__ + args + extra, :lift)
137
- else
138
- ::Lambda.new(@__repr__ + args + extra)
144
+ def lift
145
+ ::Lambda.new(@__repr__, :lift)
139
146
  end
140
- end
141
147
 
142
- def lift
143
- ::Lambda.new(@__repr__, :lift)
148
+ def unlift
149
+ @__tuply__ ? ::Lambda.new(@__repr__) : self
150
+ end
144
151
  end
145
152
 
146
- def unlift
147
- @__tuply__ ? ::Lambda.new(@__repr__) : self
153
+ # The identity Lambda object (`_`).
154
+ # Store in a short variable, and use it as a building block for anonymous functions.
155
+ #
156
+ # _ = EmanLib._
157
+ # [1, 2, 3].map(&_.succ) => [2, 3, 4]
158
+ # [[1, 2], [3, 4]].map(&(_ + _).lift) => [3, 7]
159
+ _ = Lambda.new
160
+
161
+ # Support for using a `_` as the second operand with operators.
162
+ # WARN: This method WILL MODIFY the standard library classes.
163
+ # In particular, the operators: `- * / % ** & | ^ << >> <=> == === != > < >= <=`
164
+ # in the classes: `Integer, Float, Rational, Complex, Array, String, Hash, Range, Set`
165
+ def support_lambda
166
+ [[Integer, Float, Rational, Complex, Array, String, Hash, Range, Set],
167
+ %i[- * / % ** & | ^ << >> <=> == === != > < >= <=]].op(:product)
168
+ .each do |klass, op|
169
+ next unless klass.instance_methods(false).include?(op)
170
+
171
+ original = klass.instance_method(op)
172
+ klass.define_method(op) do |other|
173
+ if other.is_a?(Lambda)
174
+ repr = [self]
175
+ repr.concat(other.repr)
176
+ repr << Lambda::Function.new(op)
177
+ Lambda.new(repr)
178
+ else
179
+ original.bind(self).call(other)
180
+ end
181
+ end
182
+ end
148
183
  end
184
+
185
+ module_function :support_lambda
149
186
  end
data/lib/patch/let.rb ADDED
@@ -0,0 +1,389 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Enhances the Binding class to easily extract local variables into a Hash.
4
+ class Binding
5
+ # Converts the local variables accessible from this binding into a Hash.
6
+ # The keys of the hash are the variable names (as Symbols), and the values
7
+ # are the corresponding variable values.
8
+ #
9
+ # @return (Symbol to Object) {} - A hash mapping local variable names to their values
10
+ #
11
+ # @example
12
+ # def my_method
13
+ # a = 10
14
+ # b = "hello"
15
+ # binding.variables # => {:a=>10, :b=>"hello"}
16
+ # end
17
+ def variables
18
+ Hash[
19
+ local_variables.map do |var|
20
+ [var, local_variable_get(var)]
21
+ end
22
+ ]
23
+ end
24
+ end
25
+
26
+ # Enhances the Array class with a utility method to check its structure.
27
+ class Array
28
+ # Checks if the array is "hashy", meaning it consists entirely of
29
+ # two-element arrays. This structure is suitable for conversion to a Hash
30
+ # using `to_h`.
31
+ #
32
+ # @return [Boolean] `true` if the array is "hashy", `false` otherwise.
33
+ #
34
+ # @example
35
+ # [[:a, 1], [:b, 2]].hashy? # => true
36
+ # [["key1", "value1"]].hashy? # => true
37
+ # [[1, 2, 3], [:b, 2]].hashy? # => false (first item has 3 elements)
38
+ # [1, 2, 3].hashy? # => false (items are not arrays)
39
+ # [[], [:a, 1]].hashy? # => false (first item is empty)
40
+ def hashy?
41
+ all? { |item| item.is_a?(Array) && item.size == 2 }
42
+ end
43
+ end
44
+
45
+ # Enhances the String class with validation for method and variable names.
46
+ class String
47
+ # Checks if the string is a valid Ruby method or variable name.
48
+ #
49
+ # Valid method names can include letters, numbers, underscores, and may
50
+ # end with `!`, `=`, or `?`.
51
+ # Valid variable names can include letters, numbers, and underscores but
52
+ # cannot end with `!`, `=`, or `?`.
53
+ #
54
+ # @param target [[:method, :variable, :var]] Whether to validate as a method
55
+ # name or a variable name. Defaults to `:method`.
56
+ # @return [Boolean] `true` if the string is a valid name for the specified target,
57
+ # `false` otherwise.
58
+ #
59
+ # @example
60
+ # "my_method".valid_name? # => true
61
+ # "my_method?".valid_name? # => true
62
+ # "setter=".valid_name? # => true
63
+ # "_private_method!".valid_name? # => true
64
+ # "ConstantLike".valid_name? # => true
65
+ # "1invalid".valid_name? # => false (starts with a number)
66
+ # "invalid-name".valid_name? # => false (contains hyphen)
67
+ #
68
+ # "my_variable".valid_name?(:variable) # => true
69
+ # "_var".valid_name?(:variable) # => true
70
+ # "my_variable?".valid_name?(:variable)# => false (ends with ?)
71
+ # "A_CONSTANT".valid_name?(:variable) # => true
72
+ def valid_name?(target = :method)
73
+ case target
74
+ when :method
75
+ self =~ /\A[a-zA-Z_]\w*[!?=]?\z/
76
+ when :var, :variable
77
+ self =~ /\A[a-zA-Z_]\w*\z/
78
+ else
79
+ false
80
+ end
81
+ end
82
+ end
83
+
84
+ module EmanLib
85
+ # A convenient shorthand for `Maplet.new.define!(...)`.
86
+ #
87
+ # @param args [Array<Hash, Array>] A list of Hashes or "hashy" Arrays.
88
+ # @param block [Proc] If provided, its local variables are used to define methods.
89
+ # @return [Object] A new object with dynamically defined methods.
90
+ #
91
+ # @see [Maplet#define!]
92
+ #
93
+ # @example
94
+ # person = let(name: "Rio", age: 37)
95
+ # puts person.name # => "Rio"
96
+ # puts person.age # => 37
97
+ #
98
+ # point = let **{x: 10, y: 20}
99
+ # puts point.x # => 10
100
+ #
101
+ # settings = let do
102
+ # theme = "dark"
103
+ # font_size = 12
104
+ # binding
105
+ # end
106
+ # puts settings.theme # => "dark"
107
+ #
108
+ # complex_data = let([[:id, 42]], name: "Xed") do
109
+ # details = { color: "red", size: "large" }
110
+ # binding # Required
111
+ # end
112
+ #
113
+ # puts complex_data.id # => 42
114
+ # puts complex_data.name # => "Xed"
115
+ # puts complex_data.details.color # => "red"
116
+ def let(*args, &block)
117
+ Maplet.new.define!(*args, &block)
118
+ end
119
+
120
+ module_function :let
121
+
122
+ # Class that allows for dynamic definition of properties
123
+ class Maplet
124
+ include Enumerable
125
+
126
+ def initialize
127
+ @props = []
128
+ end
129
+
130
+ # Dynamically defines properties based on the provided arguments and/or block.
131
+ #
132
+ # Arguments can be Hashes or "hashy" Arrays (arrays of `[key, value]` pairs).
133
+ # If a block is given, its local variables are also used to define methods.
134
+ # Keys are converted to symbols and validated as method names.
135
+ #
136
+ # If a value is a Hash or a "hashy" Array, it's recursively
137
+ # used to define nested properties.
138
+ #
139
+ # @param `args` [Array<Hash, Array>] A list of Hashes or "hashy" Arrays.
140
+ # Each key-value pair will result in a getter and setter method.
141
+ # @param `block` [Proc] If provided, `block.call` is expected to return a `Binding`
142
+ # (i.e. last expression in the block must be `binding`).
143
+ # Local variables from this binding will be used to define methods.
144
+ #
145
+ # @return [self] The object itself, now with the newly defined methods.
146
+ #
147
+ # @raise [ArgumentError] If an argument is not a Hash or a "hashy" Array.
148
+ # @raise [ArgumentError] If a key is not a valid method name.
149
+ #
150
+ # @example Defining with a Hash
151
+ # # let(...) === Maplet.new.define!(...)
152
+ #
153
+ # person = let(name: "Alice", age: 30)
154
+ # person.name # => "Alice"
155
+ # person.age = 31
156
+ # person.age # => 31
157
+ #
158
+ # @example Defining with a "hashy" Array
159
+ # config = let([[:host, "localhost"], [:port, 8080]])
160
+ # config.host # => "localhost"
161
+ #
162
+ # @example Defining with a block
163
+ # user = let do
164
+ # username = "bob"
165
+ # active = true
166
+ # binding # Important: makes local variables available
167
+ # end
168
+ #
169
+ # user.username # => "bob"
170
+ # user.active? # This won't define active? automatically, but user.active
171
+ #
172
+ # @example Nested definitions
173
+ # settings = let(
174
+ # database: { adapter: "sqlite3", pool: 5 },
175
+ # logging: [[:level, "info"], [:file, "/var/log/app.log"]]
176
+ # )
177
+ # settings.database.adapter # => "sqlite3"
178
+ # settings.logging.level # => "info"
179
+ #
180
+ # @example Combining arguments
181
+ # complex = let({id: 1}, [[:type, "example"]]) do
182
+ # description = "A complex object"
183
+ # status = :new
184
+ # binding
185
+ # end
186
+ #
187
+ # complex.id # => 1
188
+ # complex.type # => "example"
189
+ # complex.description # => "A complex object"
190
+ def define!(*args, &block)
191
+ # Stores all key-value pairs to be defined
192
+ variable = {}
193
+
194
+ # Process Hashes and "hashy" Arrays first
195
+ args.each do |arg|
196
+ case arg
197
+ when Hash
198
+ variable.merge!(arg)
199
+ when Array
200
+ raise ArgumentError, "Array should be Hash like." unless arg.hashy?
201
+ variable.merge!(arg.to_h)
202
+ else
203
+ raise ArgumentError, "Invalid argument type: #{arg.class}"
204
+ end
205
+ end
206
+
207
+ # Process local variables from the block
208
+ if block_given?
209
+ binding = block.call # The block is expected to return its binding.
210
+ raise ArgumentError, "Block must return a Binding object." unless binding.is_a?(Binding)
211
+
212
+ variable.merge!(binding.variables)
213
+ end
214
+
215
+ # Define getters and setters and store values
216
+ variable.each do |prop, value|
217
+ prop = prop.to_s.to_sym
218
+ raise ArgumentError, "Invalid name: #{prop}" unless prop.to_s.valid_name?
219
+
220
+ # Recursively define for nested Hashes or "hashy" Arrays
221
+ if value.is_a? Hash
222
+ value = Maplet.new.define!(value)
223
+ elsif value.is_a?(Array) && value.hashy?
224
+ value = Maplet.new.define!(value.to_h)
225
+ end
226
+
227
+ # Store the original value in an instance variable
228
+ instance_variable_set("@#{prop}", value)
229
+
230
+ define_singleton_method(prop) do
231
+ instance_variable_get("@#{prop}")
232
+ end
233
+
234
+ define_singleton_method("#{prop}=") do |value|
235
+ instance_variable_set("@#{prop}", value)
236
+ end
237
+ end
238
+
239
+ @props += variable.keys.map(&:to_sym)
240
+ self
241
+ end
242
+
243
+ # Accesses a prop's value, allowing for nested access.
244
+ #
245
+ # @param prop [Array<Symbol, String>] A sequence of prop names to access.
246
+ # @return [Object] The value of the specified property.
247
+ #
248
+ # @raise [ArgumentError] If no prop is given or does not exist.
249
+ #
250
+ # @example Accessing top-level and nested properties
251
+ # settings = let(
252
+ # host: "localhost",
253
+ # database: { adapter: "sqlite3", pool: 5 }
254
+ # )
255
+ #
256
+ # settings[:host] # => "localhost"
257
+ # settings[:database] # => <EmanLib::Maplet ...>
258
+ # settings[:database, :adapter] # => "sqlite3"
259
+ # settings[:database, :pool] # => 5
260
+ def [](*prop)
261
+ raise ArgumentError, "No property specified." if prop.empty?
262
+ value = instance_variable_get("@#{prop.first}")
263
+
264
+ if prop.size == 1
265
+ value
266
+ else
267
+ value[*prop[1..]]
268
+ end
269
+ rescue NameError
270
+ error = "Property '#{prop.join ?.}' is not defined in this Maplet."
271
+ error += " Available properties: [#{@props.join(", ")}]"
272
+ raise ArgumentError, error
273
+ end
274
+
275
+ # Converts the Maplet and any nested Maplets back into a Hash.
276
+ # Recursively transforms inner maplets into nested Hashes.
277
+ #
278
+ # @return [Hash{Symbol => Object}] A hash representation of the Maplet.
279
+ #
280
+ # @example
281
+ # settings = let(host: 'localhost', db: { name: 'dev', pool: 5 })
282
+ # settings.to_h # => { host: "localhost", db: { name: "dev", pool: 5 } }
283
+ def to_h
284
+ @props.each_with_object({}) do |prop, hash|
285
+ value = self[prop]
286
+
287
+ if value.is_a?(Maplet)
288
+ hash[prop] = value.to_h
289
+ else
290
+ hash[prop] = value
291
+ end
292
+ end
293
+ end
294
+
295
+ # Iterates over each leaf property of the Maplet.
296
+ #
297
+ # @yield [value, path] Gives the value and its full access path.
298
+ # @yieldparam value [Object] The value of the leaf property.
299
+ # @yieldparam path [String] The dotted path to the prop (e.g., `"dir.home"`).
300
+ #
301
+ # @return [self, Enumerator] Self if a block is given, otherwise an Enumerator.
302
+ #
303
+ # @example
304
+ # user = let(name: 'Ian', meta: { role: 'Admin', active: true })
305
+ # user.each { |value, path| puts "#{path}: #{value}" }
306
+ # # Prints:
307
+ # # name: Ian
308
+ # # meta.role: Admin
309
+ # # meta.active: true
310
+ def each(&block)
311
+ return enum_for(:each) unless block_given?
312
+
313
+ tap do
314
+ @props.each do |prop|
315
+ value = self[prop]
316
+ if value.is_a?(Maplet)
317
+ value.each do |inner, nested|
318
+ yield inner, "#{prop}.#{nested}"
319
+ end
320
+ else
321
+ yield value, prop.to_s
322
+ end
323
+ end
324
+ end
325
+ end
326
+
327
+ # Creates a new Maplet by applying a block to each property's value.
328
+ # You can transform all properties or just a select few.
329
+ #
330
+ # @param only [Array<Symbol, String>] Optional. A list of top-level property
331
+ # names to transform. If provided, other properties are copied as-is.
332
+ # @yield [value, prop] The block to apply to each selected property.
333
+ # @yieldparam value [Object] The value of the property.
334
+ # @yieldparam prop [String] The name of the property.
335
+ #
336
+ # @return [Maplet] A new Maplet with the transformed values.
337
+ #
338
+ # @example Transforming all numeric values
339
+ # config = let(port: 80, timeout: 3000, host: "localhost")
340
+ # doubled = config.map do |value, _|
341
+ # value.is_a?(Numeric) ? value * 2 : value
342
+ # end
343
+ # doubled.to_h # => { port: 160, timeout: 6000, host: "localhost" }
344
+ #
345
+ # @example Transforming only a specific property
346
+ # config = let(port: 80, host: "localhost")
347
+ # upcased = config.map(:host) { |val, _| val.upcase }
348
+ # upcased.to_h # => { port: 80, host: "LOCALHOST" }
349
+ def map(*only, &block)
350
+ return enum_for(:map) unless block_given?
351
+ hash = {}
352
+
353
+ @props.each do |prop|
354
+ value = self[prop]
355
+ if value.is_a?(Maplet)
356
+ hash[prop] = value.map(*only, &block)
357
+ elsif only.empty?
358
+ hash[prop] = yield(value, prop)
359
+ elsif only.any? { |p| p.to_sym == prop }
360
+ hash[prop] = yield(value, prop)
361
+ else
362
+ hash[prop] = value
363
+ end
364
+ end
365
+
366
+ Maplet.new.define!(hash)
367
+ end
368
+
369
+ # Returns a new Maplet that excludes the specified top-level properties.
370
+ # Note: This only works on top-level properties.
371
+ #
372
+ # @param props [Array<Symbol, String>] A list of property names to exclude.
373
+ # @return [Maplet] A new Maplet instance without the specified properties.
374
+ #
375
+ # @example
376
+ # user = let(id: 1, name: 'Tico', email: 'tico@example.com')
377
+ #
378
+ # user_public_data = user.without(:id)
379
+ # user_public_data.to_h # => { name: "Tico", email: "tico@example.com" }
380
+ def without(*props)
381
+ return self if props.empty?
382
+
383
+ remaining = to_h
384
+ props.each { |p| remaining.delete(p.to_sym) }
385
+
386
+ Maplet.new.define!(remaining)
387
+ end
388
+ end
389
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: emanlib
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - emanrdesu
@@ -17,9 +17,10 @@ extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
19
  - lib/emanlib.rb
20
- - lib/patch/define.rb
20
+ - lib/patch/enum.rb
21
21
  - lib/patch/foobar.rb
22
22
  - lib/patch/lambda.rb
23
+ - lib/patch/let.rb
23
24
  homepage: https://github.com/emanrdesu/lib
24
25
  licenses:
25
26
  - GPL-3.0-only
data/lib/patch/define.rb DELETED
@@ -1,198 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Enhances the Binding class to easily extract local variables into a Hash.
4
- class Binding
5
- # Converts the local variables accessible from this binding into a Hash.
6
- # The keys of the hash are the variable names (as Symbols), and the values
7
- # are the corresponding variable values.
8
- #
9
- # @return [Hash<Symbol, Object>] A hash mapping local variable names to their values.
10
- #
11
- # @example
12
- # def my_method
13
- # a = 10
14
- # b = "hello"
15
- # binding.variables # => {:a=>10, :b=>"hello"}
16
- # end
17
- def variables
18
- Hash[
19
- local_variables.map do |var|
20
- [var, local_variable_get(var)]
21
- end
22
- ]
23
- end
24
- end
25
-
26
- # Enhances the Array class with a utility method to check its structure.
27
- class Array
28
- # Checks if the array is "hashy", meaning it consists entirely of
29
- # two-element arrays. This structure is suitable for conversion to a Hash
30
- # using `to_h`.
31
- #
32
- # @return [Boolean] `true` if the array is "hashy", `false` otherwise.
33
- #
34
- # @example
35
- # [[:a, 1], [:b, 2]].hashy? # => true
36
- # [["key1", "value1"]].hashy? # => true
37
- # [[1, 2, 3], [:b, 2]].hashy? # => false (first item has 3 elements)
38
- # [1, 2, 3].hashy? # => false (items are not arrays)
39
- # [[], [:a, 1]].hashy? # => false (first item is not a 2-element array)
40
- def hashy?
41
- all? { |item| item.is_a?(Array) && item.size == 2 }
42
- end
43
- end
44
-
45
- # Enhances the String class with validation for method and variable names.
46
- class String
47
- # Checks if the string is a valid Ruby method or variable name.
48
- #
49
- # Valid method names can include letters, numbers, underscores, and may
50
- # end with `!`, `=`, or `?`.
51
- # Valid variable names can include letters, numbers, and underscores but
52
- # cannot end with `!`, `=`, or `?`.
53
- #
54
- # @param target [:method, :variable] Specifies whether to validate as a method
55
- # name or a variable name. Defaults to `:method`.
56
- # @return [Boolean] `true` if the string is a valid name for the specified target,
57
- # `false` otherwise.
58
- #
59
- # @example
60
- # "my_method".valid_name? # => true
61
- # "my_method?".valid_name? # => true
62
- # "setter=".valid_name? # => true
63
- # "_private_method!".valid_name? # => true
64
- # "ConstantLike".valid_name? # => true
65
- # "1invalid".valid_name? # => false (starts with a number)
66
- # "invalid-name".valid_name? # => false (contains hyphen)
67
- #
68
- # "my_variable".valid_name?(target: :variable) # => true
69
- # "_var".valid_name?(target: :variable) # => true
70
- # "my_variable?".valid_name?(target: :variable)# => false (ends with ?)
71
- # "A_CONSTANT".valid_name?(target: :variable) # => true
72
- def valid_name?(target: :method)
73
- case target
74
- when :method
75
- self =~ /\A[a-zA-Z_]\w*[!?=]?\z/
76
- when :variable
77
- self =~ /\A[a-zA-Z_]\w*\z/
78
- else
79
- false
80
- end
81
- end
82
- end
83
-
84
- # Enhances the base Object class to allow dynamic definition of properties
85
- class Object
86
- # Dynamically defines properties on `self`,
87
- # based on the provided arguments and/or block.
88
- #
89
- # Arguments can be Hashes or "hashy" Arrays (arrays of `[key, value]` pairs).
90
- # If a block is given, its local variables are also used to define methods.
91
- # Keys are converted to symbols and validated as method names.
92
- #
93
- # If a value is a Hash or a "hashy" Array, it's recursively
94
- # used to define nested properties.
95
- #
96
- # @param `args` [Array<Hash, Array>] A list of Hashes or "hashy" Arrays.
97
- # Each key-value pair will result in a getter and setter method.
98
- # @param `block` [Proc] If provided, `block.call` is expected to return a `Binding`
99
- # (i.e. last expression in the block must be `binding`).
100
- # Local variables from this binding will be used to define methods.
101
- #
102
- # @return [self] The object itself, now with the newly defined methods.
103
- #
104
- # @raise [ArgumentError] If an argument is not a Hash or a "hashy" Array.
105
- # @raise [ArgumentError] If a key is not a valid method name.
106
- #
107
- # @example Defining with a Hash
108
- # # let(...) === Object.new.define(...)
109
- #
110
- # person = let(name: "Alice", age: 30)
111
- # person.name # => "Alice"
112
- # person.age = 31
113
- # person.age # => 31
114
- #
115
- # @example Defining with a "hashy" Array
116
- # config = let([[:host, "localhost"], [:port, 8080]])
117
- # config.host # => "localhost"
118
- #
119
- # @example Defining with a block
120
- # user = let do
121
- # username = "bob"
122
- # active = true
123
- # binding # Important: makes local variables available
124
- # end
125
- #
126
- # user.username # => "bob"
127
- # user.active? # This won't define active? automatically, but user.active
128
- #
129
- # @example Nested definitions
130
- # settings = let(
131
- # database: { adapter: "sqlite3", pool: 5 },
132
- # logging: [[:level, "info"], [:file, "/var/log/app.log"]]
133
- # )
134
- # settings.database.adapter # => "sqlite3"
135
- # settings.logging.level # => "info"
136
- #
137
- # @example Combining arguments
138
- # complex = let({id: 1}, [[:type, "example"]]) do
139
- # description = "A complex object"
140
- # status = :new
141
- # binding
142
- # end
143
- #
144
- # complex.id # => 1
145
- # complex.type # => "example"
146
- # complex.description # => "A complex object"
147
- def define(*args, &block)
148
- # Stores all key-value pairs to be defined
149
- variable = {}
150
-
151
- # Process Hashes and "hashy" Arrays first
152
- args.each do |arg|
153
- case arg
154
- when Hash
155
- variable.merge!(arg)
156
- when Array
157
- raise ArgumentError, "Array should be Hash like." unless arg.hashy?
158
- variable.merge!(arg.to_h)
159
- else
160
- raise ArgumentError, "Invalid argument type: #{arg.class}"
161
- end
162
- end
163
-
164
- # Process local variables from the block
165
- if block_given? # If provided
166
- binding = block.call # The block is expected to return its binding.
167
- raise ArgumentError, "Block must return a Binding object." unless binding.is_a?(Binding)
168
-
169
- variable.merge!(binding.variables)
170
- end
171
-
172
- # Define getters and setters and store values
173
- variable.each do |name, value|
174
- name = name.to_s.to_sym
175
- raise ArgumentError, "Invalid name: #{name}" unless name.to_s.valid_name?(target: :method)
176
-
177
- # Recursively define for nested Hashes or "hashy" Arrays
178
- if value.is_a? Hash
179
- value = Object.new.define(value)
180
- elsif value.is_a?(Array) && value.hashy?
181
- value = Object.new.define(value.to_h)
182
- end
183
-
184
- # Store the original value in an instance variable
185
- instance_variable_set("@#{name}", value)
186
-
187
- define_singleton_method(name) do
188
- instance_variable_get("@#{name}")
189
- end
190
-
191
- define_singleton_method("#{name}=") do |value|
192
- instance_variable_set("@#{name}", value)
193
- end
194
- end
195
-
196
- self
197
- end
198
- end