├── .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 · Supported by Cado Labs · [![Gem Version](https://badge.fury.io/rb/smart_engine.svg)](https://badge.fury.io/rb/smart_engine) 2 | 3 | Generic SmartCore functionality. 4 | 5 | --- 6 | 7 |

8 | 9 | Supported by Cado Labs 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 | Supported by Cado Labs 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 | --------------------------------------------------------------------------------