├── .gitignore
├── .rspec
├── .rubocop.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── Gemfile.lock
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
├── console
└── setup
├── lib
├── smart_core.rb
└── smart_core
│ ├── engine.rb
│ ├── engine
│ ├── atom.rb
│ ├── cache.rb
│ ├── ext.rb
│ ├── frozener.rb
│ ├── lock.rb
│ ├── read_write_lock.rb
│ ├── rescue_ext.rb
│ └── version.rb
│ ├── errors.rb
│ ├── ext.rb
│ └── ext
│ └── basic_object_as_object.rb
├── smart_engine.gemspec
└── spec
├── spec_helper.rb
└── units
├── atom_spec.rb
├── cache_spec.rb
├── extensions
└── basic_object_as_object_spec.rb
├── frozener_spec.rb
├── inline_rescue_pipe_spec.rb
├── lock_spec.rb
├── read_write_lock_spec.rb
└── version_spec.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 | .rspec_status
10 | /.idea
11 | .ruby-version
12 | *.gem
13 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format progress
2 | --color
3 | --require spec_helper
4 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_gem:
2 | armitage-rubocop:
3 | - lib/rubocop.general.yml
4 | - lib/rubocop.rake.yml
5 | - lib/rubocop.rspec.yml
6 |
7 | AllCops:
8 | TargetRubyVersion: 3.1.0
9 | NewCops: enable
10 | Include:
11 | - lib/**/*.rb
12 | - spec/**/*.rb
13 | - Gemfile
14 | - Rakefile
15 | - smart_engine.gemspec
16 | - bin/console
17 |
18 | # NOTE: for better representativeness of test examples
19 | RSpec/DescribedClass:
20 | Enabled: false
21 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | ## [0.17.0] - 2022-10-14
5 | ### Changed
6 | - **SmartCore::Engine::ReadWriteLock**: allow #read_sync invocations inside #write_sync;
7 |
8 | ## [0.16.0] - 2022-09-30
9 | ### Changed
10 | - `SmartCore::Engine::ReadWriteLock` does not lock the current thread if the current thread has already acquired the lock;
11 |
12 | ## [0.15.0] - 2022-09-30
13 | ### Added
14 | - `SmartCore::Engine::ReadWriteLock#write_owned?` - checking that write lock is owned by current thread or not;
15 |
16 | ## [0.14.0] - 2022-09-30
17 | ### Added
18 | - Read/Write locking mechanizm: `SmartCore::Engine::ReadWriteLock`;
19 |
20 | ## [0.13.0] - 2022-09-30
21 | ### Added
22 | - Simplest in-memory cache storage implementation: `SmartCore::Engine::Cache`;
23 | ### Changed
24 | - Minimal Ruby version is `2.5` (`>= 2.5`);
25 | - Better `BasicObject`'s refinement extention specs;
26 | - Updated development dependencies;
27 |
28 | ## [0.12.0] - 2021-12-09
29 | ### Added
30 | - `using SmartCore::Ext::BasicObjectAsObject` provides native support for:
31 | - `BasicObject#inspect`;
32 |
33 | ## [0.11.0] - 2021-01-17
34 | ### Added
35 | - Support for **Ruby@3**;
36 |
37 | ## [0.10.0] - 2020-12-22
38 | ### Added
39 | - Support for `#hash` and `#instance_of?` for `SmartCore::Ext::BasicObjectAsObject` refinement;
40 |
41 | ## [0.9.0] - 2020-12-20
42 | ### Added
43 | - New type of utilities: *Extensions* (`SmartCore::Ext`);
44 | - New extension: `SmartCore::Ext::BasicObjectAsObject` refinement:
45 | - `using SmartCore::Ext::BasicObjectAsObject` provides native support for:
46 | - `BasicObject#is_a?`;
47 | - `BasicObject#kind_of?`;
48 | - `BasicObject#freeze`;
49 | - `BasicObject#frozen?`;
50 |
51 | ### Changed
52 | - Updated development dependencies;
53 | - Support for *Ruby@2.4* has ended;
54 |
55 | ### Fixed
56 | - `SmartCore::Engine::Frozener` can not be used with rubies lower than `@2.7`;
57 |
58 | ## [0.8.0] - 2020-07-25
59 | ### Added
60 | - Any object frozener (`SmartCore::Engine::Frozener`, `SmartCore::Engine::Frozener::Mixin`);
61 |
62 | ## [0.7.0] - 2020-07-03
63 | ### Added
64 | - Atomic threadsafe value container (`SmartCore::Engine::Atom`);
65 |
66 | ## [0.6.0] - 2020-05-03
67 | ### Added
68 | - Inline rescue pipe (`SmartCore::Engine::RescueExt.inline_rescue_pipe`);
69 | - Actualized development dependencies and test environment;
70 |
71 | ## [0.5.0] - 2020-01-22
72 | ### Added
73 | - Global error type `SmartCore::TypeError` inherited from `::TypeError`;
74 |
75 | ## [0.4.0] - 2020-01-19
76 | ### Added
77 | - `SmartCore::Engine::Lock` - simple reentrant-based locking primitive;
78 |
79 | ## [0.3.0] - 2020-01-17
80 | ### Added
81 | - Global error type `SmartCore::NameError` inherited from `::NameError`;
82 |
83 | ### Changed
84 | - Actualized development dependencies;
85 |
86 | ### Fixed
87 | - Invalid gem requirement in `bin/console`;
88 |
89 | ## [0.2.0] - 2020-01-02
90 | ### Changed
91 | - `SmartCore::FrozenError` inherits classic `::FrozenError` behaviour for `Ruby >= 2.5.0` and old `::RuntimeError` behaviour for `Ruby < 2.5.0`;
92 |
93 | ## [0.1.0] - 2020-01-02
94 |
95 | - Minimalistic Release :)
96 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at iamdaiver@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: https://contributor-covenant.org
74 | [version]: https://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | gemspec
6 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | smart_engine (0.17.0)
5 |
6 | GEM
7 | remote: https://rubygems.org/
8 | specs:
9 | activesupport (7.0.4)
10 | concurrent-ruby (~> 1.0, >= 1.0.2)
11 | i18n (>= 1.6, < 2)
12 | minitest (>= 5.1)
13 | tzinfo (~> 2.0)
14 | armitage-rubocop (1.30.1.1)
15 | rubocop (= 1.30.1)
16 | rubocop-performance (= 1.14.2)
17 | rubocop-rails (= 2.15.0)
18 | rubocop-rake (= 0.6.0)
19 | rubocop-rspec (= 2.11.1)
20 | ast (2.4.2)
21 | coderay (1.1.3)
22 | concurrent-ruby (1.1.10)
23 | diff-lcs (1.5.0)
24 | docile (1.4.0)
25 | i18n (1.12.0)
26 | concurrent-ruby (~> 1.0)
27 | method_source (1.0.0)
28 | minitest (5.16.3)
29 | parallel (1.22.1)
30 | parser (3.1.2.1)
31 | ast (~> 2.4.1)
32 | pry (0.14.1)
33 | coderay (~> 1.1)
34 | method_source (~> 1.0)
35 | rack (3.0.0)
36 | rainbow (3.1.1)
37 | rake (13.0.6)
38 | regexp_parser (2.6.0)
39 | rexml (3.2.5)
40 | rspec (3.11.0)
41 | rspec-core (~> 3.11.0)
42 | rspec-expectations (~> 3.11.0)
43 | rspec-mocks (~> 3.11.0)
44 | rspec-core (3.11.0)
45 | rspec-support (~> 3.11.0)
46 | rspec-expectations (3.11.1)
47 | diff-lcs (>= 1.2.0, < 2.0)
48 | rspec-support (~> 3.11.0)
49 | rspec-mocks (3.11.1)
50 | diff-lcs (>= 1.2.0, < 2.0)
51 | rspec-support (~> 3.11.0)
52 | rspec-support (3.11.1)
53 | rubocop (1.30.1)
54 | parallel (~> 1.10)
55 | parser (>= 3.1.0.0)
56 | rainbow (>= 2.2.2, < 4.0)
57 | regexp_parser (>= 1.8, < 3.0)
58 | rexml (>= 3.2.5, < 4.0)
59 | rubocop-ast (>= 1.18.0, < 2.0)
60 | ruby-progressbar (~> 1.7)
61 | unicode-display_width (>= 1.4.0, < 3.0)
62 | rubocop-ast (1.21.0)
63 | parser (>= 3.1.1.0)
64 | rubocop-performance (1.14.2)
65 | rubocop (>= 1.7.0, < 2.0)
66 | rubocop-ast (>= 0.4.0)
67 | rubocop-rails (2.15.0)
68 | activesupport (>= 4.2.0)
69 | rack (>= 1.1)
70 | rubocop (>= 1.7.0, < 2.0)
71 | rubocop-rake (0.6.0)
72 | rubocop (~> 1.0)
73 | rubocop-rspec (2.11.1)
74 | rubocop (~> 1.19)
75 | ruby-progressbar (1.11.0)
76 | simplecov (0.21.2)
77 | docile (~> 1.1)
78 | simplecov-html (~> 0.11)
79 | simplecov_json_formatter (~> 0.1)
80 | simplecov-html (0.12.3)
81 | simplecov_json_formatter (0.1.4)
82 | tzinfo (2.0.5)
83 | concurrent-ruby (~> 1.0)
84 | unicode-display_width (2.3.0)
85 |
86 | PLATFORMS
87 | arm64-darwin-21
88 |
89 | DEPENDENCIES
90 | armitage-rubocop (~> 1.30)
91 | bundler (~> 2.3)
92 | pry (~> 0.14)
93 | rake (~> 13.0)
94 | rspec (~> 3.11)
95 | simplecov (~> 0.21)
96 | smart_engine!
97 |
98 | BUNDLED WITH
99 | 2.3.17
100 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020-2024 Rustam Ibragimov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SmartCore::Engine ·
· [](https://badge.fury.io/rb/smart_engine)
2 |
3 | Generic SmartCore functionality.
4 |
5 | ---
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ---
14 |
15 | ## Installation
16 |
17 | ```ruby
18 | gem 'smart_engine'
19 | ```
20 |
21 | ```shell
22 | bundle install
23 | # --- or ---
24 | gem install smart_engine
25 | ```
26 |
27 | ```ruby
28 | require 'smart_core'
29 | ```
30 |
31 | ---
32 |
33 | ## Technologies
34 |
35 | - [Global set of error types](#global-set-of-error-types)
36 | - [Simple reentrant lock](#simple-reentrant-lock)
37 | - [Read/Write Lock](#readwrite-lock)
38 | - [Cache Storage](#cache-storage)
39 | - [Atomic thread-safe value container](#atomic-thread-safe-value-container)
40 | - [Any Object Frozener](#any-object-frozener) (classic c-level `frozen?`/`freeze`)
41 | - [Basic Object Refinements](#basic-object-refinements) (`SmartCore::Ext::BasicObjectAsObject`)
42 | - [Inline rescue pipe](#inline-rescue-pipe)
43 |
44 |
45 | ---
46 |
47 | ### Global set of error types
48 |
49 | - `SmartCore::Error` (inherited from `::StandardError`);
50 | - `SmartCore::ArgumentError` (inherited from `::ArgumentError`);
51 | - `SmartCore::FrozenError` (inherited from `::FrozenError`);
52 | - `SmartCore::NameError` (inherited from `::NameError`);
53 | - `SmartCore::TypeError` (inherited from `::TypeError`);
54 |
55 | ---
56 |
57 | ### Simple reentrant lock
58 |
59 | ```ruby
60 | lock = SmartCore::Engine::Lock.new
61 | lock.synchronize { your_code }
62 | ```
63 |
64 | ---
65 |
66 | ### Read/Write Lock
67 |
68 | - non-controlable reader count;
69 | - readers does not lock each other;
70 | - readers waits for writer;
71 | - writer waits for readers;
72 |
73 | ```ruby
74 | lock = SmartCore::Engine::ReadWriteLock.new
75 |
76 | lock.read_sync { ...some-read-op... } # waits for writer
77 | lock.read_sync { ...some-read-op... } # waits for writer
78 | lock.write_sync { ... some-write-op... } # waits for all readers and current writer
79 |
80 | # is write_sync lock is owned by current thread?
81 | lock.write_owned? # true or false
82 | ```
83 |
84 | ---
85 |
86 | ### Cache Storage
87 |
88 | - you can use any object as a cache key;
89 | - you can store any object as a cache value;
90 | - you can cache `nil` object too;
91 |
92 | - cache `read` has `fetch` semantics:
93 | - signature: `#read(key, &fallback)`;
94 | - in the event of cache miss the `&fallback` black will be invoked;
95 | - the return value of the fallback block will be written to the cache, and that return value will be returned;
96 | - cache `write`:
97 | - signature: `#write(key, value)`;
98 | - you can use any object as a cache key;
99 | - you can store any object as a value;
100 | - you can write `nil` object too;
101 | - cache clear:
102 | - signature: `#clear`;
103 |
104 | ```ruby
105 | cache = SmartCore::Engine::Cache.new
106 |
107 | # write and read
108 | cache.write(:amount, 123.456) # => 123.456
109 | cache.read(:amount) # => 123.456
110 |
111 | # read non-existing with a fallback
112 | cache.read('name') # => nil
113 | cache.read('name') { 'D@iVeR' } # => 'D@iVeR'
114 | cache.read('name') # => 'D@iVeR'
115 |
116 | # store nil object
117 | cache.write(:nil_value, nil) # => nil
118 | cache.read(:nil_value) # => nil
119 | cache.read(:nil_value) { 'rewritten' } # => nil
120 | cache.read(:nil_value) # => nil
121 |
122 | # clear cache
123 | cache.clear # => nil
124 | ```
125 |
126 | ```ruby
127 | # aliases:
128 |
129 | # write:
130 | cache[:key1] = 'test'
131 |
132 | # read:
133 | cache[:key1] # => 'test'
134 |
135 | # read with fallback:
136 | cache[:key2] { 'test2' } # => 'test2'
137 | cache[:key2] # => 'test2'
138 | ```
139 |
140 | ---
141 |
142 | ### Atomic thread-safe value container
143 |
144 | ```ruby
145 | atom = SmartCore::Engine::Atom.new # initial value - nil
146 | atom.value # => nil
147 | # --- or ---
148 | atom = SmartCore::Engine::Atom.new(7) # initial value - 7
149 | atom.value # => 7
150 |
151 | # set new value (thread-safely)
152 | atom.swap { |original_value| original_value * 2 }
153 | atom.value # => 14
154 | ```
155 |
156 | ---
157 |
158 | ### Any Object Frozener
159 |
160 | - works with any type of ruby objects (event with `BasicObject`);
161 | - uses classic Ruby C-level `frozen?`/`freeze` functionality;
162 |
163 | ```ruby
164 | # as a singleton
165 |
166 | object = BasicObject.new
167 | SmartCore::Engine::Frozener.frozen?(object) # => false
168 |
169 | SmartCore::Engine::Frozener.freeze(object)
170 | SmartCore::Engine::Frozener.frozen?(object) # => true
171 | ```
172 |
173 | ```ruby
174 | # as a mixin
175 |
176 | class EmptyObject < BasicObject
177 | include SmartCore::Engine::Frozener::Mixin
178 | end
179 |
180 | object = EmptyObject.new
181 |
182 | object.frozen? # => false
183 | object.freeze
184 | object.frozen? # => true
185 | ```
186 |
187 | ---
188 |
189 | ### Basic Object Refinements
190 |
191 | Ruby's `BasicObject` class does not have some fundamental (extremely important for instrumenting) methods:
192 |
193 | - `is_a?` / `kind_of?`
194 | - `instance_of?`
195 | - `freeze` / `frozen?`
196 | - `hash`
197 | - `nil?`
198 | - `inspect`
199 |
200 | `SmartCore::Ext::BasicObjectAsObject` refinement solves this problem (by Ruby's internal API without any manualy-emulated behavior).
201 |
202 | ```ruby
203 | # without refinement:
204 | basic_obj = ::BasicObject.new
205 |
206 | basic_obj.is_a?(::BasicObject) # raises ::NoMethodError
207 | basic_obj.kind_of?(::BasicObject) # raises ::NoMethodError
208 | basic_obj.instance_of?(::BasicObject) # rasies ::NoMethodError
209 | basic_obj.freeze # raises ::NoMethodError
210 | basic_obj.frozen? # raises ::NoMethodError
211 | basic_object.hash # raises ::NoMethodError
212 | basic_object.nil? # raises ::NoMethodError
213 | basic_object.inspect # raises ::NoMethodError
214 | ```
215 |
216 | ```ruby
217 | # with refinement:
218 | using SmartCore::Ext::BasicObjectAsObject
219 |
220 | basic_obj = ::BasicObject.new
221 |
222 | basic_obj.is_a?(::BasicObject) # => true
223 | basic_obj.kind_of?(::BasicObject) # => true
224 | basic_obj.instance_of?(::BasicObject) # => true
225 | basic_obj.instance_of?(::Object) # => false
226 | basic_obj.is_a?(::Integer) # => false
227 | basic_obj.kind_of?(::Integer) # => false
228 |
229 | basic_obj.frozen? # => false
230 | basic_obj.freeze # => self
231 | basic_obj.frozen? # => true
232 |
233 | basic_obj.nil? # => false
234 |
235 | basic_obj.hash # => 2682859680348634421 (some Integer value)
236 |
237 | basic_obj.inspect # => "#"
238 | ```
239 |
240 | ---
241 |
242 | ### Inline rescue pipe
243 |
244 | - works with an array of proc objects;
245 | - returns the result of the first non-failed proc;
246 | - provides an error interception interface (a block argument);
247 | - fails with the last failed proc exception (if all procs were failed and interceptor was not passed);
248 |
249 | #### Return the result of the first non-failed proc
250 |
251 | ```ruby
252 | SmartCore::Engine::RescueExt.inline_rescue_pipe(
253 | -> { raise },
254 | -> { raise },
255 | -> { 123 },
256 | -> { 567 },
257 | -> { raise },
258 | )
259 | # => output: 123
260 | ```
261 |
262 | #### Fail with the last failed proc exception
263 |
264 | ```ruby
265 | SmartCore::Engine::RescueExt.inline_rescue_pipe(
266 | -> { raise(::ArgumentError) },
267 | -> { raise(::TypeError) },
268 | -> { raise(::ZeroDivisionError) }
269 | )
270 | # => fails with ZeroDivisionError
271 | ```
272 |
273 | #### Error interception
274 |
275 | ```ruby
276 | SmartCore::Engine::RescueExt.inline_rescue_pipe(
277 | -> { raise(::ArgumentError) },
278 | -> { raise(::TypeError) },
279 | -> { raise(::ZeroDivisionError, 'Intercepted exception') }
280 | ) do |error|
281 | error.message
282 | end
283 | # => output: "Intercepted exception"
284 | ```
285 |
286 | ---
287 |
288 | ## Roadmap
289 |
290 | - migrate to Github Actions in CI;
291 | - thread-safety for BasicObject extensions;
292 | - `SmartCore::Engine::Cache`:
293 | - thread-safety;
294 | - support for `ttl:` option for `#write` and for fallback block attribute of `#read`;
295 | - support for key-value-pair iteration;
296 | - support for `#keys` method;
297 | - support for `#key?` method;
298 | - think about some layer of cache object serialization;
299 | - `SmartCore::Engine::ReadWriteLock`:
300 | - an ability to set a maximum count of readers;
301 |
302 | ---
303 |
304 | ## Contributing
305 |
306 | - Fork it ( https://github.com/smart-rb/smart_engine )
307 | - Create your feature branch (`git checkout -b feature/my-new-feature`)
308 | - Commit your changes (`git commit -am '[feature_context] Add some feature'`)
309 | - Push to the branch (`git push origin feature/my-new-feature`)
310 | - Create new Pull Request
311 |
312 | ## License
313 |
314 | Released under MIT License.
315 |
316 | ## Supporting
317 |
318 |
319 |
320 |
321 |
322 | ## Authors
323 |
324 | [Rustam Ibragimov](https://github.com/0exp)
325 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'bundler/gem_tasks'
4 | require 'rspec/core/rake_task'
5 | require 'rubocop'
6 | require 'rubocop/rake_task'
7 | require 'rubocop-rails'
8 | require 'rubocop-performance'
9 | require 'rubocop-rspec'
10 | require 'rubocop-rake'
11 |
12 | RuboCop::RakeTask.new(:rubocop) do |t|
13 | config_path = File.expand_path(File.join('.rubocop.yml'), __dir__)
14 | t.options = ['--config', config_path]
15 | t.requires << 'rubocop-rspec'
16 | t.requires << 'rubocop-performance'
17 | t.requires << 'rubocop-rake'
18 | end
19 |
20 | RSpec::Core::RakeTask.new(:rspec)
21 |
22 | task default: :rspec
23 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'bundler/setup'
5 | require 'smart_core'
6 |
7 | require 'pry'
8 | Pry.start
9 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/lib/smart_core.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api public
4 | # @since 0.1.0
5 | module SmartCore
6 | require_relative 'smart_core/errors'
7 | require_relative 'smart_core/engine'
8 | require_relative 'smart_core/ext'
9 | end
10 |
--------------------------------------------------------------------------------
/lib/smart_core/engine.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api public
4 | # @since 0.1.0
5 | module SmartCore::Engine
6 | require_relative 'engine/version'
7 | require_relative 'engine/lock'
8 | require_relative 'engine/read_write_lock'
9 | require_relative 'engine/rescue_ext'
10 | require_relative 'engine/atom'
11 | require_relative 'engine/frozener'
12 | require_relative 'engine/cache'
13 | end
14 |
--------------------------------------------------------------------------------
/lib/smart_core/engine/atom.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api public
4 | # @since 0.7.0
5 | class SmartCore::Engine::Atom
6 | # @param initial_value [Any]
7 | # @return [void]
8 | #
9 | # @api private
10 | # @since 0.7.0
11 | def initialize(initial_value = nil)
12 | @value = initial_value
13 | @barrier = SmartCore::Engine::Lock.new
14 | end
15 |
16 | # @return [Any]
17 | #
18 | # @api public
19 | # @since 0.7.0
20 | def value
21 | with_barrier { @value }
22 | end
23 |
24 | # @param block [Block]
25 | # @return [Any]
26 | #
27 | # @api public
28 | # @since 0.7.0
29 | def swap(&block)
30 | with_barrier { @value = yield(@value) }
31 | end
32 |
33 | private
34 |
35 | # @param block [Block]
36 | # @return [Any]
37 | #
38 | # @api private
39 | # @since 0.1.0
40 | def with_barrier(&block)
41 | @barrier.synchronize(&block)
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/smart_core/engine/cache.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api public
4 | # @since 0.13.0
5 | class SmartCore::Engine::Cache
6 | # @return [void]
7 | #
8 | # @api public
9 | # @since 0.13.0
10 | def initialize
11 | @store = {}
12 | # TODO: thread-safety (use SmartCore::Engine::Lock)
13 | end
14 |
15 | # @param key [Any]
16 | # @apram value [Any]
17 | # @return [Any]
18 | #
19 | # @api public
20 | # @since 0.13.0
21 | def write(key, value)
22 | @store[key] = value
23 | end
24 | alias_method :[]=, :write
25 |
26 | # @param key [Any]
27 | # @param fallback [Block]
28 | # @return [Any, NilClass]
29 | #
30 | # @api public
31 | # @since 0.13.0
32 | # rubocop:disable Style/NestedTernaryOperator
33 | def read(key, &fallback)
34 | # @note
35 | # key?-flow is a compromise used to provide an ability to cache `nil` objects too.
36 | @store.key?(key) ? @store[key] : (block_given? ? write(key, yield) : nil)
37 | end
38 | alias_method :[], :read
39 | # rubocop:enable Style/NestedTernaryOperator
40 |
41 | # @return [NilClass]
42 | #
43 | # @api public
44 | # @since 0.13.0
45 | def clear
46 | @store.clear
47 | nil
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/smart_core/engine/ext.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.9.0
5 | module SmartCore::Engine::Ext
6 | require_relative 'ext/basic_object_as_object'
7 | end
8 |
--------------------------------------------------------------------------------
/lib/smart_core/engine/frozener.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api public
4 | # @since 0.8.0
5 | # @version 0.9.0
6 | module SmartCore::Engine::Frozener
7 | # @api public
8 | # @since 0.8.0
9 | module Mixin
10 | # @return [self]
11 | #
12 | # @api public
13 | # @since 0.8.0
14 | def freeze
15 | SmartCore::Engine::Frozener.freeze(self)
16 | end
17 |
18 | # @return [Boolean]
19 | #
20 | # @api public
21 | # @since 0.8.0
22 | def frozen?
23 | SmartCore::Engine::Frozener.frozen?(self)
24 | end
25 | end
26 |
27 | # @return [UnboundMethod]
28 | #
29 | # @api private
30 | # @since 0.8.0
31 | FROZENER = Object.new.method(:freeze).unbind.tap(&:freeze)
32 |
33 | # @return [UnboundMethod]
34 | #
35 | # @api private
36 | # @since 0.8.0
37 | FROZEN_CHECK = Object.new.method(:frozen?).unbind.tap(&:freeze)
38 |
39 | class << self
40 | # @param object [Any]
41 | # @return [object]
42 | #
43 | # @api public
44 | # @since 0.8.0
45 | # @version 0.9.0
46 | def freeze(object)
47 | # rubocop:disable Performance/BindCall
48 | # NOTE: disabled in order to support older Ruby versions than Ruby@3
49 | FROZENER.bind(object).call
50 | # rubocop:enable Performance/BindCall
51 | end
52 |
53 | # @param object [Any]
54 | # @return [Boolean]
55 | #
56 | # @api public
57 | # @since 0.8.0
58 | # @version 0.9.0
59 | def frozen?(object)
60 | # rubocop:disable Performance/BindCall
61 | # NOTE: disabled in order to support older Ruby versions than Ruby@3
62 | FROZEN_CHECK.bind(object).call
63 | # rubocop:enable Performance/BindCall
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/smart_core/engine/lock.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api public
4 | # @since 0.4.0
5 | class SmartCore::Engine::Lock
6 | # @return [void]
7 | #
8 | # @api public
9 | # @since 0.4.0
10 | def initialize
11 | @lock = ::Mutex.new
12 | end
13 |
14 | # @param block [Block]
15 | # @return [Any]
16 | #
17 | # @api public
18 | # @since 0.4.0
19 | def synchronize(&block)
20 | @lock.owned? ? yield : @lock.synchronize(&block)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/smart_core/engine/read_write_lock.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api public
4 | # @since 0.14.0
5 | # @version 0.17.0
6 | class SmartCore::Engine::ReadWriteLock
7 | # @return [void]
8 | #
9 | # @api public
10 | # @sicne 0.14.0
11 | def initialize
12 | # NOTE:
13 | # ivars has no readers cuz we want to avoid
14 | # Ruby VM's context-switching during reade-method invokation.
15 | @active_reader = false
16 | @write_lock = ::Mutex.new
17 | end
18 |
19 | # @param block [Block]
20 | # @return [Any]
21 | #
22 | # @api public
23 | # @since 0.14.0
24 | # @version 0.17.0
25 | def read_sync(&block)
26 | @active_reader = true
27 | if @write_lock.locked? && @write_lock.owned?
28 | yield
29 | else
30 | while @write_lock.locked? do; end
31 | yield
32 | end
33 | ensure
34 | @active_reader = false
35 | end
36 |
37 | # @return [Boolean]
38 | #
39 | # @api public
40 | # @since 0.15.0
41 | def write_owned?
42 | @write_lock.owned?
43 | end
44 |
45 | # @param block [Block]
46 | # @return [Any]
47 | #
48 | # @api public
49 | # @since 0.14.0
50 | # @version 0.16.0
51 | def write_sync(&block)
52 | if @write_lock.owned?
53 | yield
54 | else
55 | while @active_reader do; end
56 | @write_lock.synchronize do
57 | @active_reader = true
58 | begin
59 | yield
60 | ensure
61 | @active_reader = false
62 | end
63 | end
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/smart_core/engine/rescue_ext.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api public
4 | # @since 0.6.0
5 | module SmartCore::Engine::RescueExt
6 | # REASON:
7 | # {module_funciton} is used to be able to include/extend any module/class
8 | # and at the same time to use without any inclusion/extending logic as a service
9 |
10 | module_function
11 |
12 | # @param proks [Array]
13 | # @param error_interceptor [Block]
14 | # @return [Any]
15 | #
16 | # @api public
17 | # @since 0.6.0
18 | # rubocop:disable Performance/RedundantBlockCall
19 | def inline_rescue_pipe(*proks, &error_interceptor)
20 | unless proks.all? { |prok| prok.is_a?(::Proc) }
21 | raise(SmartCore::ArgumentError, 'Invalid proc object')
22 | end
23 |
24 | interceptable_bloks = proks.to_enum
25 | pipe_invokation_result = nil
26 | last_exception = nil
27 |
28 | begin
29 | while current_block = interceptable_bloks.next
30 | begin
31 | pipe_invokation_result = current_block.call
32 | break
33 | rescue => error
34 | last_exception = error
35 | end
36 | end
37 |
38 | pipe_invokation_result
39 | rescue ::StopIteration
40 | error_interceptor ? error_interceptor.call(last_exception) : raise(last_exception)
41 | end
42 | end
43 | # rubocop:enable Performance/RedundantBlockCall
44 | end
45 |
--------------------------------------------------------------------------------
/lib/smart_core/engine/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmartCore
4 | module Engine
5 | # @return [String]
6 | #
7 | # @api public
8 | # @since 0.1.0
9 | # @version 0.17.0
10 | VERSION = '0.17.0'
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/smart_core/errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmartCore
4 | # @api public
5 | # @since 0.1.0
6 | Error = Class.new(::StandardError)
7 |
8 | # @api public
9 | # @since 0.1.0
10 | ArgumentError = Class.new(::ArgumentError)
11 |
12 | # @api public
13 | # @since 0.3.0
14 | NameError = Class.new(::NameError)
15 |
16 | # @api public
17 | # @since 0.5.0
18 | TypeError = Class.new(::TypeError)
19 |
20 | # @api public
21 | # @since 0.2.0
22 | FrozenError = # rubocop:disable Naming/ConstantName
23 | # :nocov:
24 | # rubocop:disable Layout/CommentIndentation
25 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5.0')
26 | Class.new(::FrozenError)
27 | else
28 | Class.new(::RuntimeError)
29 | end
30 | # :nocov:
31 | # rubocop:enable Layout/CommentIndentation
32 | end
33 |
--------------------------------------------------------------------------------
/lib/smart_core/ext.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api private
4 | # @since 0.9.0
5 | module SmartCore::Ext
6 | require_relative 'ext/basic_object_as_object'
7 | end
8 |
--------------------------------------------------------------------------------
/lib/smart_core/ext/basic_object_as_object.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @api public
4 | # @since 0.9.0
5 | # @version 0.10.0
6 | module SmartCore::Ext::BasicObjectAsObject
7 | refine BasicObject do
8 | _m_obj = ::Object.new
9 |
10 | _is_a = _m_obj.method(:is_a?).unbind.tap(&:freeze)
11 | _freeze = _m_obj.method(:freeze).unbind.tap(&:freeze)
12 | _frozen = _m_obj.method(:frozen?).unbind.tap(&:freeze)
13 | _hash = _m_obj.method(:hash).unbind.tap(&:freeze)
14 | _nil = _m_obj.method(:nil?).unbind.tap(&:freeze)
15 | _instance_of = _m_obj.method(:instance_of?).unbind.tap(&:freeze)
16 | _inspect = _m_obj.method(:inspect).unbind.tap(&:freeze)
17 |
18 | # @note Object#is_a? behavior copy
19 | # @param klass [Class]
20 | # @return [Boolean]
21 | #
22 | # @api public
23 | # @since 0.9.0
24 | define_method(:is_a?) do |klass|
25 | # rubocop:disable Performance/BindCall
26 | # NOTE: disabled in order to support older Ruby versions than Ruby@3
27 | _is_a.bind(self).call(klass)
28 | # rubocop:enable Performance/BindCall
29 | end
30 | alias_method :kind_of?, :is_a?
31 |
32 | # @note Object#freeze behavior copy
33 | # @return [self]
34 | #
35 | # @api public
36 | # @since 0.9.0
37 | define_method(:freeze) do
38 | # rubocop:disable Performance/BindCall
39 | # NOTE: disabled in order to support older Ruby versions than Ruby@3
40 | _freeze.bind(self).call
41 | # rubocop:enable Performance/BindCall
42 | end
43 |
44 | # @note Object#frozen? behavior copy
45 | # @return [Boolean]
46 | #
47 | # @api public
48 | # @since 0.9.0
49 | define_method(:frozen?) do
50 | # rubocop:disable Performance/BindCall
51 | # NOTE: disabled in order to support older Ruby versions than Ruby@3
52 | _frozen.bind(self).call
53 | # rubocop:enable Performance/BindCall
54 | end
55 |
56 | # @return [Integer]
57 | #
58 | # @api public
59 | # @since 0.10.0
60 | define_method(:hash) do
61 | # rubocop:disable Performance/BindCall
62 | # NOTE: disabled in order to support older Ruby versions than Ruby@3
63 | _hash.bind(self).call
64 | # rubocop:enable Performance/BindCall
65 | end
66 |
67 | # @return [Boolean]
68 | #
69 | # @api public
70 | # @since 0.10.0
71 | define_method(:nil?) do
72 | # rubocop:disable Performance/BindCall
73 | # NOTE: disabled in order to support older Ruby versions than Ruby@3
74 | _nil.bind(self).call
75 | # rubocop:enable Performance/BindCall
76 | end
77 |
78 | # @return [Boolean]
79 | #
80 | # @api public
81 | # @since 0.1.0
82 | define_method(:instance_of?) do |klass|
83 | # rubocop:disable Performance/BindCall
84 | # NOTE: disabled in order to support older Ruby versions than Ruby@3
85 | _instance_of.bind(self).call(klass)
86 | # rubocop:enable Performance/BindCall
87 | end
88 |
89 | # @return [String]
90 | #
91 | # @api public
92 | # @since 0.12.0
93 | define_method(:inspect) do
94 | # rubocop:disable Performance/BindCall
95 | # NOTE: disabled in order to support older Ruby versions than Ruby@3
96 | _inspect.bind(self).call
97 | # rubocop:enable Performance/BindCall
98 | end
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/smart_engine.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'lib/smart_core/engine/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.required_ruby_version = Gem::Requirement.new('>= 2.5')
7 |
8 | spec.name = 'smart_engine'
9 | spec.version = SmartCore::Engine::VERSION
10 | spec.authors = ['Rustam Ibragimov']
11 | spec.email = ['iamdaiver@gmail.com']
12 | spec.homepage = 'https://github.com/smart-rb/smart_engine'
13 | spec.license = 'MIT'
14 |
15 | spec.summary = <<~GEM_SUMMARY
16 | SmartCore Engine - a generic subset of SmartCore's functionality.
17 | GEM_SUMMARY
18 |
19 | spec.description = <<~GEM_DESCRIPTION
20 | SmartCore Engine - a set of core functionality shared beetwen a series of SmartCore gems.
21 | GEM_DESCRIPTION
22 |
23 | spec.metadata['homepage_uri'] = spec.homepage
24 | spec.metadata['source_code_uri'] =
25 | 'https://github.com/smart-rb/smart_engine'
26 | spec.metadata['changelog_uri'] =
27 | 'https://github.com/smart-rb/smart_engine/blob/master/CHANGELOG.md'
28 |
29 | spec.files = Dir.chdir(File.expand_path(__dir__)) do
30 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
31 | end
32 |
33 | spec.bindir = 'exe'
34 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35 | spec.require_paths = ['lib']
36 |
37 | spec.add_development_dependency 'bundler', '~> 2.3'
38 | spec.add_development_dependency 'rake', '~> 13.0'
39 | spec.add_development_dependency 'rspec', '~> 3.11'
40 | spec.add_development_dependency 'armitage-rubocop', '~> 1.30'
41 | spec.add_development_dependency 'simplecov', '~> 0.21'
42 | spec.add_development_dependency 'pry', '~> 0.14'
43 | end
44 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'simplecov'
4 |
5 | SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter
6 | SimpleCov.minimum_coverage(100)
7 | SimpleCov.enable_coverage(:branch)
8 | SimpleCov.enable_coverage(:line)
9 | SimpleCov.primary_coverage(:line)
10 | SimpleCov.add_filter('spec')
11 | SimpleCov.start
12 |
13 | require 'bundler/setup'
14 | require 'smart_core'
15 | require 'pry'
16 |
17 | RSpec.configure do |config|
18 | Kernel.srand config.seed
19 | config.disable_monkey_patching!
20 | config.filter_run_when_matching :focus
21 | config.order = :random
22 | config.shared_context_metadata_behavior = :apply_to_host_groups
23 | config.expect_with(:rspec) { |c| c.syntax = :expect }
24 | Thread.abort_on_exception = true
25 | end
26 |
--------------------------------------------------------------------------------
/spec/units/atom_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmartCore::Engine::Atom do
4 | context 'without initial value' do
5 | let(:atom) { SmartCore::Engine::Atom.new }
6 |
7 | specify 'value swap' do
8 | expect(atom.value).to eq(nil)
9 |
10 | result = atom.swap { 22 }
11 | expect(atom.value).to eq(22)
12 | expect(result).to eq(22)
13 |
14 | result = atom.swap { |value| value * 10 }
15 | expect(atom.value).to eq(220)
16 | expect(result).to eq(220)
17 | end
18 | end
19 |
20 | context 'with initial value' do
21 | let(:atom) { SmartCore::Engine::Atom.new('overwatch') }
22 |
23 | specify 'value swap' do
24 | expect(atom.value).to eq('overwatch')
25 |
26 | result = atom.swap { |value| value * 2 }
27 | expect(atom.value).to eq('overwatchoverwatch')
28 | expect(result).to eq('overwatchoverwatch')
29 |
30 | result = atom.swap(&:reverse)
31 | expect(atom.value).to eq('hctawrevohctawrevo')
32 | expect(result).to eq('hctawrevohctawrevo')
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/units/cache_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmartCore::Engine::Cache do
4 | let(:cache) { SmartCore::Engine::Cache.new }
5 |
6 | describe 'cache #read' do
7 | specify 'read existing key' do
8 | test1_value = 123
9 | test2_value = '123'
10 |
11 | cache.write(:test1, test1_value)
12 | cache.write(:test2, test2_value)
13 |
14 | expect(cache.read(:test1)).to eq(test1_value)
15 | expect(cache.read(:test2)).to eq(test2_value)
16 | end
17 |
18 | specify 'read non-existing key' do
19 | expect(cache.read(:test3)).to eq(nil)
20 | expect(cache.read(:test4)).to eq(nil)
21 | end
22 |
23 | specify 'read an existing key with fallback' do
24 | value1 = Object.new
25 | value2 = 'another-object'
26 |
27 | cache.write(:test1, value1)
28 |
29 | expect(cache.read(:test1) { value2 }).to eq(value1)
30 | expect(cache.read(:test1)).to eq(value1)
31 | end
32 |
33 | specify 'read a non-existing key with fallback' do
34 | fallback_value = Object.new
35 |
36 | expect(cache.read(:test1) { fallback_value }).to eq(fallback_value)
37 | expect(cache.read(:test1)).to eq(fallback_value)
38 | end
39 |
40 | specify '[] method alias' do
41 | test1_value = 'test1'
42 | test2_value = Object
43 |
44 | cache.write(:test1, test1_value)
45 | expect(cache[:test1]).to eq(test1_value)
46 |
47 | expect(cache[:test2] { test2_value }).to eq(test2_value)
48 | expect(cache[:test2]).to eq(test2_value)
49 | end
50 | end
51 |
52 | describe 'cache #write' do
53 | specify '#write returns a written value' do
54 | test1_value = Object.new
55 | expect(cache.write(:test1, test1_value)).to eq(test1_value)
56 | end
57 |
58 | specify '#write rewrites existing values' do
59 | first_value = Object.new
60 | second_value = Object.new
61 |
62 | cache.write(:test1, first_value)
63 | expect(cache.read(:test1)).to eq(first_value)
64 |
65 | cache.write(:test1, second_value) # NOTE: rewrite existing value
66 | expect(cache.read(:test1)).to eq(second_value)
67 | end
68 |
69 | specify 'rewrite returns written value' do
70 | test1_value = Object.new
71 | test2_value = Object.new
72 |
73 | cache.write(:test1, test1_value)
74 | expect(cache.write(:test1, test2_value)).to eq(test2_value)
75 | end
76 |
77 | specify 'we can #write a nil object (and fallback is not invoked if passed)' do
78 | cache.write(:test1, nil)
79 | expect(cache.read(:test1)).to eq(nil)
80 | expect(cache.read(:test1) { 'nil-fallback' }).to eq(nil)
81 | end
82 |
83 | specify '[]= method alias' do
84 | test1_value = Object.new
85 |
86 | expect(cache[:test1] = test1_value).to eq(test1_value)
87 | end
88 |
89 | specify 'we can use any object as a cache key and any object as a cachable object' do
90 | object_key, some_object = Object.new, Object.new
91 | string_key, some_string = 'test1', 'some-string'
92 | symbol_key, some_symbol = :test, :some_symbol
93 | number_key, some_number = 12_345, 7_776_655
94 | float_key, some_float = 123.456, 555.666
95 | hash_key, some_hash = { a: 1 }, { b: 100, c: 200 }
96 | time_key, some_time = Time.now, Time.now
97 | date_key, some_date = Date.new, Date.new
98 | nil_key, some_nil = nil, nil
99 |
100 | expect(cache.write(object_key, some_object)).to eq(some_object)
101 | expect(cache.read(object_key)).to eq(some_object)
102 |
103 | expect(cache.write(string_key, some_string)).to eq(some_string)
104 | expect(cache.read(string_key)).to eq(some_string)
105 |
106 | expect(cache.write(symbol_key, some_symbol)).to eq(some_symbol)
107 | expect(cache.read(symbol_key)).to eq(some_symbol)
108 |
109 | expect(cache.write(number_key, some_number)).to eq(some_number)
110 | expect(cache.read(number_key)).to eq(some_number)
111 |
112 | expect(cache.write(float_key, some_float)).to eq(some_float)
113 | expect(cache.read(float_key)).to eq(some_float)
114 |
115 | expect(cache.write(hash_key, some_hash)).to eq(some_hash)
116 | expect(cache.read(hash_key)).to eq(some_hash)
117 |
118 | expect(cache.write(time_key, some_time)).to eq(some_time)
119 | expect(cache.read(time_key)).to eq(some_time)
120 |
121 | expect(cache.write(date_key, some_date)).to eq(some_date)
122 | expect(cache.read(date_key)).to eq(some_date)
123 |
124 | expect(cache.write(nil_key, some_nil)).to eq(some_nil)
125 | expect(cache.read(nil_key)).to eq(some_nil)
126 | end
127 | end
128 |
129 | describe 'cache #clear' do
130 | specify 'cache clearing' do
131 | cache.write(:test1, 'test1')
132 | cache.write(:test2, 'test2')
133 |
134 | cache.clear
135 |
136 | expect(cache.read(:test1) { 'new-test1' }).to eq('new-test1') # cache miss => new value
137 | expect(cache.read(:test2) { 'new-test2' }).to eq('new-test2') # cache miss => new value
138 | end
139 |
140 | specify 'cache #clear result should be :))' do
141 | cache[:test1] = 'test1'
142 | expect(cache.clear).to eq(nil)
143 | end
144 | end
145 | end
146 |
--------------------------------------------------------------------------------
/spec/units/extensions/basic_object_as_object_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | using SmartCore::Ext::BasicObjectAsObject # NOTE: testing this functionality
4 |
5 | RSpec.describe SmartCore::Ext::BasicObjectAsObject do
6 | it 'provides support for #frozen/#freeze?/#is_a?/#kind_of?' do
7 | # we will use two different objects for the test clarity
8 | basic_obj_a = ::BasicObject.new
9 | basic_obj_b = ::BasicObject.new
10 |
11 | aggregate_failures 'frozen state' do
12 | expect(basic_obj_a.frozen?).to eq(false)
13 | expect(basic_obj_b.frozen?).to eq(false)
14 |
15 | expect(basic_obj_a.freeze).to eq(basic_obj_a)
16 | expect(basic_obj_a.frozen?).to eq(true)
17 | expect(basic_obj_b.frozen?).to eq(false)
18 |
19 | expect(basic_obj_b.freeze).to eq(basic_obj_b)
20 | expect(basic_obj_b.frozen?).to eq(true)
21 | expect(basic_obj_a.frozen?).to eq(true)
22 | end
23 |
24 | aggregate_failures 'support for type checking' do
25 | expect(basic_obj_a.is_a?(::Object)).to eq(false)
26 | expect(basic_obj_b.is_a?(::Object)).to eq(false)
27 |
28 | expect(basic_obj_a.is_a?(::Integer)).to eq(false)
29 | expect(basic_obj_b.is_a?(::String)).to eq(false)
30 |
31 | expect(basic_obj_a.is_a?(::BasicObject)).to eq(true)
32 | expect(basic_obj_b.is_a?(::BasicObject)).to eq(true)
33 | end
34 |
35 | aggregate_failures 'support for #hash' do
36 | expect(basic_obj_a.hash).to be_a(::Integer)
37 | expect(basic_obj_b.hash).to be_a(::Integer)
38 |
39 | expect(basic_obj_a.hash).to eq(basic_obj_a.hash)
40 | expect(basic_obj_b.hash).to eq(basic_obj_b.hash)
41 |
42 | expect(basic_obj_a.hash).not_to eq(basic_obj_b.hash)
43 | end
44 |
45 | # rubocop:disable Style/NilComparison
46 | aggregate_failures 'support for #nil?' do
47 | expect(basic_obj_a.nil?).to eq(false)
48 | expect(basic_obj_b.nil?).to eq(false)
49 | end
50 | # rubocop:enable Style/NilComparison
51 |
52 | aggregate_failures 'support for #instance_of?' do
53 | expect(basic_obj_a.instance_of?(::Object)).to eq(false)
54 | expect(basic_obj_a.instance_of?(::BasicObject)).to eq(true)
55 | expect(basic_obj_b.instance_of?(::Object)).to eq(false)
56 | expect(basic_obj_b.instance_of?(::BasicObject)).to eq(true)
57 | end
58 |
59 | aggregate_failures 'support for #inspect' do
60 | expect(basic_obj_a.inspect).to match(/\A#\z/)
61 | expect(basic_obj_b.inspect).to match(/\A#\z/)
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/spec/units/frozener_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmartCore::Engine::Frozener do
4 | specify 'singleton: freeze / frozen?' do
5 | object = BasicObject.new
6 |
7 | expect(SmartCore::Engine::Frozener.frozen?(object)).to eq(false)
8 | expect(SmartCore::Engine::Frozener.freeze(object)).to eq(object)
9 | expect(SmartCore::Engine::Frozener.frozen?(object)).to eq(true)
10 | end
11 |
12 | specify 'mixin: freeze / frozen?' do
13 | object = Class.new(BasicObject) { include SmartCore::Engine::Frozener::Mixin }.new
14 |
15 | expect(object.frozen?).to eq(false)
16 | expect(object.freeze).to eq(object)
17 | expect(object.frozen?).to eq(true)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/units/inline_rescue_pipe_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmartCore::Engine::RescueExt do
4 | describe 'pipe-lined inline rescue wrapper' do
5 | specify 'returns the first non-failed proc result' do
6 | result = SmartCore::Engine::RescueExt.inline_rescue_pipe(
7 | -> { raise },
8 | -> { raise },
9 | -> { :pipe3 },
10 | -> { :pipe4 },
11 | -> { raise }
12 | )
13 |
14 | expect(result).to eq(:pipe3)
15 | end
16 |
17 | specify 'provides custom error wrapper' do
18 | stub_const('SmartCoreCustomError', Class.new(StandardError))
19 |
20 | result = SmartCore::Engine::RescueExt.inline_rescue_pipe(
21 | -> { raise },
22 | -> { raise },
23 | -> { raise },
24 | -> { raise(SmartCoreCustomError, 'test message') }
25 | ) { |error| error }
26 |
27 | expect(result).to be_a(SmartCoreCustomError)
28 | expect(result.message).to eq('test message')
29 | end
30 |
31 | specify 'fails with the last exception when the error wrapper is not provided' do
32 | stub_const('NoCustomSmartErrorceptor', Class.new(StandardError))
33 |
34 | expect do
35 | SmartCore::Engine::RescueExt.inline_rescue_pipe(
36 | -> { raise },
37 | -> { raise },
38 | -> { raise },
39 | -> { raise(NoCustomSmartErrorceptor) }
40 | )
41 | end.to raise_error(NoCustomSmartErrorceptor)
42 | end
43 |
44 | specify 'fails when at least one of passed proc object is not a proc' do
45 | expect do
46 | SmartCore::Engine::RescueExt.inline_rescue_pipe(
47 | -> {},
48 | -> {},
49 | 123,
50 | -> {}
51 | ) { |error| error }
52 | end.to raise_error(SmartCore::ArgumentError)
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/spec/units/lock_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmartCore::Engine::Lock do
4 | specify 'multi-threading access' do
5 | counter = Class.new do
6 | def initialize
7 | @lock = SmartCore::Engine::Lock.new
8 | @counter = 0
9 | end
10 |
11 | def state
12 | @counter
13 | end
14 |
15 | def up
16 | @lock.synchronize { @counter += 1 }
17 | end
18 | end.new
19 |
20 | Array.new(1000) { Thread.new { counter.up } }.each(&:join)
21 |
22 | expect(counter.state).to eq(1000)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/units/read_write_lock_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rubocop:disable Style/Semicolon
4 | RSpec.describe SmartCore::Engine::ReadWriteLock do
5 | specify 'write-lock locks all read-locks / read-locks does not lock each other' do
6 | lock = SmartCore::Engine::ReadWriteLock.new
7 | output = +''
8 |
9 | lock.read_sync { output << '1' }
10 | Thread.new { lock.write_sync { sleep(3); output << '2' } }
11 | sleep(1)
12 |
13 | [
14 | Thread.new { lock.read_sync { output << '3' } },
15 | Thread.new { lock.read_sync { output << '4' } },
16 | Thread.new { lock.read_sync { output << '5' } }
17 | ].each(&:join)
18 |
19 | Thread.new { lock.write_sync { sleep(3); output << '6' } }
20 | sleep(1)
21 |
22 | [
23 | Thread.new { lock.read_sync { output << '7' } },
24 | Thread.new { lock.read_sync { output << '8' } },
25 | Thread.new { lock.read_sync { output << '9' } }
26 | ].each(&:join)
27 |
28 | expect(output[0..1]).to eq('12')
29 | expect(output[5]).to eq('6')
30 |
31 | expect(output.chars).to contain_exactly(*%w[1 2 3 4 5 6 7 8 9])
32 | end
33 |
34 | # rubocop:disable Naming/VariableName
35 | specify 'checking that is write lock is owned by current thread or not' do
36 | lock = SmartCore::Engine::ReadWriteLock.new
37 | __GBL_SMRTNGN_RSPC_THRD_CHK__ = false
38 | Thread.new { lock.write_sync { __GBL_SMRTNGN_RSPC_THRD_CHK__ = lock.write_owned?; sleep(3) } }
39 | sleep(1) # wait for value change
40 | expect(lock.write_owned?).to eq(false) # current thread - no
41 | expect(__GBL_SMRTNGN_RSPC_THRD_CHK__).to eq(true) # other thread - yes
42 | end
43 | # rubocop:enable Naming/VariableName
44 |
45 | specify 'has no dead-locks when the current thrad has already acquired the lock' do
46 | lock = SmartCore::Engine::ReadWriteLock.new
47 | output = +''
48 |
49 | expect do
50 | lock.write_sync { lock.write_sync { lock.write_sync { output << '1' } } }
51 | end.not_to raise_error
52 |
53 | expect(output).to eq('1')
54 | end
55 | end
56 | # rubocop:enable Style/Semicolon
57 |
--------------------------------------------------------------------------------
/spec/units/version_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'SmartCore Engine version' do
4 | specify { expect(SmartCore::Engine::VERSION).not_to eq(nil) }
5 | end
6 |
--------------------------------------------------------------------------------