safe_memoize 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +38 -0
- data/.github/workflows/release.yml +91 -0
- data/CHANGELOG.md +13 -0
- data/README.md +82 -2
- data/lib/safe_memoize/release_tooling.rb +66 -0
- data/lib/safe_memoize/version.rb +1 -1
- data/lib/safe_memoize.rb +163 -27
- data/sig/safe_memoize.rbs +36 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00d20095844739078f88cd287d4389b749f9953b9b6055939d733f2da139c19d
|
|
4
|
+
data.tar.gz: 5c4336dbe5e3ee1e1e2b822b92a3abd4f6d5452dedd36d99706c54eb8528ed7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d14486d2a280e2e243e2c54184875d175447f8dc921ff9e152374e9f8327061b0b4ce6de0e9213871000c0950a8cab146db7d303a0de8242d081c30e54c53ce
|
|
7
|
+
data.tar.gz: 67be0eb15e01782f7dc6b0a35cb79c24677f8d56ebf042a9dfdd49d9ec88a44c08711af24063150688c0b1ad9047c70a6c3754fd1cc662ba54a83000c1c5247a
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
name: Ruby ${{ matrix.ruby }}
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
ruby:
|
|
21
|
+
- "3.2"
|
|
22
|
+
- "3.3"
|
|
23
|
+
- "3.4"
|
|
24
|
+
- "4.0"
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- name: Check out repository
|
|
28
|
+
uses: actions/checkout@v5
|
|
29
|
+
|
|
30
|
+
- name: Set up Ruby
|
|
31
|
+
uses: ruby/setup-ruby@v1
|
|
32
|
+
with:
|
|
33
|
+
ruby-version: ${{ matrix.ruby }}
|
|
34
|
+
bundler-cache: true
|
|
35
|
+
|
|
36
|
+
- name: Run test and lint suite
|
|
37
|
+
run: bundle exec rake
|
|
38
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
release:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- name: Check out repository
|
|
17
|
+
uses: actions/checkout@v5
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0
|
|
20
|
+
|
|
21
|
+
- name: Set up Ruby
|
|
22
|
+
uses: ruby/setup-ruby@v1
|
|
23
|
+
with:
|
|
24
|
+
ruby-version: "3.4"
|
|
25
|
+
bundler-cache: true
|
|
26
|
+
|
|
27
|
+
- name: Verify tag matches gem version
|
|
28
|
+
env:
|
|
29
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
30
|
+
run: |
|
|
31
|
+
expected_version="${RELEASE_TAG#v}"
|
|
32
|
+
actual_version="$(bundle exec ruby -e 'require_relative "lib/safe_memoize/version"; print SafeMemoize::VERSION')"
|
|
33
|
+
|
|
34
|
+
if [ "$actual_version" != "$expected_version" ]; then
|
|
35
|
+
echo "Tag $expected_version does not match gem version $actual_version"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
- name: Run test suite
|
|
40
|
+
run: bundle exec rake
|
|
41
|
+
|
|
42
|
+
- name: Build gem artifact
|
|
43
|
+
env:
|
|
44
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
45
|
+
run: |
|
|
46
|
+
version="${RELEASE_TAG#v}"
|
|
47
|
+
mkdir -p pkg
|
|
48
|
+
gem build safe_memoize.gemspec --output "pkg/safe_memoize-${version}.gem"
|
|
49
|
+
|
|
50
|
+
- name: Check whether the gem version is already on RubyGems
|
|
51
|
+
id: rubygems
|
|
52
|
+
env:
|
|
53
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
54
|
+
run: |
|
|
55
|
+
version="${RELEASE_TAG#v}"
|
|
56
|
+
|
|
57
|
+
if ruby -rjson -ropen-uri -e 'version = ARGV.fetch(0); versions = JSON.parse(URI.open("https://rubygems.org/api/v1/versions/safe_memoize.json", &:read)); exit(versions.any? { |item| item["number"] == version } ? 0 : 1)' "$version"; then
|
|
58
|
+
echo "already_published=true" >> "$GITHUB_OUTPUT"
|
|
59
|
+
else
|
|
60
|
+
echo "already_published=false" >> "$GITHUB_OUTPUT"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
- name: Skip RubyGems publish for existing versions
|
|
64
|
+
if: steps.rubygems.outputs.already_published == 'true'
|
|
65
|
+
env:
|
|
66
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
67
|
+
run: echo "RubyGems already has version ${RELEASE_TAG#v}; skipping gem push."
|
|
68
|
+
|
|
69
|
+
- name: Publish gem to RubyGems
|
|
70
|
+
if: steps.rubygems.outputs.already_published != 'true'
|
|
71
|
+
env:
|
|
72
|
+
GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
|
73
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
74
|
+
run: |
|
|
75
|
+
version="${RELEASE_TAG#v}"
|
|
76
|
+
gem push "pkg/safe_memoize-${version}.gem"
|
|
77
|
+
|
|
78
|
+
- name: Generate GitHub release notes from CHANGELOG
|
|
79
|
+
env:
|
|
80
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
81
|
+
run: |
|
|
82
|
+
version="${RELEASE_TAG#v}"
|
|
83
|
+
bundle exec ruby bin/release_notes "$version" "${RUNNER_TEMP}/release_notes.md"
|
|
84
|
+
|
|
85
|
+
- name: Create GitHub release
|
|
86
|
+
uses: softprops/action-gh-release@v2
|
|
87
|
+
with:
|
|
88
|
+
body_path: ${{ runner.temp }}/release_notes.md
|
|
89
|
+
files: pkg/*.gem
|
|
90
|
+
tag_name: ${{ github.ref_name }}
|
|
91
|
+
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.2] - 2026-05-13
|
|
4
|
+
|
|
5
|
+
- Preserve public, protected, and private visibility for memoized methods
|
|
6
|
+
- Allow reset_memo to clear one cached argument combination or all entries for a method
|
|
7
|
+
- Add a memoized? helper for checking whether a method call is already cached
|
|
8
|
+
- Add a memo_count helper for inspecting cache size per instance or method
|
|
9
|
+
- Add a memo_keys helper for inspecting cached argument signatures
|
|
10
|
+
- Add a memo_values helper for inspecting cached signatures and their values
|
|
11
|
+
|
|
12
|
+
## [0.1.1] - 2026-05-13
|
|
13
|
+
|
|
14
|
+
- Add automated release tooling plus a GitHub Actions workflow for RubyGems publishing and GitHub releases
|
|
15
|
+
|
|
3
16
|
## [0.1.0] - 2026-02-26
|
|
4
17
|
|
|
5
18
|
- Initial release
|
data/README.md
CHANGED
|
@@ -21,6 +21,12 @@ SafeMemoize uses `Hash#key?` to distinguish "not yet cached" from "cached nil/fa
|
|
|
21
21
|
- Thread-safe via double-check locking
|
|
22
22
|
- Zero runtime dependencies
|
|
23
23
|
- Simple `prepend` + `memoize` API
|
|
24
|
+
- Preserves public, protected, and private method visibility
|
|
25
|
+
- Supports targeted cache invalidation by argument combination
|
|
26
|
+
- Includes a `memoized?` helper for cache inspection
|
|
27
|
+
- Includes a `memo_count` helper for cache size stats
|
|
28
|
+
- Includes a `memo_keys` helper for inspecting cached signatures
|
|
29
|
+
- Includes a `memo_values` helper for inspecting cached signatures and values
|
|
24
30
|
- Block arguments bypass cache (blocks aren't comparable)
|
|
25
31
|
|
|
26
32
|
## Installation
|
|
@@ -94,12 +100,51 @@ class Config
|
|
|
94
100
|
end
|
|
95
101
|
```
|
|
96
102
|
|
|
103
|
+
### Works with private methods
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
class TokenProvider
|
|
107
|
+
prepend SafeMemoize
|
|
108
|
+
|
|
109
|
+
def bearer_token
|
|
110
|
+
token
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def token
|
|
116
|
+
fetch_token_from_service
|
|
117
|
+
end
|
|
118
|
+
memoize :token
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
97
122
|
### Cache reset
|
|
98
123
|
|
|
99
124
|
```ruby
|
|
100
125
|
obj = MyService.new
|
|
101
|
-
obj.reset_memo(:current_user)
|
|
102
|
-
obj.
|
|
126
|
+
obj.reset_memo(:current_user) # Clears all cached entries for one method
|
|
127
|
+
obj.reset_memo(:find_user, 42) # Clears only the cached call for find_user(42)
|
|
128
|
+
obj.reset_memo(:search, "ruby", page: 2) # Clears one positional/keyword combination
|
|
129
|
+
obj.reset_all_memos # Clears all memoized values
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Cache inspection
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
obj = MyService.new
|
|
136
|
+
|
|
137
|
+
obj.memoized?(:current_user) # => false
|
|
138
|
+
obj.current_user
|
|
139
|
+
obj.memoized?(:current_user) # => true
|
|
140
|
+
|
|
141
|
+
obj.memoized?(:search, "ruby", page: 2) # Checks one cached argument combination
|
|
142
|
+
obj.memo_count # Total cached entries for this instance
|
|
143
|
+
obj.memo_count(:search) # Cached entries for one method
|
|
144
|
+
obj.memo_keys # All cached signatures with method, args, kwargs
|
|
145
|
+
obj.memo_keys(:search) # Cached signatures for one method
|
|
146
|
+
obj.memo_values # Cached signatures and values for all methods
|
|
147
|
+
obj.memo_values(:search) # Cached signatures and values for one method
|
|
103
148
|
```
|
|
104
149
|
|
|
105
150
|
## How It Works
|
|
@@ -110,6 +155,41 @@ SafeMemoize uses Ruby's `prepend` mechanism. When you call `memoize :method_name
|
|
|
110
155
|
|
|
111
156
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt.
|
|
112
157
|
|
|
158
|
+
GitHub Actions also runs the full `bundle exec rake` suite automatically for pull requests, manual workflow runs, and pushes to `main` via `.github/workflows/ci.yml`.
|
|
159
|
+
|
|
160
|
+
## Releasing
|
|
161
|
+
|
|
162
|
+
Releases are automated in two parts:
|
|
163
|
+
|
|
164
|
+
1. Run `bin/release VERSION` locally to:
|
|
165
|
+
- update `lib/safe_memoize/version.rb`
|
|
166
|
+
- convert the current `## [Unreleased]` section in `CHANGELOG.md` into a dated release entry
|
|
167
|
+
- create the release commit and annotated tag
|
|
168
|
+
2. Push the branch and tag to GitHub. The workflow in `.github/workflows/release.yml` will:
|
|
169
|
+
- run the test and lint suite
|
|
170
|
+
- build the gem
|
|
171
|
+
- push it to RubyGems when that version is not already published
|
|
172
|
+
- create a GitHub release using the matching section from `CHANGELOG.md`
|
|
173
|
+
|
|
174
|
+
One-time setup:
|
|
175
|
+
|
|
176
|
+
- add a `RUBYGEMS_API_KEY` repository secret in GitHub
|
|
177
|
+
|
|
178
|
+
Typical release flow:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
bundle exec rake
|
|
182
|
+
bin/release 0.1.1
|
|
183
|
+
git push origin HEAD
|
|
184
|
+
git push origin v0.1.1
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
To preview the changelog/version update without changing anything, use:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
bin/release 0.1.1 --dry-run
|
|
191
|
+
```
|
|
192
|
+
|
|
113
193
|
## Contributing
|
|
114
194
|
|
|
115
195
|
Bug reports and pull requests are welcome on GitHub at https://github.com/eclectic-coding/safe_memoize.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module SafeMemoize
|
|
6
|
+
module ReleaseTooling
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
VERSION_PATTERN = /VERSION = "[^"]+"/
|
|
10
|
+
SEMVER_PATTERN = /\A\d+\.\d+\.\d+(?:[-.][0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?\z/
|
|
11
|
+
UNRELEASED_HEADING = "## [Unreleased]"
|
|
12
|
+
|
|
13
|
+
def normalize_version(version)
|
|
14
|
+
normalized_version = version.to_s.sub(/\Av/, "")
|
|
15
|
+
|
|
16
|
+
unless normalized_version.match?(SEMVER_PATTERN)
|
|
17
|
+
raise ArgumentError, "version must look like x.y.z"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
normalized_version
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def update_version_file(contents, version)
|
|
24
|
+
normalized_version = normalize_version(version)
|
|
25
|
+
|
|
26
|
+
unless contents.match?(VERSION_PATTERN)
|
|
27
|
+
raise ArgumentError, "version file does not define VERSION"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
contents.sub(VERSION_PATTERN, %(VERSION = "#{normalized_version}"))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def finalize_changelog(contents, version, date = Date.today)
|
|
34
|
+
normalized_version = normalize_version(version)
|
|
35
|
+
release_heading = "## [#{normalized_version}] - #{date.iso8601}"
|
|
36
|
+
|
|
37
|
+
unless contents.include?(UNRELEASED_HEADING)
|
|
38
|
+
raise ArgumentError, "CHANGELOG.md must contain an Unreleased heading"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if contents.match?(/^## \[#{Regexp.escape(normalized_version)}\](?: - .+)?$/)
|
|
42
|
+
raise ArgumentError, "CHANGELOG.md already contains #{normalized_version}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
contents.sub(UNRELEASED_HEADING, "#{UNRELEASED_HEADING}\n\n#{release_heading}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def extract_release_notes(contents, version)
|
|
49
|
+
normalized_version = normalize_version(version)
|
|
50
|
+
lines = contents.lines
|
|
51
|
+
release_heading = /^## \[#{Regexp.escape(normalized_version)}\](?: - .+)?$/
|
|
52
|
+
start_index = lines.index { |line| line.match?(release_heading) }
|
|
53
|
+
|
|
54
|
+
raise ArgumentError, "CHANGELOG.md is missing release notes for #{normalized_version}" unless start_index
|
|
55
|
+
|
|
56
|
+
body = lines[(start_index + 1)..].take_while { |line| !line.start_with?("## [") }.join.strip
|
|
57
|
+
body = "- No changes listed." if body.empty?
|
|
58
|
+
|
|
59
|
+
<<~MARKDOWN
|
|
60
|
+
## SafeMemoize #{normalized_version}
|
|
61
|
+
|
|
62
|
+
#{body}
|
|
63
|
+
MARKDOWN
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/safe_memoize/version.rb
CHANGED
data/lib/safe_memoize.rb
CHANGED
|
@@ -12,59 +12,195 @@ module SafeMemoize
|
|
|
12
12
|
module ClassMethods
|
|
13
13
|
def memoize(method_name)
|
|
14
14
|
method_name = method_name.to_sym
|
|
15
|
+
visibility = memoized_method_visibility(method_name)
|
|
15
16
|
|
|
16
17
|
mod = Module.new do
|
|
17
18
|
define_method(method_name) do |*args, **kwargs, &block|
|
|
18
19
|
# Blocks bypass cache entirely — they aren't comparable
|
|
19
20
|
return super(*args, **kwargs, &block) if block
|
|
20
21
|
|
|
21
|
-
cache_key =
|
|
22
|
-
|
|
23
|
-
@__safe_memo_mutex__ ||= Mutex.new
|
|
22
|
+
cache_key = safe_memo_cache_key(method_name, args, kwargs)
|
|
24
23
|
|
|
25
24
|
# Fast path: check without lock
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# Slow path: lock and double-check
|
|
31
|
-
@__safe_memo_mutex__.synchronize do
|
|
32
|
-
@__safe_memo_cache__ ||= {}
|
|
33
|
-
|
|
34
|
-
if @__safe_memo_cache__.key?(cache_key)
|
|
35
|
-
@__safe_memo_cache__[cache_key]
|
|
36
|
-
else
|
|
37
|
-
@__safe_memo_cache__[cache_key] = super(*args, **kwargs)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
25
|
+
return memo_cache_read(cache_key) if memo_cache_hit?(cache_key)
|
|
26
|
+
|
|
27
|
+
memo_fetch_or_store(cache_key) { super(*args, **kwargs) }
|
|
40
28
|
end
|
|
29
|
+
|
|
30
|
+
send(visibility, method_name)
|
|
41
31
|
end
|
|
42
32
|
|
|
43
33
|
prepend mod
|
|
44
34
|
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def memoized_method_visibility(method_name)
|
|
39
|
+
return :private if private_method_defined?(method_name)
|
|
40
|
+
return :protected if protected_method_defined?(method_name)
|
|
41
|
+
|
|
42
|
+
:public
|
|
43
|
+
end
|
|
45
44
|
end
|
|
46
45
|
|
|
47
|
-
def
|
|
46
|
+
def memoized?(method_name, *args, **kwargs, &block)
|
|
47
|
+
return false if block
|
|
48
|
+
|
|
49
|
+
cache_key = safe_memo_cache_key(method_name, args, kwargs)
|
|
50
|
+
|
|
51
|
+
with_memo_lock do
|
|
52
|
+
with_memo_cache { |cache| cache.key?(cache_key) } || false
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def memo_count(*method_name)
|
|
57
|
+
scoped_method = safe_memo_scoped_method(method_name)
|
|
58
|
+
|
|
59
|
+
with_memo_lock do
|
|
60
|
+
safe_memo_count_for(scoped_method)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def memo_keys(*method_name)
|
|
65
|
+
scoped_method = safe_memo_scoped_method(method_name)
|
|
66
|
+
|
|
67
|
+
with_memo_lock do
|
|
68
|
+
safe_memo_keys_for(scoped_method)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def memo_values(*method_name)
|
|
73
|
+
scoped_method = safe_memo_scoped_method(method_name)
|
|
74
|
+
|
|
75
|
+
with_memo_lock do
|
|
76
|
+
safe_memo_values_for(scoped_method)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def reset_memo(method_name, *args, **kwargs)
|
|
48
81
|
method_name = method_name.to_sym
|
|
49
82
|
|
|
50
|
-
|
|
83
|
+
matcher = memo_matcher_for(method_name, args, kwargs)
|
|
51
84
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
85
|
+
with_memo_lock do
|
|
86
|
+
with_memo_cache do |cache|
|
|
87
|
+
cache.delete_if { |key, _| matcher.call(key) }
|
|
55
88
|
end
|
|
56
|
-
else
|
|
57
|
-
@__safe_memo_cache__.delete_if { |key, _| key[0] == method_name }
|
|
58
89
|
end
|
|
59
90
|
end
|
|
60
91
|
|
|
61
92
|
def reset_all_memos
|
|
93
|
+
with_memo_lock do
|
|
94
|
+
@__safe_memo_cache__ = {}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def safe_memo_scoped_method(method_name)
|
|
101
|
+
raise ArgumentError, "expected 0 or 1 arguments" if method_name.length > 1
|
|
102
|
+
|
|
103
|
+
method_name.first&.to_sym
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def with_memo_lock
|
|
62
107
|
if defined?(@__safe_memo_mutex__) && @__safe_memo_mutex__
|
|
63
|
-
@__safe_memo_mutex__.synchronize
|
|
64
|
-
|
|
108
|
+
@__safe_memo_mutex__.synchronize { yield }
|
|
109
|
+
else
|
|
110
|
+
yield
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def memo_cache_or_nil
|
|
115
|
+
return nil unless defined?(@__safe_memo_cache__)
|
|
116
|
+
|
|
117
|
+
@__safe_memo_cache__
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def memo_cache_hit?(cache_key)
|
|
121
|
+
cache = memo_cache_or_nil
|
|
122
|
+
cache&.key?(cache_key)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def memo_cache_read(cache_key)
|
|
126
|
+
cache = memo_cache_or_nil
|
|
127
|
+
cache && cache[cache_key]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def memo_fetch_or_store(cache_key)
|
|
131
|
+
memo_mutex!.synchronize do
|
|
132
|
+
@__safe_memo_cache__ ||= {}
|
|
133
|
+
|
|
134
|
+
if @__safe_memo_cache__.key?(cache_key)
|
|
135
|
+
@__safe_memo_cache__[cache_key]
|
|
136
|
+
else
|
|
137
|
+
@__safe_memo_cache__[cache_key] = yield
|
|
65
138
|
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def memo_mutex!
|
|
143
|
+
@__safe_memo_mutex__ ||= Mutex.new
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def with_memo_cache
|
|
147
|
+
cache = memo_cache_or_nil
|
|
148
|
+
return nil unless cache
|
|
149
|
+
|
|
150
|
+
yield cache
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def memo_matcher_for(method_name, args, kwargs)
|
|
154
|
+
if args.empty? && kwargs.empty?
|
|
155
|
+
->(key) { key[0] == method_name }
|
|
66
156
|
else
|
|
67
|
-
|
|
157
|
+
cache_key = safe_memo_cache_key(method_name, args, kwargs)
|
|
158
|
+
->(key) { key == cache_key }
|
|
68
159
|
end
|
|
69
160
|
end
|
|
161
|
+
|
|
162
|
+
def memo_entries_for(method_name)
|
|
163
|
+
cache = memo_cache_or_nil
|
|
164
|
+
return [] unless cache
|
|
165
|
+
|
|
166
|
+
entries = cache.to_a
|
|
167
|
+
return entries unless method_name
|
|
168
|
+
|
|
169
|
+
entries.select { |(cache_key, _)| cache_key[0] == method_name }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def safe_memo_count_for(method_name)
|
|
173
|
+
memo_entries_for(method_name).length
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def safe_memo_keys_for(method_name)
|
|
177
|
+
entries = memo_entries_for(method_name)
|
|
178
|
+
include_method = method_name.nil?
|
|
179
|
+
|
|
180
|
+
entries.map do |(cache_key, value)|
|
|
181
|
+
memo_projection(cache_key, value, include_method: include_method, include_value: false)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def safe_memo_values_for(method_name)
|
|
186
|
+
entries = memo_entries_for(method_name)
|
|
187
|
+
include_method = method_name.nil?
|
|
188
|
+
|
|
189
|
+
entries.map do |(cache_key, value)|
|
|
190
|
+
memo_projection(cache_key, value, include_method: include_method, include_value: true)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def memo_projection(cache_key, value, include_method:, include_value:)
|
|
195
|
+
method_name, args, kwargs = cache_key
|
|
196
|
+
|
|
197
|
+
payload = {args: args, kwargs: kwargs}
|
|
198
|
+
payload[:method] = method_name if include_method
|
|
199
|
+
payload[:value] = value if include_value
|
|
200
|
+
payload
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def safe_memo_cache_key(method_name, args, kwargs)
|
|
204
|
+
[method_name.to_sym, args, kwargs]
|
|
205
|
+
end
|
|
70
206
|
end
|
data/sig/safe_memoize.rbs
CHANGED
|
@@ -1,12 +1,47 @@
|
|
|
1
1
|
module SafeMemoize
|
|
2
2
|
VERSION: String
|
|
3
3
|
|
|
4
|
+
type memo_key = [Symbol, Array[untyped], Hash[Symbol, untyped]]
|
|
5
|
+
type memo_entry = [memo_key, untyped]
|
|
6
|
+
|
|
7
|
+
@__safe_memo_cache__: Hash[memo_key, untyped]?
|
|
8
|
+
@__safe_memo_mutex__: Mutex?
|
|
9
|
+
|
|
4
10
|
def self.prepended: (Class base) -> void
|
|
5
11
|
|
|
6
|
-
def
|
|
12
|
+
def memoized?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
|
|
13
|
+
def memo_count: () -> Integer
|
|
14
|
+
def memo_count: (Symbol | String method_name) -> Integer
|
|
15
|
+
def memo_keys: () -> Array[untyped]
|
|
16
|
+
def memo_keys: (Symbol | String method_name) -> Array[untyped]
|
|
17
|
+
def memo_values: () -> Array[untyped]
|
|
18
|
+
def memo_values: (Symbol | String method_name) -> Array[untyped]
|
|
19
|
+
def reset_memo: (Symbol | String method_name, *untyped args, **untyped kwargs) -> void
|
|
7
20
|
def reset_all_memos: () -> void
|
|
8
21
|
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def safe_memo_scoped_method: (Array[untyped] method_name) -> Symbol?
|
|
25
|
+
def with_memo_lock: { () -> untyped } -> untyped
|
|
26
|
+
def memo_cache_or_nil: () -> Hash[memo_key, untyped]?
|
|
27
|
+
def memo_cache_hit?: (memo_key cache_key) -> bool
|
|
28
|
+
def memo_cache_read: (memo_key cache_key) -> untyped?
|
|
29
|
+
def memo_fetch_or_store: (memo_key cache_key) { () -> untyped } -> untyped
|
|
30
|
+
def memo_mutex!: () -> Mutex
|
|
31
|
+
def with_memo_cache: { (Hash[memo_key, untyped] cache) -> untyped } -> untyped?
|
|
32
|
+
def memo_matcher_for: (Symbol method_name, Array[untyped] args, Hash[Symbol, untyped] kwargs) -> ((memo_key) -> bool)
|
|
33
|
+
def memo_entries_for: (Symbol? method_name) -> Array[memo_entry]
|
|
34
|
+
def safe_memo_count_for: (Symbol? method_name) -> Integer
|
|
35
|
+
def safe_memo_keys_for: (Symbol? method_name) -> Array[untyped]
|
|
36
|
+
def safe_memo_values_for: (Symbol? method_name) -> Array[untyped]
|
|
37
|
+
def memo_projection: (memo_key cache_key, untyped value, include_method: bool, include_value: bool) -> Hash[Symbol, untyped]
|
|
38
|
+
def safe_memo_cache_key: (Symbol | String method_name, Array[untyped] args, Hash[Symbol, untyped] kwargs) -> memo_key
|
|
39
|
+
|
|
9
40
|
module ClassMethods
|
|
10
41
|
def memoize: (Symbol | String method_name) -> void
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def memoized_method_visibility: (Symbol method_name) -> (:private | :protected | :public)
|
|
11
46
|
end
|
|
12
47
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: safe_memoize
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -18,11 +18,14 @@ executables: []
|
|
|
18
18
|
extensions: []
|
|
19
19
|
extra_rdoc_files: []
|
|
20
20
|
files:
|
|
21
|
+
- ".github/workflows/ci.yml"
|
|
22
|
+
- ".github/workflows/release.yml"
|
|
21
23
|
- CHANGELOG.md
|
|
22
24
|
- LICENSE.txt
|
|
23
25
|
- README.md
|
|
24
26
|
- Rakefile
|
|
25
27
|
- lib/safe_memoize.rb
|
|
28
|
+
- lib/safe_memoize/release_tooling.rb
|
|
26
29
|
- lib/safe_memoize/version.rb
|
|
27
30
|
- sig/safe_memoize.rbs
|
|
28
31
|
homepage: https://github.com/eclectic-coding/safe_memoize
|
|
@@ -47,7 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
47
50
|
- !ruby/object:Gem::Version
|
|
48
51
|
version: '0'
|
|
49
52
|
requirements: []
|
|
50
|
-
rubygems_version:
|
|
53
|
+
rubygems_version: 3.6.9
|
|
51
54
|
specification_version: 4
|
|
52
55
|
summary: Thread-safe memoization that correctly handles nil and false values
|
|
53
56
|
test_files: []
|