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 +4 -4
- data/lib/emanlib.rb +3 -68
- data/lib/patch/enum.rb +115 -0
- data/lib/patch/foobar.rb +10 -8
- data/lib/patch/lambda.rb +135 -98
- data/lib/patch/let.rb +389 -0
- metadata +3 -2
- data/lib/patch/define.rb +0 -198
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd662dbc1918087da06994da20c822e4e3697a07db7f5681602c26c46839ed07
|
4
|
+
data.tar.gz: d1e82edbb74f7d2f60dbe906f5715cddc94a28639266107d713744517de52007
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3b5a86384a130d687ef886a075b55d1d3ea892ccf656356df78cfee9c9949dbcdaa36691b92733506956b687c5af66baba8fcf6455e3176af3eaac7bdb80c181
|
7
|
+
data.tar.gz: 67b7d61d4cf0ad053c3c8e9121c734fe5c03b8a899c17f2b5b08d5c181142f70a494008820fe365f772dbcebb468ea99d14d3d2e2e3d9ea7198ad9b8c9f59482
|
data/lib/emanlib.rb
CHANGED
@@ -1,73 +1,8 @@
|
|
1
|
-
require_relative "patch/
|
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
|
-
|
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
|
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.
|
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
|
-
#
|
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.
|
138
|
-
|
139
|
-
|
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 |
|
285
|
-
define_method(
|
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
|
-
|
23
|
-
|
24
|
-
# (`_
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
43
|
+
class Block
|
44
|
+
def initialize(block)
|
45
|
+
@proc = block
|
46
|
+
end
|
50
47
|
|
51
|
-
|
52
|
-
|
53
|
-
|
48
|
+
def to_proc
|
49
|
+
@proc
|
50
|
+
end
|
54
51
|
end
|
55
52
|
|
56
|
-
|
57
|
-
|
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
|
-
|
62
|
-
|
63
|
+
def __repr__; @__repr__ end
|
64
|
+
def __tuply__; @__tuply__ end
|
63
65
|
|
64
|
-
|
65
|
-
|
66
|
-
|
66
|
+
def initialize(repr = [Arg.new], *args)
|
67
|
+
@__tuply__ = args.include?(:lift)
|
68
|
+
@__repr__ = repr
|
67
69
|
|
68
|
-
|
70
|
+
return if repr != :lift
|
69
71
|
|
70
|
-
|
71
|
-
|
72
|
-
|
72
|
+
@__repr__ = [Arg.new]
|
73
|
+
@__tuply__ = true
|
74
|
+
end
|
73
75
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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 <<
|
101
|
+
stack << element
|
97
102
|
end
|
98
|
-
else
|
99
|
-
stack << element
|
100
103
|
end
|
101
|
-
end
|
102
104
|
|
103
|
-
|
105
|
+
stack.first
|
106
|
+
end
|
104
107
|
end
|
105
|
-
end
|
106
108
|
|
107
|
-
|
108
|
-
|
109
|
-
|
109
|
+
# Temporary wrapper for @__repr__
|
110
|
+
class Repr
|
111
|
+
attr_reader :repr
|
110
112
|
|
111
|
-
|
112
|
-
|
113
|
+
def initialize(repr)
|
114
|
+
@repr = repr
|
115
|
+
end
|
113
116
|
end
|
114
|
-
end
|
115
117
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
129
|
-
if
|
130
|
-
|
131
|
-
|
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
|
-
|
136
|
-
::Lambda.new(@__repr__
|
137
|
-
else
|
138
|
-
::Lambda.new(@__repr__ + args + extra)
|
144
|
+
def lift
|
145
|
+
::Lambda.new(@__repr__, :lift)
|
139
146
|
end
|
140
|
-
end
|
141
147
|
|
142
|
-
|
143
|
-
|
148
|
+
def unlift
|
149
|
+
@__tuply__ ? ::Lambda.new(@__repr__) : self
|
150
|
+
end
|
144
151
|
end
|
145
152
|
|
146
|
-
|
147
|
-
|
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.
|
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/
|
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
|