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 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -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,9 @@
1
+ module Memorium::Config
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ include ActiveSupport::Configurable
6
+
7
+ config_accessor(:memoize_nil_values) { true }
8
+ end
9
+ 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,13 @@
1
+ class Memorium::Memo::Eager
2
+
3
+ attr_accessor :klass
4
+
5
+ def initialize(klass)
6
+ @klass = klass
7
+ end
8
+
9
+ def apply(name, reader:, writer:)
10
+ Memorium::Memo.new(klass, name, reader:, writer:).apply
11
+ end
12
+
13
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Memorium
4
+ VERSION = "1.0.0"
5
+ 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: []