registry 0.2.0.pre.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +65 -19
- data/lib/registry/index_store.rb +58 -0
- data/lib/registry/method_watcher.rb +104 -0
- data/lib/registry/query_cache.rb +48 -0
- data/lib/registry/version.rb +3 -0
- data/lib/registry.rb +140 -72
- metadata +92 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60730b7e7b1943b16358e9ad787af3df00955cbdf4c3acfe44d1099679a4203f
|
|
4
|
+
data.tar.gz: 3aa8fa552783586504f34bf54ec7f52e1f4362ba9504ba351c379fc4ff94255c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c54ebc5c5ad44ce5e8aba1668a476733e599841d200846a55c9ff12fb6baa5b38d97b9e654c27b473ceb66e66fa9aabf2770b3d8c2b29ea6bd68787fc9dab191
|
|
7
|
+
data.tar.gz: f860b911818a5863c521be653a3dadb9bc077f1e1015c14cc4f70e68cef015a128637ddb70cecb9932767d7a07438d85fb3754c65c181b670e7720520ac86200
|
data/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## [0.3.0] - TBD
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Thread Safety**: Added optional `thread_safe: true` parameter for concurrent
|
|
8
|
+
access
|
|
9
|
+
- **Enhanced Error Handling**: New exception hierarchy with
|
|
10
|
+
`Registry::IndexNotFound`, `Registry::MissingAttributeError`
|
|
11
|
+
- **API Enhancements**:
|
|
12
|
+
- `exists?(criteria)` method for checking item existence
|
|
13
|
+
- Better error messages with contextual information
|
|
14
|
+
- **Memory Management**:
|
|
15
|
+
- Automatic cleanup of method watching
|
|
16
|
+
- `cleanup!` method for manual memory management
|
|
17
|
+
- Tracking of watched objects to prevent memory leaks
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Improved error messages with more context and suggestions
|
|
22
|
+
- Better handling of edge cases in `where` method
|
|
23
|
+
- Enhanced initialization to support new features
|
|
24
|
+
|
|
25
|
+
### Technical Improvements
|
|
26
|
+
|
|
27
|
+
- Added comprehensive test coverage for new features
|
|
28
|
+
- Improved code organization and documentation
|
|
29
|
+
- Better handling of thread safety concerns
|
|
30
|
+
|
|
31
|
+
## [0.2.0] - Previous Release
|
|
32
|
+
|
|
33
|
+
- Basic registry functionality
|
|
34
|
+
- Method watching for automatic reindexing
|
|
35
|
+
- Core indexing and querying capabilities
|
data/README.md
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
[](https://rubygems.org/gems/registry)
|
|
2
|
-
[](https://travis-ci.org/TwilightCoders/registry)
|
|
3
|
-
[](https://codeclimate.com/github/TwilightCoders/registry/maintainability)
|
|
4
|
-
[](https://codeclimate.com/github/TwilightCoders/registry/test_coverage)
|
|
5
|
-
[](https://gemnasium.com/github.com/TwilightCoders/registry)
|
|
6
|
-
|
|
7
1
|
# Registry
|
|
8
2
|
|
|
3
|
+
[](https://rubygems.org/gems/registry)
|
|
4
|
+
[](https://github.com/TwilightCoders/registry/actions)
|
|
5
|
+
[](https://qlty.sh)
|
|
6
|
+
|
|
9
7
|
Provides a data structure for collecting homogeneous objects and indexing them for quick lookup.
|
|
10
8
|
|
|
11
9
|
## Requirements
|
|
12
|
-
|
|
10
|
+
|
|
11
|
+
Ruby 3.0+
|
|
13
12
|
|
|
14
13
|
## Installation
|
|
15
14
|
|
|
@@ -21,28 +20,72 @@ gem 'registry'
|
|
|
21
20
|
|
|
22
21
|
And then execute:
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
```bash
|
|
24
|
+
bundle
|
|
25
|
+
```
|
|
25
26
|
|
|
26
27
|
Or install it yourself as:
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
```bash
|
|
30
|
+
gem install registry
|
|
31
|
+
```
|
|
29
32
|
|
|
30
33
|
## Usage
|
|
31
34
|
|
|
35
|
+
### Basic Usage
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
Person = Struct.new(:id, :name, :email)
|
|
39
|
+
|
|
40
|
+
registry = Registry.new([
|
|
41
|
+
Person.new(1, 'Dale', 'jason@twilightcoders.net'),
|
|
42
|
+
Person.new(2, 'Dale', 'dale@twilightcoders.net')
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
registry.index(:name)
|
|
46
|
+
|
|
47
|
+
# Find items using where method
|
|
48
|
+
results = registry.where(name: 'Dale')
|
|
49
|
+
|
|
50
|
+
# Check if items exist
|
|
51
|
+
registry.exists?(name: 'Dale') #=> true
|
|
52
|
+
|
|
53
|
+
# Automatic reindexing when attributes change
|
|
54
|
+
d = registry.where(name: 'Dale').first
|
|
55
|
+
d.name = "Jason"
|
|
56
|
+
registry.where(name: 'Jason') # Contains the updated item
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Advanced Features
|
|
60
|
+
|
|
61
|
+
#### Thread Safety
|
|
62
|
+
|
|
32
63
|
```ruby
|
|
33
|
-
|
|
64
|
+
# Create a thread-safe registry
|
|
65
|
+
registry = Registry.new(items, thread_safe: true)
|
|
66
|
+
```
|
|
34
67
|
|
|
35
|
-
|
|
36
|
-
Person.new(1, 'Dale', 'jason@twilightcoders.net'),
|
|
37
|
-
Person.new(2, 'Dale', 'dale@twilightcoders.net')
|
|
38
|
-
])
|
|
68
|
+
#### Memory Management
|
|
39
69
|
|
|
40
|
-
|
|
70
|
+
```ruby
|
|
71
|
+
# Clean up method watching for long-lived registries
|
|
72
|
+
registry.cleanup!
|
|
73
|
+
```
|
|
41
74
|
|
|
42
|
-
|
|
43
|
-
d.name = "Jason"
|
|
75
|
+
#### Error Handling
|
|
44
76
|
|
|
45
|
-
|
|
77
|
+
```ruby
|
|
78
|
+
begin
|
|
79
|
+
registry.where(nonexistent_index: 'value')
|
|
80
|
+
rescue Registry::IndexNotFound => e
|
|
81
|
+
puts "Index not found: #{e.message}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
begin
|
|
85
|
+
registry.add("invalid item")
|
|
86
|
+
rescue Registry::MissingAttributeError => e
|
|
87
|
+
puts "Missing required attributes: #{e.message}"
|
|
88
|
+
end
|
|
46
89
|
```
|
|
47
90
|
|
|
48
91
|
## Development
|
|
@@ -51,7 +94,10 @@ After checking out the repo, run `bundle` to install dependencies. Then, run `bu
|
|
|
51
94
|
|
|
52
95
|
## Contributing
|
|
53
96
|
|
|
54
|
-
Bug reports and pull requests are welcome on GitHub at
|
|
97
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
98
|
+
<https://github.com/TwilightCoders/registry>. This project is intended to be a safe,
|
|
99
|
+
welcoming space for collaboration, and contributors are expected to adhere to the
|
|
100
|
+
[Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
55
101
|
|
|
56
102
|
## License
|
|
57
103
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
# Manages index storage and reindexing operations
|
|
6
|
+
module RegistryIndexStore
|
|
7
|
+
attr_reader :indexes
|
|
8
|
+
|
|
9
|
+
def initialize_index_store
|
|
10
|
+
@indexed = {}
|
|
11
|
+
@indexes = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Add one or more indexes to the registry
|
|
15
|
+
def index(*new_indexes)
|
|
16
|
+
new_indexes.each do |idx|
|
|
17
|
+
warn "Index #{idx} already exists!" and next if @indexed.key?(idx)
|
|
18
|
+
|
|
19
|
+
# OPTIMIZE: Build index hash directly instead of using group_by + transformation
|
|
20
|
+
index_hash = {}
|
|
21
|
+
each do |item|
|
|
22
|
+
watch_setter(item, idx)
|
|
23
|
+
add_to_watched_objects(item) # Track watched objects
|
|
24
|
+
|
|
25
|
+
# Get the index value and build the index in one pass
|
|
26
|
+
idx_value = item.send(idx)
|
|
27
|
+
(index_hash[idx_value] ||= Set.new) << item
|
|
28
|
+
end
|
|
29
|
+
@indexed[idx] = index_hash
|
|
30
|
+
@indexes << idx
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Rebuild all indexes from scratch
|
|
35
|
+
def reindex!(new_indexes = [])
|
|
36
|
+
@indexed.clear
|
|
37
|
+
@indexes.clear
|
|
38
|
+
index(*new_indexes)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Update an index when an item's value changes
|
|
42
|
+
def reindex_item(idx, item, old_value, new_value)
|
|
43
|
+
return unless @indexed.key?(idx)
|
|
44
|
+
|
|
45
|
+
@indexed[idx][old_value].delete item
|
|
46
|
+
(@indexed[idx][new_value] ||= Set.new).add item
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Look up items by index value
|
|
50
|
+
def lookup_index(idx, value)
|
|
51
|
+
@indexed.dig(idx, value) || Set.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check if an index exists
|
|
55
|
+
def index_exists?(idx)
|
|
56
|
+
@indexed.key?(idx)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
# Manages method watching for automatic reindexing when object attributes change
|
|
6
|
+
module RegistryMethodWatcher
|
|
7
|
+
def initialize_method_watcher
|
|
8
|
+
@watched_objects = Set.new
|
|
9
|
+
@method_cache = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Add an item to the watched objects set
|
|
13
|
+
def add_to_watched_objects(item)
|
|
14
|
+
@watched_objects.add(item)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Remove an item from the watched objects set
|
|
18
|
+
def remove_from_watched_objects(item)
|
|
19
|
+
@watched_objects.delete(item)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Set up watching on a setter method to trigger reindexing
|
|
23
|
+
def watch_setter(item, idx)
|
|
24
|
+
return if item.frozen?
|
|
25
|
+
|
|
26
|
+
ensure_registry_reference(item)
|
|
27
|
+
setter_method = lookup_setter_method(item.class, idx)
|
|
28
|
+
return unless setter_method
|
|
29
|
+
|
|
30
|
+
watched_method = :"__watched_#{setter_method}"
|
|
31
|
+
return if item.methods.include?(watched_method)
|
|
32
|
+
|
|
33
|
+
install_watched_method(item, idx, setter_method, watched_method)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Remove watching from a setter method
|
|
37
|
+
def ignore_setter(item, idx)
|
|
38
|
+
return if item.frozen?
|
|
39
|
+
|
|
40
|
+
# Use cached method lookup
|
|
41
|
+
item_class = item.class
|
|
42
|
+
cache_key = [item_class, idx]
|
|
43
|
+
setter_method = @method_cache[cache_key]
|
|
44
|
+
|
|
45
|
+
return unless setter_method
|
|
46
|
+
|
|
47
|
+
original_method = setter_method
|
|
48
|
+
watched_method = :"__watched_#{original_method}"
|
|
49
|
+
renamed_method = :"__unwatched_#{original_method}"
|
|
50
|
+
|
|
51
|
+
return unless item.methods.include?(watched_method)
|
|
52
|
+
|
|
53
|
+
item.singleton_class.class_eval do
|
|
54
|
+
alias_method original_method, renamed_method
|
|
55
|
+
remove_method(watched_method)
|
|
56
|
+
remove_method(renamed_method)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Clean up all watched methods from all watched objects
|
|
61
|
+
def cleanup_watched_methods
|
|
62
|
+
@watched_objects.each do |item|
|
|
63
|
+
indexes.each do |idx|
|
|
64
|
+
ignore_setter(item, idx)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
@watched_objects.clear
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Full cleanup of watched methods and cache
|
|
71
|
+
def cleanup!
|
|
72
|
+
cleanup_watched_methods
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def lookup_setter_method(item_class, idx)
|
|
78
|
+
cache_key = [item_class, idx]
|
|
79
|
+
@method_cache[cache_key] ||= begin
|
|
80
|
+
method_name = :"#{idx}="
|
|
81
|
+
item_class.instance_methods.include?(method_name) ? method_name : nil
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def ensure_registry_reference(item)
|
|
86
|
+
item.instance_variable_set(:@__registry__, self) unless item.instance_variable_defined?(:@__registry__)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def install_watched_method(item, idx, setter_method, watched_method)
|
|
90
|
+
original_method = setter_method
|
|
91
|
+
renamed_method = :"__unwatched_#{original_method}"
|
|
92
|
+
|
|
93
|
+
item.singleton_class.class_eval do
|
|
94
|
+
define_method(watched_method) do |*args|
|
|
95
|
+
old_value = send(idx)
|
|
96
|
+
send(renamed_method, *args).tap do |new_value|
|
|
97
|
+
instance_variable_get(:@__registry__).send(:reindex_item, idx, self, old_value, new_value)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
alias_method renamed_method, original_method
|
|
101
|
+
alias_method original_method, watched_method
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Manages query result caching for performance optimization
|
|
4
|
+
module RegistryQueryCache
|
|
5
|
+
CACHE_SIZE_LIMIT = 1000
|
|
6
|
+
|
|
7
|
+
def initialize_query_cache
|
|
8
|
+
@query_cache = {}
|
|
9
|
+
@cache_hits = 0
|
|
10
|
+
@cache_misses = 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Check cache for a query result
|
|
14
|
+
def check_cache(cache_key)
|
|
15
|
+
if @query_cache.key?(cache_key)
|
|
16
|
+
@cache_hits += 1
|
|
17
|
+
@query_cache[cache_key]
|
|
18
|
+
else
|
|
19
|
+
@cache_misses += 1
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Store a query result in the cache
|
|
25
|
+
def store_in_cache(cache_key, result_set)
|
|
26
|
+
return if @query_cache.size >= CACHE_SIZE_LIMIT
|
|
27
|
+
|
|
28
|
+
@query_cache[cache_key] = result_set.dup
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Invalidate all cached queries (called when registry changes)
|
|
32
|
+
def invalidate_cache
|
|
33
|
+
@query_cache.clear
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get cache performance statistics
|
|
37
|
+
def cache_stats
|
|
38
|
+
total_queries = @cache_hits + @cache_misses
|
|
39
|
+
return { hits: 0, misses: 0, hit_rate: 0.0, total_queries: 0 } if total_queries.zero?
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
hits: @cache_hits,
|
|
43
|
+
misses: @cache_misses,
|
|
44
|
+
hit_rate: (@cache_hits.to_f / total_queries * 100).round(2),
|
|
45
|
+
total_queries: total_queries
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/registry.rb
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'registry/version'
|
|
4
|
+
|
|
5
|
+
require 'set'
|
|
6
|
+
require_relative 'registry/index_store'
|
|
7
|
+
require_relative 'registry/query_cache'
|
|
8
|
+
require_relative 'registry/method_watcher'
|
|
9
|
+
|
|
1
10
|
class Registry < Set
|
|
11
|
+
include RegistryIndexStore
|
|
12
|
+
include RegistryQueryCache
|
|
13
|
+
include RegistryMethodWatcher
|
|
2
14
|
|
|
3
|
-
|
|
15
|
+
# Exception classes for better error handling
|
|
16
|
+
class RegistryError < StandardError; end
|
|
17
|
+
class MoreThanOneRecordFound < RegistryError; end
|
|
18
|
+
class IndexNotFound < RegistryError; end
|
|
19
|
+
class MissingAttributeError < RegistryError; end
|
|
20
|
+
class ThreadSafetyError < RegistryError; end
|
|
4
21
|
|
|
5
|
-
VERSION =
|
|
22
|
+
VERSION = REGISTRY_VERSION
|
|
6
23
|
|
|
7
24
|
DEFAULT_INDEX = :object_id
|
|
8
25
|
|
|
9
|
-
def initialize(*args, indexes: [])
|
|
10
|
-
@
|
|
26
|
+
def initialize(*args, indexes: [], thread_safe: false)
|
|
27
|
+
@thread_safe = thread_safe
|
|
28
|
+
@mutex = Mutex.new if @thread_safe
|
|
29
|
+
|
|
30
|
+
# Initialize module-specific state
|
|
31
|
+
initialize_index_store
|
|
32
|
+
initialize_query_cache
|
|
33
|
+
initialize_method_watcher
|
|
34
|
+
|
|
11
35
|
super(*args)
|
|
12
36
|
reindex!(indexes)
|
|
13
37
|
end
|
|
@@ -20,10 +44,6 @@ class Registry < Set
|
|
|
20
44
|
@indexed
|
|
21
45
|
end
|
|
22
46
|
|
|
23
|
-
def indexes
|
|
24
|
-
@indexed.keys - [:object_id]
|
|
25
|
-
end
|
|
26
|
-
|
|
27
47
|
def delete(item)
|
|
28
48
|
@indexed.each do |idx, store|
|
|
29
49
|
ignore_setter(item, idx) if include?(item)
|
|
@@ -31,11 +51,15 @@ class Registry < Set
|
|
|
31
51
|
idx_value = item.send(idx)
|
|
32
52
|
(store[idx_value] ||= Set.new).delete(item)
|
|
33
53
|
store.delete(idx_value) if store[idx_value].empty?
|
|
34
|
-
rescue NoMethodError
|
|
35
|
-
raise
|
|
54
|
+
rescue NoMethodError
|
|
55
|
+
raise MissingAttributeError,
|
|
56
|
+
"Item #{item.inspect} cannot be deleted because indexable attribute '#{idx}' " \
|
|
57
|
+
'is missing or not accessible.'
|
|
36
58
|
end
|
|
37
59
|
end
|
|
38
|
-
|
|
60
|
+
remove_from_watched_objects(item)
|
|
61
|
+
invalidate_cache
|
|
62
|
+
super
|
|
39
63
|
end
|
|
40
64
|
|
|
41
65
|
def add(item)
|
|
@@ -43,99 +67,143 @@ class Registry < Set
|
|
|
43
67
|
watch_setter(item, idx) unless include?(item)
|
|
44
68
|
begin
|
|
45
69
|
idx_value = item.send(idx)
|
|
46
|
-
(store[idx_value] ||= Set.new) <<
|
|
47
|
-
rescue NoMethodError
|
|
48
|
-
raise
|
|
70
|
+
(store[idx_value] ||= Set.new) << item
|
|
71
|
+
rescue NoMethodError
|
|
72
|
+
raise MissingAttributeError,
|
|
73
|
+
"Item #{item.inspect} cannot be added because indexable attribute '#{idx}' is missing or not accessible."
|
|
49
74
|
end
|
|
50
75
|
end
|
|
51
|
-
|
|
76
|
+
add_to_watched_objects(item) unless include?(item)
|
|
77
|
+
invalidate_cache
|
|
78
|
+
super
|
|
52
79
|
end
|
|
53
80
|
alias << add
|
|
54
81
|
|
|
55
82
|
def find!(search_criteria)
|
|
56
|
-
_find(search_criteria) { raise MoreThanOneRecordFound,
|
|
83
|
+
_find(search_criteria) { raise MoreThanOneRecordFound, 'There were more than 1 records found' }
|
|
57
84
|
end
|
|
58
85
|
|
|
59
86
|
def find(search_criteria)
|
|
60
|
-
_find(search_criteria) { warn
|
|
87
|
+
_find(search_criteria) { warn 'There were more than 1 records found' }
|
|
61
88
|
end
|
|
62
89
|
|
|
63
|
-
def where(search_criteria)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
90
|
+
def where(limit: nil, offset: 0, **search_criteria)
|
|
91
|
+
with_thread_safety do
|
|
92
|
+
# Handle nil or empty criteria
|
|
93
|
+
return new_registry_from_set(Set.new) if search_criteria.nil? || search_criteria.empty?
|
|
94
|
+
|
|
95
|
+
cache_key = [:where, search_criteria.sort, limit, offset]
|
|
96
|
+
cached_result = check_cache(cache_key)
|
|
97
|
+
return new_registry_from_set(cached_result) if cached_result
|
|
98
|
+
|
|
99
|
+
result_set = if search_criteria.size == 1
|
|
100
|
+
single_criteria_search(search_criteria)
|
|
101
|
+
else
|
|
102
|
+
multi_criteria_search(search_criteria)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Apply pagination if specified
|
|
106
|
+
if limit || offset.positive?
|
|
107
|
+
records_array = result_set.to_a
|
|
108
|
+
start_idx = offset
|
|
109
|
+
end_idx = limit ? start_idx + limit - 1 : -1
|
|
110
|
+
result_set = Set.new(records_array[start_idx..end_idx] || [])
|
|
111
|
+
end
|
|
68
112
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
113
|
+
store_in_cache(cache_key, result_set)
|
|
114
|
+
new_registry_from_set(result_set)
|
|
115
|
+
end
|
|
72
116
|
end
|
|
73
117
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
indexed_records = group_by { |a| a.send(idx) }
|
|
79
|
-
indexed_sets = Hash[indexed_records.keys.zip(indexed_records.values.map { |e| Set.new(e) })]
|
|
80
|
-
@indexed[idx] = indexed_sets
|
|
118
|
+
# Check if any items exist matching the criteria
|
|
119
|
+
def exists?(**search_criteria)
|
|
120
|
+
with_thread_safety do
|
|
121
|
+
search_criteria.size == 1 ? single_criteria_exists?(search_criteria) : multi_criteria_exists?(search_criteria)
|
|
81
122
|
end
|
|
82
123
|
end
|
|
83
124
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
125
|
+
# Count items matching the criteria without creating a Registry object
|
|
126
|
+
def count_where(**search_criteria)
|
|
127
|
+
with_thread_safety do
|
|
128
|
+
return 0 if search_criteria.nil? || search_criteria.empty?
|
|
129
|
+
|
|
130
|
+
result_set = if search_criteria.size == 1
|
|
131
|
+
single_criteria_search(search_criteria)
|
|
132
|
+
else
|
|
133
|
+
multi_criteria_search(search_criteria)
|
|
134
|
+
end
|
|
135
|
+
result_set.size
|
|
136
|
+
end
|
|
87
137
|
end
|
|
88
138
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
(@indexed[idx][new_value] ||= Set.new).add item
|
|
95
|
-
end
|
|
139
|
+
def reindex!(new_indexes = [])
|
|
140
|
+
cleanup_watched_methods # Clean up before reindexing
|
|
141
|
+
@indexed = {}
|
|
142
|
+
@indexes = []
|
|
143
|
+
index(*([DEFAULT_INDEX] | new_indexes))
|
|
96
144
|
end
|
|
97
145
|
|
|
98
146
|
private
|
|
99
147
|
|
|
100
148
|
def _find(search_criteria)
|
|
101
|
-
results = where(search_criteria)
|
|
149
|
+
results = where(**search_criteria)
|
|
102
150
|
yield if block_given? && results.count > 1
|
|
103
151
|
results.first
|
|
104
152
|
end
|
|
105
153
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
154
|
+
def new_registry_from_set(set)
|
|
155
|
+
Registry.new(set.to_a, indexes: indexes, thread_safe: @thread_safe)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def validate_index_exists!(idx)
|
|
159
|
+
return if index_exists?(idx)
|
|
160
|
+
|
|
161
|
+
raise IndexNotFound,
|
|
162
|
+
"Index '#{idx}' not found. Available indexes: #{indexes.inspect}. Add it with '.index(:#{idx})'"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def single_criteria_search(search_criteria)
|
|
166
|
+
idx, value = search_criteria.first
|
|
167
|
+
validate_index_exists!(idx)
|
|
168
|
+
lookup_index(idx, value)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def multi_criteria_search(search_criteria)
|
|
172
|
+
result_set = nil
|
|
173
|
+
search_criteria.each do |idx, value|
|
|
174
|
+
validate_index_exists!(idx)
|
|
175
|
+
current_set = lookup_index(idx, value)
|
|
176
|
+
result_set = result_set ? (result_set & current_set) : current_set
|
|
177
|
+
break if result_set.empty?
|
|
124
178
|
end
|
|
179
|
+
result_set || Set.new
|
|
125
180
|
end
|
|
126
181
|
|
|
127
|
-
def
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
182
|
+
def single_criteria_exists?(search_criteria)
|
|
183
|
+
idx, value = search_criteria.first
|
|
184
|
+
validate_index_exists!(idx)
|
|
185
|
+
lookup_index(idx, value).any?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def multi_criteria_exists?(search_criteria)
|
|
189
|
+
result_set = nil
|
|
190
|
+
search_criteria.each do |idx, value|
|
|
191
|
+
validate_index_exists!(idx)
|
|
192
|
+
current_set = lookup_index(idx, value)
|
|
193
|
+
return false if current_set.nil? || current_set.empty?
|
|
194
|
+
|
|
195
|
+
result_set = result_set ? (result_set & current_set) : current_set
|
|
196
|
+
return false if result_set.empty?
|
|
138
197
|
end
|
|
198
|
+
true
|
|
139
199
|
end
|
|
140
200
|
|
|
201
|
+
# Thread safety wrapper
|
|
202
|
+
def with_thread_safety(&block)
|
|
203
|
+
if @thread_safe
|
|
204
|
+
@mutex.synchronize(&block)
|
|
205
|
+
else
|
|
206
|
+
yield
|
|
207
|
+
end
|
|
208
|
+
end
|
|
141
209
|
end
|
metadata
CHANGED
|
@@ -1,72 +1,140 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: registry
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dale Stevens
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
13
|
+
name: benchmark-ips
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
16
|
- - "~>"
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '2.10'
|
|
20
19
|
type: :development
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - "~>"
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '2.10'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: bundler
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
30
29
|
requirements:
|
|
31
|
-
- - "
|
|
30
|
+
- - ">="
|
|
32
31
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
32
|
+
version: '0'
|
|
34
33
|
type: :development
|
|
35
34
|
prerelease: false
|
|
36
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
36
|
requirements:
|
|
38
|
-
- - "
|
|
37
|
+
- - ">="
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: pry-byebug
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
41
54
|
- !ruby/object:Gem::Dependency
|
|
42
55
|
name: rake
|
|
43
56
|
requirement: !ruby/object:Gem::Requirement
|
|
44
57
|
requirements:
|
|
45
58
|
- - "~>"
|
|
46
59
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
60
|
+
version: '13.0'
|
|
48
61
|
type: :development
|
|
49
62
|
prerelease: false
|
|
50
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
64
|
requirements:
|
|
52
65
|
- - "~>"
|
|
53
66
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '
|
|
67
|
+
version: '13.0'
|
|
55
68
|
- !ruby/object:Gem::Dependency
|
|
56
69
|
name: rspec
|
|
57
70
|
requirement: !ruby/object:Gem::Requirement
|
|
58
71
|
requirements:
|
|
59
72
|
- - "~>"
|
|
60
73
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '3.
|
|
74
|
+
version: '3.12'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.12'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rspec_junit_formatter
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.6'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.6'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rubocop
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '1.50'
|
|
62
103
|
type: :development
|
|
63
104
|
prerelease: false
|
|
64
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
106
|
requirements:
|
|
66
107
|
- - "~>"
|
|
67
108
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
69
|
-
|
|
109
|
+
version: '1.50'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: simplecov
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0.22'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0.22'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: simplecov-json
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0.2'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0.2'
|
|
70
138
|
email:
|
|
71
139
|
- dale@twilightcoders.net
|
|
72
140
|
executables: []
|
|
@@ -77,12 +145,16 @@ files:
|
|
|
77
145
|
- LICENSE
|
|
78
146
|
- README.md
|
|
79
147
|
- lib/registry.rb
|
|
148
|
+
- lib/registry/index_store.rb
|
|
149
|
+
- lib/registry/method_watcher.rb
|
|
150
|
+
- lib/registry/query_cache.rb
|
|
151
|
+
- lib/registry/version.rb
|
|
80
152
|
homepage: https://github.com/TwilightCoders/registry.
|
|
81
153
|
licenses:
|
|
82
154
|
- MIT
|
|
83
155
|
metadata:
|
|
84
156
|
allowed_push_host: https://rubygems.org
|
|
85
|
-
|
|
157
|
+
rubygems_mfa_required: 'true'
|
|
86
158
|
rdoc_options: []
|
|
87
159
|
require_paths:
|
|
88
160
|
- lib
|
|
@@ -91,15 +163,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
91
163
|
requirements:
|
|
92
164
|
- - ">="
|
|
93
165
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: '
|
|
166
|
+
version: '3.0'
|
|
95
167
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
168
|
requirements:
|
|
97
|
-
- - "
|
|
169
|
+
- - ">="
|
|
98
170
|
- !ruby/object:Gem::Version
|
|
99
|
-
version:
|
|
171
|
+
version: '0'
|
|
100
172
|
requirements: []
|
|
101
|
-
rubygems_version:
|
|
102
|
-
signing_key:
|
|
173
|
+
rubygems_version: 4.0.5
|
|
103
174
|
specification_version: 4
|
|
104
175
|
summary: Data structure for quick item lookup via indexes.
|
|
105
176
|
test_files: []
|