├── .pdd
├── .gitignore
├── renovate.json
├── .0pdd.yml
├── .github
└── workflows
│ ├── xcop.yml
│ ├── reuse.yml
│ ├── typos.yml
│ ├── pdd.yml
│ ├── yamllint.yml
│ ├── copyrights.yml
│ ├── markdown-lint.yml
│ ├── actionlint.yml
│ ├── codecov.yml
│ └── rake.yml
├── Gemfile
├── .rubocop.yml
├── .rultor.yml
├── REUSE.toml
├── Rakefile
├── LICENSE.txt
├── LICENSES
└── MIT.txt
├── syncem.gemspec
├── lib
└── syncem.rb
├── Gemfile.lock
├── README.md
├── test
└── test_syncem.rb
└── logo.svg
/.pdd:
--------------------------------------------------------------------------------
1 | --source=.
2 | --verbose
3 | --rule min-words:20
4 | --rule min-estimate:15
5 | --rule max-estimate:90
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle/
2 | .DS_Store
3 | .idea/
4 | .yardoc/
5 | *.gem
6 | coverage/
7 | doc/
8 | node_modules/
9 | rdoc/
10 | vendor/
11 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.0pdd.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | errors:
5 | - yegor256@gmail.com
6 | # alerts:
7 | # github:
8 | # - yegor256
9 |
10 | tags:
11 | - pdd
12 | - bug
13 |
--------------------------------------------------------------------------------
/.github/workflows/xcop.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | # yamllint disable rule:line-length
5 | name: xcop
6 | 'on':
7 | push:
8 | pull_request:
9 | jobs:
10 | xcop:
11 | timeout-minutes: 15
12 | runs-on: ubuntu-24.04
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: g4s8/xcop-action@master
16 |
--------------------------------------------------------------------------------
/.github/workflows/reuse.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | # yamllint disable rule:line-length
5 | name: reuse
6 | 'on':
7 | push:
8 | branches:
9 | - master
10 | pull_request:
11 | branches:
12 | - master
13 | jobs:
14 | reuse:
15 | timeout-minutes: 15
16 | runs-on: ubuntu-24.04
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: fsfe/reuse-action@v5
20 |
--------------------------------------------------------------------------------
/.github/workflows/typos.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | # yamllint disable rule:line-length
5 | name: typos
6 | 'on':
7 | push:
8 | branches:
9 | - master
10 | pull_request:
11 | branches:
12 | - master
13 | jobs:
14 | typos:
15 | timeout-minutes: 15
16 | runs-on: ubuntu-24.04
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: crate-ci/typos@v1.32.0
20 |
--------------------------------------------------------------------------------
/.github/workflows/pdd.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | # yamllint disable rule:line-length
5 | name: pdd
6 | 'on':
7 | push:
8 | branches:
9 | - master
10 | pull_request:
11 | branches:
12 | - master
13 | jobs:
14 | pdd:
15 | timeout-minutes: 15
16 | runs-on: ubuntu-24.04
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: volodya-lombrozo/pdd-action@master
20 |
--------------------------------------------------------------------------------
/.github/workflows/yamllint.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | # yamllint disable rule:line-length
5 | name: yamllint
6 | 'on':
7 | push:
8 | branches:
9 | - master
10 | pull_request:
11 | branches:
12 | - master
13 | jobs:
14 | yamllint:
15 | timeout-minutes: 15
16 | runs-on: ubuntu-24.04
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: ibiqlik/action-yamllint@v3
20 |
--------------------------------------------------------------------------------
/.github/workflows/copyrights.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | # yamllint disable rule:line-length
5 | name: copyrights
6 | 'on':
7 | push:
8 | branches:
9 | - master
10 | pull_request:
11 | branches:
12 | - master
13 | jobs:
14 | copyrights:
15 | timeout-minutes: 15
16 | runs-on: ubuntu-24.04
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: yegor256/copyrights-action@0.0.8
20 |
--------------------------------------------------------------------------------
/.github/workflows/markdown-lint.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | # yamllint disable rule:line-length
5 | name: markdown-lint
6 | 'on':
7 | push:
8 | branches:
9 | - master
10 | pull_request:
11 | branches:
12 | - master
13 | jobs:
14 | markdown-lint:
15 | timeout-minutes: 15
16 | runs-on: ubuntu-24.04
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: DavidAnson/markdownlint-cli2-action@v20.0.0
20 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko
4 | # SPDX-License-Identifier: MIT
5 |
6 | source 'https://rubygems.org'
7 | gemspec
8 |
9 | gem 'minitest', '~>5.25', require: false
10 | gem 'rake', '~>13.2', require: false
11 | gem 'rdoc', '~>6.11', require: false
12 | gem 'rubocop', '~>1.71', require: false
13 | gem 'rubocop-minitest', '~>0.38', require: false
14 | gem 'rubocop-performance', '~>1.25', require: false
15 | gem 'rubocop-rake', '~>0.7', require: false
16 | gem 'threads', '~>0.4', require: false
17 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | AllCops:
5 | DisplayCopNames: true
6 | TargetRubyVersion: 2.3
7 | SuggestExtensions: false
8 | NewCops: enable
9 | plugins:
10 | - rubocop-rake
11 | - rubocop-minitest
12 | - rubocop-performance
13 | Minitest/EmptyLineBeforeAssertionMethods:
14 | Enabled: false
15 | Layout/EndOfLine:
16 | EnforcedStyle: lf
17 | Metrics/MethodLength:
18 | Max: 50
19 | Layout/ParameterAlignment:
20 | Enabled: false
21 | Style/OptionalBooleanParameter:
22 | Enabled: false
23 | Metrics/CyclomaticComplexity:
24 | Max: 10
25 | Metrics/PerceivedComplexity:
26 | Max: 10
27 | Metrics/AbcSize:
28 | Max: 25
29 | require: []
30 |
--------------------------------------------------------------------------------
/.rultor.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | # yamllint disable rule:line-length
5 | docker:
6 | image: yegor256/ruby
7 | assets:
8 | rubygems.yml: yegor256/home#assets/rubygems.yml
9 | install: |
10 | sudo gem install pdd
11 | pdd -f /dev/null
12 | sudo bundle install --no-color "--gemfile=$(pwd)/Gemfile"
13 | release:
14 | pre: false
15 | script: |-
16 | bundle exec rake
17 | rm -rf *.gem
18 | sed -i "s/0\.0\.0/${tag}/g" syncem.gemspec
19 | git add syncem.gemspec
20 | git commit -m "Version set to ${tag}"
21 | gem build syncem.gemspec
22 | chmod 0600 ../rubygems.yml
23 | gem push *.gem --config-file ../rubygems.yml
24 | merge:
25 | script: |-
26 | bundle exec rake
27 |
--------------------------------------------------------------------------------
/.github/workflows/actionlint.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | # yamllint disable rule:line-length
5 | name: actionlint
6 | 'on':
7 | push:
8 | branches:
9 | - master
10 | pull_request:
11 | branches:
12 | - master
13 | jobs:
14 | actionlint:
15 | timeout-minutes: 15
16 | runs-on: ubuntu-24.04
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Download actionlint
20 | id: get_actionlint
21 | run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
22 | shell: bash
23 | - name: Check workflow files
24 | run: ${{ steps.get_actionlint.outputs.executable }} -color
25 | shell: bash
26 |
--------------------------------------------------------------------------------
/.github/workflows/codecov.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | # yamllint disable rule:line-length
5 | name: codecov
6 | 'on':
7 | push:
8 | branches:
9 | - master
10 | jobs:
11 | codecov:
12 | timeout-minutes: 15
13 | runs-on: ubuntu-24.04
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: ruby/setup-ruby@v1
17 | with:
18 | ruby-version: 3.3
19 | bundler-cache: true
20 | - run: bundle config set --global path "$(pwd)/vendor/bundle"
21 | - run: bundle install --no-color
22 | - run: bundle exec rake
23 | - uses: codecov/codecov-action@v5
24 | with:
25 | files: coverage/.resultset.json
26 | fail_ci_if_error: true
27 |
--------------------------------------------------------------------------------
/.github/workflows/rake.yml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 | ---
4 | # yamllint disable rule:line-length
5 | name: rake
6 | 'on':
7 | push:
8 | branches:
9 | - master
10 | pull_request:
11 | branches:
12 | - master
13 | jobs:
14 | test:
15 | strategy:
16 | matrix:
17 | os: [ubuntu-24.04, macos-15]
18 | ruby: [3.3]
19 | runs-on: ${{ matrix.os }}
20 | steps:
21 | - uses: actions/checkout@v4
22 | - uses: ruby/setup-ruby@v1
23 | with:
24 | ruby-version: ${{ matrix.ruby }}
25 | bundler-cache: true
26 | - run: bundle config set --global path "$(pwd)/vendor/bundle"
27 | - run: bundle install --no-color
28 | - run: bundle exec rake
29 |
--------------------------------------------------------------------------------
/REUSE.toml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
2 | # SPDX-License-Identifier: MIT
3 |
4 | version = 1
5 | [[annotations]]
6 | path = [
7 | ".DS_Store",
8 | ".gitattributes",
9 | ".gitignore",
10 | ".pdd",
11 | "**.json",
12 | "**.md",
13 | "**.svg",
14 | "**.txt",
15 | "**/.DS_Store",
16 | "**/.gitignore",
17 | "**/.pdd",
18 | "**/*.csv",
19 | "**/*.jpg",
20 | "**/*.json",
21 | "**/*.md",
22 | "**/*.pdf",
23 | "**/*.png",
24 | "**/*.svg",
25 | "**/*.txt",
26 | "**/*.vm",
27 | "**/CNAME",
28 | "**/Gemfile.lock",
29 | "Gemfile.lock",
30 | "README.md",
31 | "renovate.json",
32 | ]
33 | precedence = "override"
34 | SPDX-FileCopyrightText = "Copyright (c) 2025 Yegor Bugayenko"
35 | SPDX-License-Identifier = "MIT"
36 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko
4 | # SPDX-License-Identifier: MIT
5 |
6 | require 'rubygems'
7 | require 'rake'
8 | require 'rake/clean'
9 |
10 | CLEAN = FileList['coverage']
11 |
12 | def name
13 | @name ||= File.basename(Dir['*.gemspec'].first, '.*')
14 | end
15 |
16 | def version
17 | Gem::Specification.load(Dir['*.gemspec'].first).version
18 | end
19 |
20 | task default: %i[clean test rubocop]
21 |
22 | require 'rake/testtask'
23 | desc 'Run all unit tests'
24 | Rake::TestTask.new(:test) do |test|
25 | test.libs << 'lib' << 'test'
26 | test.pattern = 'test/**/test_*.rb'
27 | test.verbose = false
28 | end
29 |
30 | require 'rdoc/task'
31 | RDoc::Task.new do |rdoc|
32 | rdoc.main = 'README.md'
33 | rdoc.rdoc_dir = 'rdoc'
34 | rdoc.rdoc_files.include('README.md', 'lib/**/*.rb')
35 | end
36 |
37 | require 'rubocop/rake_task'
38 | desc 'Run RuboCop on all directories'
39 | RuboCop::RakeTask.new(:rubocop) do |task|
40 | task.fail_on_error = true
41 | end
42 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Yegor Bugayenko
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
13 | in 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 NON-INFRINGEMENT. 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/LICENSES/MIT.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Yegor Bugayenko
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
13 | in 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 NON-INFRINGEMENT. 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/syncem.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko
4 | # SPDX-License-Identifier: MIT
5 |
6 | require 'English'
7 | Gem::Specification.new do |s|
8 | s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
9 | s.required_ruby_version = '>=2.3'
10 | s.name = 'syncem'
11 | s.version = '0.0.0'
12 | s.license = 'MIT'
13 | s.summary = 'Thread-safe decorator of Ruby objects'
14 | s.description = 'Sometimes you have an object that is not thread-safe,
15 | but you need to make sure each of its methods is thread-safe, because they
16 | deal with some resources, like files or databases and you want them to
17 | manage those resources sequentially. This small gem will help you achieve
18 | exactly that without any re-design of the objects you already have. Just
19 | decorate them with SyncEm decorator and that is it.'
20 | s.authors = ['Yegor Bugayenko']
21 | s.email = 'yegor256@gmail.com'
22 | s.homepage = 'https://github.com/yegor256/syncem'
23 | s.files = `git ls-files | grep -v -E '^(test/|\\.|renovate)'`.split($RS)
24 | s.rdoc_options = ['--charset=UTF-8']
25 | s.extra_rdoc_files = ['README.md']
26 | s.metadata['rubygems_mfa_required'] = 'true'
27 | end
28 |
--------------------------------------------------------------------------------
/lib/syncem.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko
4 | # SPDX-License-Identifier: MIT
5 |
6 | # SyncEm is a simple decorator of an existing object that makes all its
7 | # methods thread-safe.
8 | #
9 | # For more information read
10 | # {README}[https://github.com/yegor256/syncem/blob/master/README.md] file.
11 | #
12 | # Author:: Yegor Bugayenko (yegor256@gmail.com)
13 | # Copyright:: Copyright (c) 2018-2025 Yegor Bugayenko
14 | # License:: MIT
15 | class SyncEm
16 | undef_method :send
17 |
18 | def initialize(origin)
19 | @origin = origin
20 | @mutex = Mutex.new
21 | end
22 |
23 | def method_missing(*args)
24 | @mutex.synchronize do
25 | mtd = args.shift
26 | if @origin.respond_to?(mtd)
27 | params = @origin.method(mtd).parameters
28 | reqs = params.count { |p| p[0] == :req }
29 | if params.any? { |p| p[0] == :key } && args.size > reqs
30 | @origin.__send__(mtd, *args[0...-1], **args.last) do |*a|
31 | yield(*a) if block_given?
32 | end
33 | else
34 | @origin.__send__(mtd, *args) do |*a|
35 | yield(*a) if block_given?
36 | end
37 | end
38 | else
39 | super
40 | end
41 | end
42 | end
43 |
44 | def respond_to?(method, include_private = false)
45 | @origin.respond_to?(method, include_private)
46 | end
47 |
48 | def respond_to_missing?(method, include_private = false)
49 | @origin.respond_to?(method, include_private) || super
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | syncem (0.0.0)
5 |
6 | GEM
7 | remote: https://rubygems.org/
8 | specs:
9 | ast (2.4.3)
10 | backtrace (0.4.0)
11 | concurrent-ruby (1.3.5)
12 | date (3.4.1)
13 | json (2.12.0)
14 | language_server-protocol (3.17.0.5)
15 | lint_roller (1.1.0)
16 | minitest (5.25.5)
17 | parallel (1.27.0)
18 | parser (3.3.8.0)
19 | ast (~> 2.4.1)
20 | racc
21 | prism (1.4.0)
22 | psych (5.2.6)
23 | date
24 | stringio
25 | racc (1.8.1)
26 | rainbow (3.1.1)
27 | rake (13.2.1)
28 | rdoc (6.13.1)
29 | psych (>= 4.0.0)
30 | regexp_parser (2.10.0)
31 | rubocop (1.75.5)
32 | json (~> 2.3)
33 | language_server-protocol (~> 3.17.0.2)
34 | lint_roller (~> 1.1.0)
35 | parallel (~> 1.10)
36 | parser (>= 3.3.0.2)
37 | rainbow (>= 2.2.2, < 4.0)
38 | regexp_parser (>= 2.9.3, < 3.0)
39 | rubocop-ast (>= 1.44.0, < 2.0)
40 | ruby-progressbar (~> 1.7)
41 | unicode-display_width (>= 2.4.0, < 4.0)
42 | rubocop-ast (1.44.1)
43 | parser (>= 3.3.7.2)
44 | prism (~> 1.4)
45 | rubocop-minitest (0.38.0)
46 | lint_roller (~> 1.1)
47 | rubocop (>= 1.75.0, < 2.0)
48 | rubocop-ast (>= 1.38.0, < 2.0)
49 | rubocop-performance (1.25.0)
50 | lint_roller (~> 1.1)
51 | rubocop (>= 1.75.0, < 2.0)
52 | rubocop-ast (>= 1.38.0, < 2.0)
53 | rubocop-rake (0.7.1)
54 | lint_roller (~> 1.1)
55 | rubocop (>= 1.72.1)
56 | rubocop-rspec (3.6.0)
57 | lint_roller (~> 1.1)
58 | rubocop (~> 1.72, >= 1.72.1)
59 | ruby-progressbar (1.13.0)
60 | stringio (3.1.7)
61 | threads (0.4.1)
62 | backtrace (~> 0)
63 | concurrent-ruby (~> 1.0)
64 | unicode-display_width (3.1.4)
65 | unicode-emoji (~> 4.0, >= 4.0.4)
66 | unicode-emoji (4.0.4)
67 |
68 | PLATFORMS
69 | arm64-darwin-22
70 | arm64-darwin-23
71 | arm64-darwin-24
72 | x64-mingw-ucrt
73 | x86_64-linux
74 |
75 | DEPENDENCIES
76 | minitest (~> 5.25)
77 | rake (~> 13.2)
78 | rdoc (~> 6.11)
79 | rubocop (~> 1.71)
80 | rubocop-minitest (> 0)
81 | rubocop-performance (> 0)
82 | rubocop-rake (> 0)
83 | rubocop-rspec (> 0)
84 | syncem!
85 | threads (~> 0.4)
86 |
87 | BUNDLED WITH
88 | 2.5.16
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://www.elegantobjects.org)
4 | [](https://www.rultor.com/p/yegor256/syncem)
5 | [](https://www.jetbrains.com/ruby/)
6 |
7 | [](https://github.com/yegor256/syncem/actions/workflows/rake.yml)
8 | [](https://badge.fury.io/rb/syncem)
9 | [](https://codeclimate.com/github/yegor256/syncem/maintainability)
10 | [](https://rubydoc.info/github/yegor256/syncem/master/frames)
11 | [](https://hitsofcode.com/view/github/yegor256/syncem)
12 | [](https://github.com/yegor256/syncem/blob/master/LICENSE.txt)
13 |
14 | Read this blog post:
15 | [_SyncEm: Thread-Safe Decorators in Ruby_](https://www.yegor256.com/2019/06/26/syncem.html).
16 |
17 | Sometimes you have an object that is not thread-safe,
18 | but you need to make sure each of its methods is thread-safe, because they
19 | deal with some resources, like files or databases and you want them to
20 | manage those resources sequentially. This small gem will help you achieve
21 | exactly that without any re-design of the objects you already have. Just
22 | decorate them with `SyncEm` [thread-safe decorator](https://www.yegor256.com/2017/01/17/synchronized-decorators.html)
23 | and that is it.
24 |
25 | First, install it:
26 |
27 | ```bash
28 | $ gem install syncem
29 | ```
30 |
31 | Then, use it like this:
32 |
33 | ```ruby
34 | require 'syncem'
35 | obj = SyncEm.new(obj)
36 | ```
37 |
38 | That's it.
39 |
40 | ## How to contribute
41 |
42 | Read [these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html).
43 | Make sure your build is green before you contribute
44 | your pull request. You will need to have [Ruby](https://www.ruby-lang.org/en/) 2.3+ and
45 | [Bundler](https://bundler.io/) installed. Then:
46 |
47 | ```
48 | $ bundle update
49 | $ bundle exec rake
50 | ```
51 |
52 | If it's clean and you don't see any error messages, submit your pull request.
53 |
--------------------------------------------------------------------------------
/test/test_syncem.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko
4 | # SPDX-License-Identifier: MIT
5 |
6 | require 'minitest/autorun'
7 | require 'threads'
8 | require_relative '../lib/syncem'
9 |
10 | # Syncem test.
11 | # Author:: Yegor Bugayenko (yegor256@gmail.com)
12 | # Copyright:: Copyright (c) 2018-2025 Yegor Bugayenko
13 | # License:: MIT
14 | class SyncEmTest < Minitest::Test
15 | class Account
16 | def initialize(file)
17 | @file = file
18 | end
19 |
20 | def balance
21 | File.exist?(@file) ? File.read(@file).to_i : 0
22 | end
23 |
24 | def add(amount)
25 | now = File.exist?(@file) ? File.read(@file).to_i : 0
26 | File.write(@file, (now + amount).to_s)
27 | end
28 |
29 | def read
30 | yield balance
31 | end
32 | end
33 |
34 | def test_wraps_simple_object
35 | Dir.mktmpdir do |dir|
36 | path = File.join(dir, 'f.txt')
37 | acc = Account.new(path)
38 | assert_equal(0, acc.balance)
39 | acc.add(50)
40 | assert_equal(50, acc.balance)
41 | acc.add(-10)
42 | assert_equal(40, acc.balance)
43 | end
44 | end
45 |
46 | def test_respond_to
47 | Dir.mktmpdir do |dir|
48 | path = File.join(dir, 'f.txt')
49 | acc = SyncEm.new(Account.new(path))
50 | assert_respond_to(acc, :balance)
51 | assert_respond_to(acc, :add)
52 | end
53 | end
54 |
55 | def test_works_with_send_method
56 | obj = Object.new
57 | def obj.send(_first, second)
58 | second
59 | end
60 | synced = SyncEm.new(obj)
61 | assert_equal(3, synced.send(2, 3))
62 | assert_equal({ y: 2 }, synced.send({ x: 1 }, { y: 2 }))
63 | end
64 |
65 | def test_works_with_optional_arguments
66 | obj = Object.new
67 | def obj.foo(first, _second, ext1: 'a', ext2: 'b')
68 | first + ext1 + ext2
69 | end
70 | synced = SyncEm.new(obj)
71 | assert_equal('.xy', synced.foo('.', {}, ext1: 'x', ext2: 'y'))
72 | assert_equal('fzb', synced.foo('f', {}, ext1: 'z'))
73 | assert_equal('-ab', synced.foo('-', {}))
74 | end
75 |
76 | def test_works_with_splat_arguments
77 | obj = Object.new
78 | def obj.foo(one, two, *rest)
79 | one + two + (rest.empty? ? '' : rest.first.to_s)
80 | end
81 | synced = SyncEm.new(obj)
82 | assert_equal('ab', synced.foo('a', 'b'))
83 | assert_equal('abc', synced.foo('a', 'b', 'c'))
84 | end
85 |
86 | def test_works_with_default_value
87 | obj = Object.new
88 | def obj.foo(first, second = 42)
89 | first + second
90 | end
91 | synced = SyncEm.new(obj)
92 | assert_equal(15, synced.foo(7, 8))
93 | assert_equal(43, synced.foo(1))
94 | end
95 |
96 | def test_works_with_block
97 | Dir.mktmpdir do |dir|
98 | path = File.join(dir, 'f.txt')
99 | acc = SyncEm.new(Account.new(path))
100 | acc.add(50)
101 | before = 0
102 | acc.read do |b|
103 | assert_equal(50, b)
104 | before = b
105 | end
106 | assert_equal(50, before)
107 | end
108 | end
109 |
110 | def test_multiple_threads
111 | Dir.mktmpdir do |dir|
112 | path = File.join(dir, 'f.txt')
113 | acc = SyncEm.new(Account.new(path))
114 | threads = 10
115 | Threads.new(threads).assert do
116 | acc.add(10)
117 | end
118 | assert_equal(threads * 10, acc.balance)
119 | end
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
20 |
--------------------------------------------------------------------------------