memorium 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +165 -0
- data/Rakefile +8 -0
- data/examples/order/order.rb +48 -0
- data/examples/order/order_item.rb +31 -0
- data/examples/order.rb +22 -0
- data/examples/save_logs/app.log +8 -0
- data/examples/save_logs/database.rb +24 -0
- data/examples/save_logs/log_entry.rb +52 -0
- data/examples/save_logs/save_logs.rb +75 -0
- data/examples/save_logs.rb +17 -0
- data/lib/memorium/access_level.rb +32 -0
- data/lib/memorium/config.rb +9 -0
- data/lib/memorium/core.rb +13 -0
- data/lib/memorium/extension.rb +30 -0
- data/lib/memorium/initialize_attr/interaction.rb +15 -0
- data/lib/memorium/initialize_attr/template.rb +50 -0
- data/lib/memorium/initialize_attr.rb +49 -0
- data/lib/memorium/memo/eager.rb +13 -0
- data/lib/memorium/memo/pending.rb +30 -0
- data/lib/memorium/memo/template.rb +40 -0
- data/lib/memorium/memo.rb +42 -0
- data/lib/memorium/naming.rb +25 -0
- data/lib/memorium/parametrs.rb +27 -0
- data/lib/memorium/require_attr/template.rb +14 -0
- data/lib/memorium/require_attr.rb +36 -0
- data/lib/memorium/version.rb +5 -0
- data/lib/memorium.rb +19 -0
- metadata +110 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1281323555096907a8699e5dd707122af3ae2b6bcea75d12f5699504cc1c6c1a
|
|
4
|
+
data.tar.gz: 2e3f29414d57e03aac316d5d2285622c840e52501bc83877d5239e3bbed36219
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 632885b625b5e4d269b3e0c3b02172b51a5bd40a199c812fcb0bca04ecce1ae92b935aa488cfe8108a678c028a25f5703a7771ff97e31290d967588d982402fa
|
|
7
|
+
data.tar.gz: de3136ffc2b5b05ea232df2bf2bb07c2b23324e4b4fba07c4390023ad7a3fcf486bdcf8ab882779172f00f8ad4cdd2b8fb5e16eb7dea0f1f9030639871a4d8fd
|
data/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Memorium
|
|
2
|
+
|
|
3
|
+
Toolkit for creating convenient classes with lazy calculations. Provides method memoization, declarative attribute initialization, and attribute validation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "memorium"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install directly:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install memorium
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Method Memoization (`memo`)
|
|
22
|
+
|
|
23
|
+
Cache the result of a method so it's computed only once:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
class Calculator
|
|
27
|
+
memo def heavy_computation
|
|
28
|
+
sleep 2
|
|
29
|
+
42
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
calc = Calculator.new
|
|
34
|
+
calc.heavy_computation # => 42 (takes 2 seconds)
|
|
35
|
+
calc.heavy_computation # => 42 (instant, cached)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### Inline syntax
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
memo def result
|
|
42
|
+
expensive_call
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Block syntax
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
memo
|
|
50
|
+
def result
|
|
51
|
+
expensive_call
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Access levels
|
|
56
|
+
|
|
57
|
+
Control visibility of the memoized method:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
class Example
|
|
61
|
+
memo def private_writer
|
|
62
|
+
compute
|
|
63
|
+
end, writer: :private
|
|
64
|
+
|
|
65
|
+
memo writer: :private
|
|
66
|
+
def private_writer
|
|
67
|
+
compute
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Writer
|
|
73
|
+
|
|
74
|
+
You can also generate a writer to reset the memoized value:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class Example
|
|
78
|
+
memo def value
|
|
79
|
+
rand(100)
|
|
80
|
+
end, writer: :public
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
obj = Example.new
|
|
84
|
+
obj.value # => 42
|
|
85
|
+
obj.value = 999
|
|
86
|
+
obj.value # => 999
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Attribute Initialization (`initialize_attr`)
|
|
90
|
+
|
|
91
|
+
Declaratively define attributes with auto-generated `initialize`:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
class User
|
|
95
|
+
initialize_attr :name
|
|
96
|
+
initialize_attr :email, required: :not_nil?
|
|
97
|
+
initialize_attr :age, required: :nil?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
user = User.new(name: "Alice", email: "alice@example.com")
|
|
101
|
+
user.name # => "Alice"
|
|
102
|
+
user.email # => "alice@example.com"
|
|
103
|
+
user.age # => nil
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### With validation
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class User
|
|
110
|
+
initialize_attr :name, required: :not_nil?
|
|
111
|
+
initialize_attr :email, required: :not_nil?
|
|
112
|
+
initialize_attr :role, required: :nil?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
User.new(name: "Bob", email: "bob@example.com")
|
|
116
|
+
User.new(name: nil, email: "bob@example.com")
|
|
117
|
+
# => raises Memorium::AttributeRequired: Attribute `name` must be not_nil: nil
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Attribute Validation (`require_attr`)
|
|
121
|
+
|
|
122
|
+
Validate attribute values at runtime:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
class User
|
|
126
|
+
initialize_attr :name, required: :not_nil?
|
|
127
|
+
|
|
128
|
+
memo def name_caps = require_attr(:name).upcase
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
user = User.new(name: "alice")
|
|
132
|
+
user.name_caps # => "ALICE"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Available predicates:
|
|
136
|
+
- `:nil?` — value must be nil
|
|
137
|
+
- `:not_nil?` — value must not be nil
|
|
138
|
+
- `:not_empty?` — value must not be empty
|
|
139
|
+
- Any method name as a predicate (e.g., `:zero?`, `:positive?`)
|
|
140
|
+
- Custom Proc: `require_attr(:name, by: ->(v) { v.size > 2 })`
|
|
141
|
+
|
|
142
|
+
### Configuration
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# By default, nil values are also memoized
|
|
146
|
+
Memorium.memoize_nil_values = true
|
|
147
|
+
|
|
148
|
+
# Disable nil memoization (nil will be recomputed each time)
|
|
149
|
+
Memorium.memoize_nil_values = false
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then run `rake spec` to run the tests.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
bin/setup
|
|
158
|
+
rake spec
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
You can also run `bin/console` for an interactive prompt.
|
|
162
|
+
|
|
163
|
+
## Contributing
|
|
164
|
+
|
|
165
|
+
Bug reports and pull requests are welcome at https://github.com/gem-forge/memorium.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# A customer order containing line items, an optional discount, and computed totals.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# items = [
|
|
8
|
+
# OrderItem.new(name: "Coffee", quantity: 2, unit_price: 150.0),
|
|
9
|
+
# OrderItem.new(name: "Croissant", quantity: 3, unit_price: 80.0),
|
|
10
|
+
# ]
|
|
11
|
+
#
|
|
12
|
+
# order = Order.new(customer_name: "Alice", items: items, discount_percent: 10)
|
|
13
|
+
# order.subtotal # => 540.0
|
|
14
|
+
# order.discount_amount # => 54.0
|
|
15
|
+
# order.total # => 486.0
|
|
16
|
+
#
|
|
17
|
+
class Order
|
|
18
|
+
# Customer's full name.
|
|
19
|
+
# @return [String]
|
|
20
|
+
initialize_attr :customer_name, required: :not_nil?
|
|
21
|
+
|
|
22
|
+
# Line items included in the order.
|
|
23
|
+
# @return [Array<OrderItem>]
|
|
24
|
+
initialize_attr :items, required: :not_empty?
|
|
25
|
+
|
|
26
|
+
# Discount percentage to apply (0–100). +nil+ means no discount.
|
|
27
|
+
# @return [Integer, nil]
|
|
28
|
+
initialize_attr :discount_percent
|
|
29
|
+
|
|
30
|
+
# Sum of all line item totals before discount.
|
|
31
|
+
# @return [Float]
|
|
32
|
+
memo def subtotal = items.sum(&:total_price)
|
|
33
|
+
|
|
34
|
+
# Discount amount in currency units.
|
|
35
|
+
# Equals zero when {#discount_percent} is +nil+.
|
|
36
|
+
# @return [Float]
|
|
37
|
+
memo def discount_amount = subtotal * (discount_percent.to_f / 100)
|
|
38
|
+
|
|
39
|
+
# Final amount due after applying the discount.
|
|
40
|
+
# @return [Float]
|
|
41
|
+
memo def total = subtotal - discount_amount
|
|
42
|
+
|
|
43
|
+
# One-line order summary.
|
|
44
|
+
# @return [String]
|
|
45
|
+
memo def summary
|
|
46
|
+
"Order for #{customer_name}: #{items.size} item(s), total #{require_attr(:total)}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# A single line item in an order — a product with quantity and unit price.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# item = OrderItem.new(name: "Coffee", quantity: 2, unit_price: 150.0)
|
|
8
|
+
# item.total_price # => 300.0
|
|
9
|
+
#
|
|
10
|
+
class OrderItem
|
|
11
|
+
|
|
12
|
+
# Product name.
|
|
13
|
+
# @return [String]
|
|
14
|
+
initialize_attr :name, required: :not_nil?
|
|
15
|
+
|
|
16
|
+
# Number of units.
|
|
17
|
+
# @return [Integer]
|
|
18
|
+
initialize_attr :quantity, required: :not_nil?
|
|
19
|
+
|
|
20
|
+
# Price per unit in currency.
|
|
21
|
+
# @return [Float]
|
|
22
|
+
initialize_attr :unit_price, required: :not_nil?
|
|
23
|
+
|
|
24
|
+
# Total cost of this line item (quantity × unit price).
|
|
25
|
+
# @return [Float]
|
|
26
|
+
memo def total_price = quantity * unit_price
|
|
27
|
+
|
|
28
|
+
# Human-readable representation of the line item.
|
|
29
|
+
# @return [String]
|
|
30
|
+
memo def to_s = "#{name} x#{quantity} @ #{unit_price} = #{total_price}"
|
|
31
|
+
end
|
data/examples/order.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "memorium"
|
|
4
|
+
require_relative "order/order_item"
|
|
5
|
+
require_relative "order/order"
|
|
6
|
+
|
|
7
|
+
items = [
|
|
8
|
+
OrderItem.new(name: "Coffee", quantity: 2, unit_price: 150.0),
|
|
9
|
+
OrderItem.new(name: "Croissant", quantity: 3, unit_price: 80.0),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
order = Order.new(customer_name: "Alice", items: items, discount_percent: 10)
|
|
13
|
+
|
|
14
|
+
puts "Line items:"
|
|
15
|
+
items.each { |item| puts " #{item}" }
|
|
16
|
+
puts
|
|
17
|
+
puts "Subtotal : #{order.subtotal}" # => 540.0
|
|
18
|
+
puts "Discount (10%) : #{order.discount_amount}" # => 54.0
|
|
19
|
+
puts "Total : #{order.total}" # => 486.0
|
|
20
|
+
puts order.summary
|
|
21
|
+
puts
|
|
22
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
2024-01-15 10:23:45 INFO App started
|
|
2
|
+
2024-01-15 10:24:01 DEBUG Loading config from /etc/app/config.yml
|
|
3
|
+
2024-01-15 10:24:03 INFO Connected to database
|
|
4
|
+
2024-01-15 10:31:17 WARN Memory usage above 80%
|
|
5
|
+
2024-01-15 10:45:02 ERROR Disk full on /dev/sda1
|
|
6
|
+
2024-01-15 10:45:03 ERROR Failed to write temp file: /tmp/upload_xyz
|
|
7
|
+
2024-01-15 11:00:00 INFO Scheduled cleanup started
|
|
8
|
+
2024-01-15 11:00:04 INFO Scheduled cleanup finished
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Stub that simulates a database connection.
|
|
5
|
+
# Stores inserted records in memory and prints each insert to stdout.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# db = Database.new
|
|
9
|
+
# db.insert(level: "ERROR", message: "Oops", date: "2024-01-15", time: "10:45:02")
|
|
10
|
+
#
|
|
11
|
+
class Database
|
|
12
|
+
# In-memory store of all inserted records.
|
|
13
|
+
# @return [Array<Hash>]
|
|
14
|
+
memo def records = []
|
|
15
|
+
|
|
16
|
+
# Simulates inserting a log record into the database.
|
|
17
|
+
# @param [Hash] attrs Column values to insert.
|
|
18
|
+
def insert(attrs)
|
|
19
|
+
records << attrs
|
|
20
|
+
puts " [DB] INSERT log_entries #{attrs.inspect}"
|
|
21
|
+
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# DTO that parses a single raw log line into structured fields.
|
|
5
|
+
#
|
|
6
|
+
# Expected format: +YYYY-MM-DD HH:MM:SS LEVEL message text+
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# entry = LogEntry.new(raw: "2024-01-15 10:23:45 ERROR Disk full on /dev/sda1")
|
|
10
|
+
# entry.level # => "ERROR"
|
|
11
|
+
# entry.message # => "Disk full on /dev/sda1"
|
|
12
|
+
# entry.error? # => true
|
|
13
|
+
#
|
|
14
|
+
class LogEntry
|
|
15
|
+
# Raw log line to parse.
|
|
16
|
+
# @return [String]
|
|
17
|
+
initialize_attr :raw, required: :not_empty?
|
|
18
|
+
|
|
19
|
+
# Date portion of the timestamp (YYYY-MM-DD).
|
|
20
|
+
# @return [String]
|
|
21
|
+
memo def date = parts[0]
|
|
22
|
+
|
|
23
|
+
# Time portion of the timestamp (HH:MM:SS).
|
|
24
|
+
# @return [String]
|
|
25
|
+
memo def time = parts[1]
|
|
26
|
+
|
|
27
|
+
# Severity level (DEBUG / INFO / WARN / ERROR).
|
|
28
|
+
# @return [String]
|
|
29
|
+
memo def level = parts[2]
|
|
30
|
+
|
|
31
|
+
# Human-readable log message (everything after the level token).
|
|
32
|
+
# @return [String]
|
|
33
|
+
memo def message = parts[3..].join(" ")
|
|
34
|
+
|
|
35
|
+
# Returns +true+ when the entry has ERROR severity.
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
memo def error? = level == "ERROR"
|
|
38
|
+
|
|
39
|
+
# Returns +true+ when the entry has WARN severity.
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
memo def warn? = level == "WARN"
|
|
42
|
+
|
|
43
|
+
# One-line string representation of the entry.
|
|
44
|
+
# @return [String]
|
|
45
|
+
memo def to_s = "[#{date} #{time}] #{level.ljust(5)} #{message}"
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Raw line split into tokens.
|
|
50
|
+
# @return [Array<String>]
|
|
51
|
+
memo def parts = raw.split(" ")
|
|
52
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Lint/MissingCopEnableDirective
|
|
4
|
+
# rubocop:disable Layout/EndAlignment
|
|
5
|
+
# rubocop:disable Style/TrailingBodyOnClass
|
|
6
|
+
|
|
7
|
+
#
|
|
8
|
+
# Reads raw log lines from a file, parses them into LogEntry,
|
|
9
|
+
# and persists each entry into the database.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# SaveLogs.new(log_path: "examples/save_logs/app.log", db: Database.new).call
|
|
13
|
+
#
|
|
14
|
+
class SaveLogs
|
|
15
|
+
initialize_attr :log_path, required: :not_nil?
|
|
16
|
+
initialize_attr :db, required: :not_nil?
|
|
17
|
+
|
|
18
|
+
#
|
|
19
|
+
# Parses the log file and saves every entry to the database.
|
|
20
|
+
#
|
|
21
|
+
def call
|
|
22
|
+
persist_entries
|
|
23
|
+
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#--------------------------------------- Attributes
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
#
|
|
31
|
+
# Parsed LogEntry objects built from raw file lines.
|
|
32
|
+
#
|
|
33
|
+
# @return [Array<LogEntry>]
|
|
34
|
+
#
|
|
35
|
+
memo def entries
|
|
36
|
+
raw_lines.map { |line| LogEntry.new(raw: line) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
#
|
|
40
|
+
# Raw non-empty lines read from the log file.
|
|
41
|
+
#
|
|
42
|
+
# @return [Array<String>]
|
|
43
|
+
#
|
|
44
|
+
memo def raw_lines
|
|
45
|
+
File.readlines(log_path, chomp: true).reject(&:empty?)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
#--------------------------------------- Executors
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
#
|
|
52
|
+
# Inserts every parsed entry into the database.
|
|
53
|
+
#
|
|
54
|
+
def persist_entries
|
|
55
|
+
entries.each { |entry| persist_entry(entry) }
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#
|
|
61
|
+
# Inserts a single log entry into the database.
|
|
62
|
+
#
|
|
63
|
+
# @param [LogEntry] entry The entry to persist.
|
|
64
|
+
#
|
|
65
|
+
def persist_entry(entry)
|
|
66
|
+
db.insert(
|
|
67
|
+
date: entry.date,
|
|
68
|
+
time: entry.time,
|
|
69
|
+
level: entry.level,
|
|
70
|
+
message: entry.message,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "memorium"
|
|
4
|
+
require_relative "save_logs/log_entry"
|
|
5
|
+
require_relative "save_logs/database"
|
|
6
|
+
require_relative "save_logs/save_logs"
|
|
7
|
+
|
|
8
|
+
db = Database.new
|
|
9
|
+
|
|
10
|
+
SaveLogs.new(
|
|
11
|
+
log_path: File.join(__dir__, "save_logs/app.log"),
|
|
12
|
+
db: db,
|
|
13
|
+
).call
|
|
14
|
+
|
|
15
|
+
puts
|
|
16
|
+
puts "Saved #{db.records.size} entries total."
|
|
17
|
+
puts "Errors: #{db.records.count { |r| r[:level] == "ERROR" }}"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class Memorium::AccessLevel
|
|
2
|
+
PUBLIC = :public
|
|
3
|
+
PRIVATE = :private
|
|
4
|
+
PROTECTED = :protected
|
|
5
|
+
|
|
6
|
+
attr_reader :klass, :method_name, :access_level
|
|
7
|
+
|
|
8
|
+
def initialize(klass, method_name)
|
|
9
|
+
@klass = klass
|
|
10
|
+
@method_name = method_name
|
|
11
|
+
|
|
12
|
+
@access_level = define_access_level
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def copy_to(target_method_name) = apply_level access_level, target_method_name
|
|
16
|
+
def apply_level(level, name) = klass.send(level, name)
|
|
17
|
+
|
|
18
|
+
def apply_or_copy(level, target_method_name)
|
|
19
|
+
level.nil? ? copy_to(target_method_name) : apply_level(level, target_method_name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def define_access_level
|
|
26
|
+
if klass.private_method_defined?(method_name) then PRIVATE
|
|
27
|
+
elsif klass.protected_method_defined?(method_name) then PROTECTED
|
|
28
|
+
elsif klass.public_method_defined?(method_name) then PUBLIC
|
|
29
|
+
else raise "Method #{method_name.inspect} not defined"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class Memorium::Core
|
|
2
|
+
|
|
3
|
+
attr_reader :klass
|
|
4
|
+
|
|
5
|
+
def initialize(klass)
|
|
6
|
+
@klass = klass
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def eager_memo = @eager_memo ||= Memorium::Memo::Eager.new(klass)
|
|
10
|
+
def pending_memo = @pending_memo ||= Memorium::Memo::Pending.new(klass)
|
|
11
|
+
|
|
12
|
+
def initializer = @initializer ||= Memorium::InitializeAttr::Interaction.new(klass)
|
|
13
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Memorium::Extension
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do |klass|
|
|
5
|
+
Memorium::RequireAttr.create_method(klass)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def memorium = @memorium ||= Memorium::Core.new(self)
|
|
10
|
+
|
|
11
|
+
def memo(method_name = nil, reader: nil, writer: nil)
|
|
12
|
+
if method_name.nil?
|
|
13
|
+
memorium.pending_memo.setup(reader:, writer:)
|
|
14
|
+
else
|
|
15
|
+
memorium.eager_memo.apply(method_name, reader:, writer:)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def method_added(method_name)
|
|
20
|
+
super
|
|
21
|
+
return if @memorium.nil? || !memorium.pending_memo.ready?
|
|
22
|
+
|
|
23
|
+
memorium.pending_memo.apply(method_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize_attr(attr_name, reader: nil, writer: nil, required: nil)
|
|
27
|
+
memorium.initializer.define(attr_name, reader:, writer:, required:)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class Memorium::InitializeAttr::Interaction
|
|
2
|
+
attr_reader :klass, :initialized
|
|
3
|
+
|
|
4
|
+
def initialize(klass)
|
|
5
|
+
@klass = klass
|
|
6
|
+
|
|
7
|
+
@initialized = false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def define(name, reader:, writer:, required:)
|
|
11
|
+
Memorium::InitializeAttr.new(klass, name, reader:, writer:, required:, initialized:).define
|
|
12
|
+
@initialized ||= true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
class Memorium::InitializeAttr::Template
|
|
2
|
+
attr_reader :name
|
|
3
|
+
|
|
4
|
+
def initialize(name)
|
|
5
|
+
@name = name
|
|
6
|
+
|
|
7
|
+
@result = []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def result = @result.join("\n")
|
|
11
|
+
|
|
12
|
+
def initializer
|
|
13
|
+
@result << <<~RUBY
|
|
14
|
+
def initialize(attributes = nil, **kw_attributes)
|
|
15
|
+
#{Memorium::InitializeAttr::ACCESSOR} = attributes || kw_attributes
|
|
16
|
+
end
|
|
17
|
+
RUBY
|
|
18
|
+
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def reader
|
|
22
|
+
@result << <<~RUBY
|
|
23
|
+
def #{name.original}
|
|
24
|
+
return #{name.attribute} if defined?(#{name.attribute})
|
|
25
|
+
#{name.attribute} = #{Memorium::InitializeAttr::ACCESSOR}[#{name.original.inspect}]
|
|
26
|
+
end
|
|
27
|
+
RUBY
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reader_with_require(check_type)
|
|
32
|
+
@result << <<~RUBY
|
|
33
|
+
def #{name.original}
|
|
34
|
+
return #{name.attribute} if defined?(#{name.attribute})
|
|
35
|
+
#{name.attribute} = #{Memorium::InitializeAttr::ACCESSOR}[#{name.original.inspect}]
|
|
36
|
+
require_attr(#{name.original.inspect}, by: #{check_type.inspect})
|
|
37
|
+
end
|
|
38
|
+
RUBY
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def writer
|
|
43
|
+
@result << <<~RUBY
|
|
44
|
+
def #{name.setter}(value)
|
|
45
|
+
#{name.attribute} = value
|
|
46
|
+
end
|
|
47
|
+
RUBY
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
class Memorium::InitializeAttr
|
|
2
|
+
ACCESSOR = '@__initialize_attributes'.freeze
|
|
3
|
+
|
|
4
|
+
attr_reader :klass, :attr_name, :reader_level, :writer_level, :required, :initialized
|
|
5
|
+
|
|
6
|
+
def initialize(klass, attr_name, reader: nil, writer: nil, required: nil, initialized: false)
|
|
7
|
+
@klass = klass
|
|
8
|
+
@attr_name = attr_name
|
|
9
|
+
@reader_level = reader || Memorium::AccessLevel::PUBLIC
|
|
10
|
+
@writer_level = writer || Memorium::AccessLevel::PUBLIC
|
|
11
|
+
@required = required
|
|
12
|
+
|
|
13
|
+
@initialized = initialized
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def define
|
|
17
|
+
create_methods
|
|
18
|
+
|
|
19
|
+
apply_reader_access_level
|
|
20
|
+
apply_writer_access_level
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def naming = @naming ||= Memorium::Naming.new(attr_name)
|
|
26
|
+
def access_level = @access_level ||= Memorium::AccessLevel.new(klass, attr_name)
|
|
27
|
+
|
|
28
|
+
def template
|
|
29
|
+
@template ||= begin
|
|
30
|
+
template = Template.new(naming)
|
|
31
|
+
template.initializer unless initialized
|
|
32
|
+
|
|
33
|
+
required.nil? ? template.reader : template.reader_with_require(required_type)
|
|
34
|
+
|
|
35
|
+
template.writer
|
|
36
|
+
template
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def required_type = @required_type ||= required.to_s
|
|
41
|
+
|
|
42
|
+
def create_methods = klass.class_eval(template.result, caller_location.path, caller_location.lineno + 1)
|
|
43
|
+
|
|
44
|
+
def apply_reader_access_level = access_level.apply_level(reader_level, naming.original)
|
|
45
|
+
def apply_writer_access_level = access_level.apply_level(writer_level, naming.setter)
|
|
46
|
+
|
|
47
|
+
# OPTIMIZE: Хрупкий caller_location
|
|
48
|
+
def caller_location = @caller_location ||= caller_locations(5, 1).first
|
|
49
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class Memorium::Memo::Pending
|
|
2
|
+
|
|
3
|
+
attr_accessor :reader, :writer, :klass, :is_ready
|
|
4
|
+
|
|
5
|
+
def initialize(klass)
|
|
6
|
+
@klass = klass
|
|
7
|
+
@is_ready = false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
alias ready? is_ready
|
|
11
|
+
|
|
12
|
+
def setup(reader:, writer:)
|
|
13
|
+
self.is_ready = true
|
|
14
|
+
self.reader = reader
|
|
15
|
+
self.writer = writer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def discard
|
|
19
|
+
self.is_ready = false
|
|
20
|
+
self.reader = nil
|
|
21
|
+
self.writer = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def apply(name)
|
|
25
|
+
memo = Memorium::Memo.new(klass, name, reader:, writer:)
|
|
26
|
+
discard
|
|
27
|
+
memo.apply
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class Memorium::Memo::Template
|
|
2
|
+
attr_reader :name, :params
|
|
3
|
+
|
|
4
|
+
def initialize(name, params)
|
|
5
|
+
@name = name
|
|
6
|
+
@params = params
|
|
7
|
+
|
|
8
|
+
@result = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def result = @result.join("\n")
|
|
12
|
+
|
|
13
|
+
def reader_plain
|
|
14
|
+
@result << <<~RUBY
|
|
15
|
+
def #{name.original}(#{params.signature})
|
|
16
|
+
#{name.pure_memoized} ||= #{name.memoized}(#{params.parametrize})
|
|
17
|
+
end
|
|
18
|
+
RUBY
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reader_memoize_nil
|
|
23
|
+
@result << <<~RUBY
|
|
24
|
+
def #{name.original}(#{params.signature})
|
|
25
|
+
return #{name.pure_memoized} if defined?(#{name.pure_memoized})
|
|
26
|
+
#{name.pure_memoized} = #{name.memoized}(#{params.parametrize})
|
|
27
|
+
end
|
|
28
|
+
RUBY
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def writer
|
|
33
|
+
@result << <<~RUBY
|
|
34
|
+
def #{name.setter}(value)
|
|
35
|
+
#{name.pure_memoized} = value
|
|
36
|
+
end
|
|
37
|
+
RUBY
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
class Memorium::Memo
|
|
2
|
+
attr_reader :klass, :method_name, :reader_level, :writer_level
|
|
3
|
+
|
|
4
|
+
def initialize(klass, method_name, reader: nil, writer: nil)
|
|
5
|
+
@klass = klass
|
|
6
|
+
@method_name = method_name
|
|
7
|
+
@reader_level = reader
|
|
8
|
+
@writer_level = writer
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def apply
|
|
12
|
+
create_alias
|
|
13
|
+
create_methods
|
|
14
|
+
|
|
15
|
+
apply_reader_access_level
|
|
16
|
+
apply_writer_access_level
|
|
17
|
+
apply_alias_access_level
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def naming = @naming ||= Memorium::Naming.new(method_name)
|
|
23
|
+
def parametrs = @parametrs ||= Memorium::Parametrs.new(method)
|
|
24
|
+
def access_level = @access_level ||= Memorium::AccessLevel.new(klass, naming.memoized)
|
|
25
|
+
|
|
26
|
+
def method = @method ||= klass.instance_method(method_name)
|
|
27
|
+
|
|
28
|
+
def template
|
|
29
|
+
@template ||= begin
|
|
30
|
+
template = Template.new(naming, parametrs)
|
|
31
|
+
Memorium.config.memoize_nil_values ? template.reader_memoize_nil : template.reader_plain
|
|
32
|
+
template.writer
|
|
33
|
+
template
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create_alias = klass.alias_method naming.memoized, naming.original
|
|
38
|
+
def create_methods = klass.class_eval template.result, __FILE__, __LINE__ + 1
|
|
39
|
+
def apply_reader_access_level = access_level.apply_or_copy(reader_level, naming.original)
|
|
40
|
+
def apply_writer_access_level = access_level.apply_or_copy(writer_level, naming.setter)
|
|
41
|
+
def apply_alias_access_level = access_level.apply_level(Memorium::AccessLevel::PRIVATE, naming.memoized)
|
|
42
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class Memorium::Naming
|
|
2
|
+
|
|
3
|
+
def initialize(method_name)
|
|
4
|
+
@method_name = method_name
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def original = @original ||= @method_name
|
|
8
|
+
def pure = @pure ||= clear_name(original)
|
|
9
|
+
def memoized = @memoized ||= "__memo_original_#{original}"
|
|
10
|
+
def pure_memoized = @pure_memoized ||= "@__memo_#{pure}"
|
|
11
|
+
def attribute = @attribute ||= "@#{pure}"
|
|
12
|
+
def setter = @setter ||= "#{pure}="
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def clear_name(name)
|
|
17
|
+
stripped = name.to_s.delete_suffix('?').delete_suffix('!')
|
|
18
|
+
|
|
19
|
+
case name[-1]
|
|
20
|
+
when '?' then "is_#{stripped}"
|
|
21
|
+
when '!' then "to_#{stripped}"
|
|
22
|
+
else stripped
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class Memorium::Parametrs
|
|
2
|
+
attr_reader :method
|
|
3
|
+
|
|
4
|
+
def initialize(method)
|
|
5
|
+
@method = method
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def signature = @signature ||= parse_signature
|
|
9
|
+
def parametrize = @parametrize ||= signature.gsub(/=[^,]+/, '')
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def parse_signature
|
|
14
|
+
method.parameters.map do |type, name|
|
|
15
|
+
case type
|
|
16
|
+
when :req then name.to_s
|
|
17
|
+
when :opt then "#{name}=nil"
|
|
18
|
+
when :keyreq then "#{name}:"
|
|
19
|
+
when :key then "#{name}: nil"
|
|
20
|
+
when :rest then "*#{name}"
|
|
21
|
+
when :keyrest then "**#{name}"
|
|
22
|
+
when :block then "&#{name}"
|
|
23
|
+
end
|
|
24
|
+
end.join(", ")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class Memorium::RequireAttr::Template
|
|
2
|
+
|
|
3
|
+
def require_method
|
|
4
|
+
<<~RUBY
|
|
5
|
+
def require_attr(attr_name, by: :not_nil?)
|
|
6
|
+
attribute = send(attr_name)
|
|
7
|
+
require_attribute = Memorium::RequireAttr.new(attr_name, by:, value: attribute)
|
|
8
|
+
raise(Memorium::AttributeRequired, require_attribute.raise_text, caller) unless require_attribute.check_requirements
|
|
9
|
+
attribute
|
|
10
|
+
end
|
|
11
|
+
RUBY
|
|
12
|
+
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class Memorium::RequireAttr
|
|
2
|
+
attr_reader :attr_name, :by, :value
|
|
3
|
+
|
|
4
|
+
def self.create_method(klass) = klass.class_eval Template.new.require_method, caller_location.path, caller_location.lineno + 1
|
|
5
|
+
def self.caller_location = caller_locations(4, 1).first
|
|
6
|
+
|
|
7
|
+
def initialize(attr_name, by:, value:)
|
|
8
|
+
@attr_name = attr_name.to_s
|
|
9
|
+
@by = by.is_a?(Proc) ? by : by.to_s
|
|
10
|
+
@value = value
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def check_requirements
|
|
14
|
+
return by.call(value) if by.is_a?(Proc)
|
|
15
|
+
|
|
16
|
+
inverse_type? ? inverse_check_by_type : check_by_type
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def raise_text
|
|
20
|
+
@raise_text ||= begin
|
|
21
|
+
return "Attribute `#{attr_name}` not pass the condition: #{value.inspect}" if by.is_a?(Proc)
|
|
22
|
+
|
|
23
|
+
"Attribute `#{attr_name}` #{raise_verb} be #{unpredicated_type}: #{value.inspect}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def inverse_type? = @is_inverse_type ||= by.start_with?('not_')
|
|
30
|
+
def cleared_type = @cleared_type ||= by.delete_prefix('not_')
|
|
31
|
+
def unpredicated_type = @pure_type ||= cleared_type.chomp('?')
|
|
32
|
+
def raise_verb = @raise_verb ||= inverse_type? ? 'must not' : 'must'
|
|
33
|
+
|
|
34
|
+
def check_by_type = value.public_send(cleared_type)
|
|
35
|
+
def inverse_check_by_type = !value.public_send(cleared_type)
|
|
36
|
+
end
|
data/lib/memorium.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zeitwerk"
|
|
4
|
+
require "active_support/concern"
|
|
5
|
+
require "active_support/configurable"
|
|
6
|
+
require_relative "memorium/version"
|
|
7
|
+
|
|
8
|
+
loader = Zeitwerk::Loader.for_gem
|
|
9
|
+
loader.setup
|
|
10
|
+
|
|
11
|
+
module Memorium
|
|
12
|
+
include Config
|
|
13
|
+
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
class AttributeRequired < Error; end
|
|
16
|
+
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Object.include(Memorium::Extension)
|
metadata
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: memorium
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Danila Fedorov
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: zeitwerk
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.12'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.12'
|
|
54
|
+
description: Toolkit for creating convenient classes with lazy calculations.
|
|
55
|
+
email:
|
|
56
|
+
- dan2065@mail.ru
|
|
57
|
+
executables: []
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- README.md
|
|
62
|
+
- Rakefile
|
|
63
|
+
- examples/order.rb
|
|
64
|
+
- examples/order/order.rb
|
|
65
|
+
- examples/order/order_item.rb
|
|
66
|
+
- examples/save_logs.rb
|
|
67
|
+
- examples/save_logs/app.log
|
|
68
|
+
- examples/save_logs/database.rb
|
|
69
|
+
- examples/save_logs/log_entry.rb
|
|
70
|
+
- examples/save_logs/save_logs.rb
|
|
71
|
+
- lib/memorium.rb
|
|
72
|
+
- lib/memorium/access_level.rb
|
|
73
|
+
- lib/memorium/config.rb
|
|
74
|
+
- lib/memorium/core.rb
|
|
75
|
+
- lib/memorium/extension.rb
|
|
76
|
+
- lib/memorium/initialize_attr.rb
|
|
77
|
+
- lib/memorium/initialize_attr/interaction.rb
|
|
78
|
+
- lib/memorium/initialize_attr/template.rb
|
|
79
|
+
- lib/memorium/memo.rb
|
|
80
|
+
- lib/memorium/memo/eager.rb
|
|
81
|
+
- lib/memorium/memo/pending.rb
|
|
82
|
+
- lib/memorium/memo/template.rb
|
|
83
|
+
- lib/memorium/naming.rb
|
|
84
|
+
- lib/memorium/parametrs.rb
|
|
85
|
+
- lib/memorium/require_attr.rb
|
|
86
|
+
- lib/memorium/require_attr/template.rb
|
|
87
|
+
- lib/memorium/version.rb
|
|
88
|
+
homepage: https://github.com/gem-forge/memorium
|
|
89
|
+
licenses: []
|
|
90
|
+
metadata:
|
|
91
|
+
homepage_uri: https://github.com/gem-forge/memorium
|
|
92
|
+
source_code_uri: https://github.com/gem-forge/memorium
|
|
93
|
+
rdoc_options: []
|
|
94
|
+
require_paths:
|
|
95
|
+
- lib
|
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: 3.1.0
|
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
requirements: []
|
|
107
|
+
rubygems_version: 3.6.8
|
|
108
|
+
specification_version: 4
|
|
109
|
+
summary: Toolkit for creating convenient classes with lazy calculations.
|
|
110
|
+
test_files: []
|