emanlib 0.1.1 → 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: 7c865695d10014ad5b2b967418ec8e4ea65165e9549590f7380c4f21e27b50b0
4
- data.tar.gz: 00dda6a97a260530b29a932d883377b9f8877820d15b5605bce7582cd422ac72
3
+ metadata.gz: fd662dbc1918087da06994da20c822e4e3697a07db7f5681602c26c46839ed07
4
+ data.tar.gz: d1e82edbb74f7d2f60dbe906f5715cddc94a28639266107d713744517de52007
5
5
  SHA512:
6
- metadata.gz: fc5e5219826053c84b0e007cf3e59dc5f72d6f483384ed5515f7938b5c3efb9fab39e77109adafe5b18a58fe17c65913d8951a4b4478b27445a93451a9f79851
7
- data.tar.gz: bf1123ccc076eaa6c657c28fbc47aff2c4f0e23a749625b02ec33433e7605b5e9262fcd0ff3bdf43e67c56e026a5c75c01d881f938c7517923f22024efe4283e
6
+ metadata.gz: 3b5a86384a130d687ef886a075b55d1d3ea892ccf656356df78cfee9c9949dbcdaa36691b92733506956b687c5af66baba8fcf6455e3176af3eaac7bdb80c181
7
+ data.tar.gz: 67b7d61d4cf0ad053c3c8e9121c734fe5c03b8a899c17f2b5b08d5c181142f70a494008820fe365f772dbcebb468ea99d14d3d2e2e3d9ea7198ad9b8c9f59482
data/lib/emanlib.rb CHANGED
@@ -1,76 +1,8 @@
1
- require_relative "patch/define"
2
1
  require_relative "patch/enum"
3
2
  require_relative "patch/foobar"
4
3
  require_relative "patch/lambda"
4
+ require_relative "patch/let"
5
5
 
6
6
  module EmanLib
7
- # The identity Lambda object (`_`).
8
- # Store in a short variable, and use it as a building block for anonymous functions.
9
- #
10
- # _ = EmanLib._
11
- # [1, 2, 3].map(&_.succ) => [2, 3, 4]
12
- # [[1, 2], [3, 4]].map(&(_ + _).lift) => [3, 7]
13
- _ = Lambda.new
14
-
15
- # Helper method to create definitions.
16
- # A convenient shorthand for `Object.new.define(...)`.
17
- #
18
- # @param args [Array<Hash, Array>] A list of Hashes or "hashy" Arrays.
19
- # @param block [Proc] If provided, its local variables are used to define methods.
20
- # @return [Object] A new object with dynamically defined methods.
21
- #
22
- # @see [Object#define]
23
- #
24
- # @example
25
- # person = let(name: "Rio", age: 37)
26
- # puts person.name # => "Rio"
27
- # puts person.age # => 37
28
- #
29
- # point = let **{x: 10, y: 20}
30
- # puts point.x # => 10
31
- #
32
- # settings = let do
33
- # theme = "dark"
34
- # font_size = 12
35
- # binding
36
- # end
37
- # puts settings.theme # => "dark"
38
- #
39
- # complex_data = let([[:id, 42]], name: "Xed") do
40
- # details = { color: "red", size: "large" }
41
- # binding # Required
42
- # end
43
- #
44
- # puts complex_data.id # => 42
45
- # puts complex_data.name # => "Xed"
46
- # puts complex_data.details.color # => "red"
47
- def let(*args, &block)
48
- Object.new.define(*args, &block)
49
- end
50
-
51
- # Support for using a `_` as the second operand with operators.
52
- # WARN: This method WILL MODIFY the standard library classes.
53
- # In particular, the operators: `- * / % ** & | ^ << >> <=> == === != > < >= <=`
54
- # in the classes: `Integer, Float, Rational, Complex, Array, String, Hash, Range, Set`
55
- def support_lambda
56
- [[Integer, Float, Rational, Complex, Array, String, Hash, Range, Set],
57
- %i[- * / % ** & | ^ << >> <=> == === != > < >= <=]].op(:product)
58
- .each do |klass, op|
59
- next unless klass.instance_methods(false).include?(op)
60
-
61
- original = klass.instance_method(op)
62
- klass.define_method(op) do |other|
63
- if other.is_a?(Lambda)
64
- repr = [self]
65
- repr.concat(other.repr)
66
- repr << Lambda::Function.new(op)
67
- Lambda.new(repr)
68
- else
69
- original.bind(self).call(other)
70
- end
71
- end
72
- end
73
- end
74
-
75
- module_function :let, :support_lambda
7
+ VERSION = "0.1.2"
76
8
  end
data/lib/patch/enum.rb CHANGED
@@ -12,11 +12,10 @@ module EmanLib
12
12
  # Pet = Enum[:Dog, :Cat] { |v| 0.5 * v } # Pet.Dog => 0.0, Pet.Cat => 0.5
13
13
  #
14
14
  # The generated enum class provides:
15
- # * Class methods to access each constant's value (e.g., `Lvl.MID`).
15
+ # * Class methods to access each constant's value (e.g., `Way.↑`).
16
+ # * A `size` method to get the number of defined constants.
16
17
  # * An `each` class method to iterate over value-name pairs.
17
18
  # * A `to_h` class method to get a hash of name-value pairs.
18
- # * A `consts` class method to get an array of constant names (as symbols).
19
- # * A `values` class method to get an array of constant values.
20
19
  module Enum
21
20
  def self.[](*args, &block)
22
21
  # At least one argument should be provided
@@ -55,21 +54,21 @@ module EmanLib
55
54
  # Apply block transformation if provided
56
55
  if block_given?
57
56
  values = Set.new
58
- pairs.each do |name, value|
59
- result = block.call(value, name)
57
+ pairs.each do |prop, value|
58
+ hash = block.call(value, prop)
60
59
 
61
60
  # Make sure block result is Numeric
62
- unless result.is_a?(Numeric)
63
- raise ArgumentError, "Block must return a Numeric, got #{result.class}: #{result.inspect}"
61
+ unless hash.is_a?(Numeric)
62
+ raise ArgumentError, "Block must return a Numeric, got #{hash.class}: #{hash.inspect}"
64
63
  end
65
64
 
66
65
  # Check that result is unique
67
- if values.include?(result)
68
- raise ArgumentError, "Block must return unique values, duplicate: #{result}"
66
+ if values.include?(hash)
67
+ raise ArgumentError, "Block must return unique values, duplicate: #{hash}"
69
68
  end
70
69
 
71
- values.add(result)
72
- pairs[name] = result
70
+ values.add(hash)
71
+ pairs[prop] = hash
73
72
  end
74
73
  end
75
74
 
@@ -81,11 +80,11 @@ module EmanLib
81
80
  klass.instance_variable_set(:@pairs, pairs.freeze)
82
81
 
83
82
  # Define getter methods for each constant
84
- pairs.each do |name, value|
83
+ pairs.each do |prop, value|
85
84
  begin
86
- klass.define_singleton_method(name) { value }
85
+ klass.define_singleton_method(prop) { value }
87
86
  rescue => e
88
- raise ArgumentError, "Invalid const name '#{name}': #{e.message}"
87
+ raise ArgumentError, "Invalid const name '#{prop}': #{e.message}"
89
88
  end
90
89
  end
91
90
 
@@ -94,23 +93,19 @@ module EmanLib
94
93
  end
95
94
 
96
95
  # Define utility methods directly on the class
97
- klass.define_singleton_method(:to_h) do
98
- @pairs.dup
99
- end
100
-
101
- klass.define_singleton_method(:consts) do
102
- @pairs.keys
96
+ klass.define_singleton_method(:size) do
97
+ @pairs.size
103
98
  end
104
99
 
105
- klass.define_singleton_method(:values) do
106
- @pairs.values
100
+ klass.define_singleton_method(:to_h) do
101
+ @pairs.dup
107
102
  end
108
103
 
109
104
  klass.define_singleton_method(:each) do |&block|
110
105
  return enum_for(:each) unless block
111
106
 
112
- @pairs.each do |name, value|
113
- block.call(value, name)
107
+ @pairs.each do |prop, value|
108
+ block.call(value, prop)
114
109
  end
115
110
  end
116
111
 
data/lib/patch/foobar.rb CHANGED
@@ -283,8 +283,8 @@ class Array
283
283
  # ["a","b"].third # => nil
284
284
  %i[second third fourth fifth sixth seventh eighth ninth tenth]
285
285
  .zip(1..) # 1-based index for human-readable Nth
286
- .each do |method_name, index|
287
- define_method(method_name) do |n = 1|
286
+ .each do |method, index|
287
+ define_method(method) do |n = 1|
288
288
  # `index` is 1 for 'second', 2 for 'third', etc.
289
289
  # So, for 'second' (index 1), drop 1. For 'third' (index 2), drop 2.
290
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.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - emanrdesu
@@ -17,10 +17,10 @@ extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
19
  - lib/emanlib.rb
20
- - lib/patch/define.rb
21
20
  - lib/patch/enum.rb
22
21
  - lib/patch/foobar.rb
23
22
  - lib/patch/lambda.rb
23
+ - lib/patch/let.rb
24
24
  homepage: https://github.com/emanrdesu/lib
25
25
  licenses:
26
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