key-vortex 0.2.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.reek.yml +8 -4
- data/.rubocop.yml +0 -4
- data/.yardopts +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +8 -2
- data/Guardfile +6 -0
- data/README.md +38 -47
- data/lib/key_vortex/adapter/memory.rb +22 -2
- data/lib/key_vortex/adapter.rb +35 -0
- data/lib/key_vortex/constraint/base.rb +16 -1
- data/lib/key_vortex/constraint/length.rb +10 -1
- data/lib/key_vortex/constraint/maximum.rb +10 -1
- data/lib/key_vortex/constraint/minimum.rb +11 -2
- data/lib/key_vortex/constraint.rb +42 -5
- data/lib/key_vortex/field.rb +20 -1
- data/lib/key_vortex/limitation.rb +54 -26
- data/lib/key_vortex/record.rb +130 -35
- data/lib/key_vortex/version.rb +1 -1
- data/lib/key_vortex.rb +38 -5
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ccc7720fa67e489fa24c0158866cfde5be930f1b0f68ccc04b94b77e193b69f1
|
4
|
+
data.tar.gz: 350476269a57fed2af54c84ccd887d828f0804d287445b1133b67d2a4b66ce7c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 21a080f1f96c1d2aa6dde92071f3b5df4d73e96d11207a4cca025ad4c01be5f8f93fc4567da53087645c58f3d2d74afbede919e3ef45bc2cc63e3fe85a0341b0
|
7
|
+
data.tar.gz: 2c73b0c9eeae66681633c15d887c595c86aa468afed4dec676ca5527466908f33e4d084494da2dae682ed87cae82e2255ba82941d303fad57d504836225bea5a
|
data/.reek.yml
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
detectors:
|
2
|
-
# TODO: Remove once design is set
|
3
2
|
IrresponsibleModule:
|
4
|
-
|
3
|
+
exclude:
|
4
|
+
- AnotherRecordOne
|
5
|
+
- RecordOne
|
6
|
+
- RecordTwo
|
7
|
+
- BasicRecord
|
8
|
+
- BigStringRecord
|
5
9
|
|
6
10
|
DuplicateMethodCall:
|
7
11
|
exclude:
|
8
|
-
- KeyVortex::Record#
|
9
|
-
- KeyVortex::Record#
|
12
|
+
- KeyVortex::Record#define_getter
|
13
|
+
- KeyVortex::Record#define_setter
|
data/.rubocop.yml
CHANGED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
key-vortex (0.
|
4
|
+
key-vortex (1.0.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
@@ -32,8 +32,11 @@ GEM
|
|
32
32
|
guard-rubocop (1.5.0)
|
33
33
|
guard (~> 2.0)
|
34
34
|
rubocop (< 2.0)
|
35
|
+
guard-yard (2.2.1)
|
36
|
+
guard (>= 1.1.0)
|
37
|
+
yard (>= 0.7.0)
|
35
38
|
json (2.6.3)
|
36
|
-
key_vortex-contract (0.
|
39
|
+
key_vortex-contract (1.0.0)
|
37
40
|
rantly (~> 2.0.0)
|
38
41
|
rspec (~> 3.0)
|
39
42
|
kwalify (0.7.2)
|
@@ -97,6 +100,7 @@ GEM
|
|
97
100
|
shellany (0.0.1)
|
98
101
|
thor (1.2.2)
|
99
102
|
unicode-display_width (2.4.2)
|
103
|
+
webrick (1.8.1)
|
100
104
|
yard (0.9.34)
|
101
105
|
|
102
106
|
PLATFORMS
|
@@ -108,12 +112,14 @@ DEPENDENCIES
|
|
108
112
|
guard-reek
|
109
113
|
guard-rspec
|
110
114
|
guard-rubocop
|
115
|
+
guard-yard
|
111
116
|
key-vortex!
|
112
117
|
key_vortex-contract
|
113
118
|
rake (~> 13.0)
|
114
119
|
reek (~> 6.1.4)
|
115
120
|
rspec (~> 3.0)
|
116
121
|
rubocop (~> 1.21)
|
122
|
+
webrick
|
117
123
|
yard
|
118
124
|
|
119
125
|
BUNDLED WITH
|
data/Guardfile
CHANGED
@@ -26,6 +26,12 @@
|
|
26
26
|
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
27
27
|
# * 'just' rspec: 'rspec'
|
28
28
|
|
29
|
+
guard "yard" do
|
30
|
+
watch(%r{app/.+\.rb})
|
31
|
+
watch(%r{lib/.+\.rb})
|
32
|
+
watch(%r{ext/.+\.c})
|
33
|
+
end
|
34
|
+
|
29
35
|
group :red_green_refactor, halt_on_fail: true do
|
30
36
|
guard :rspec, cmd: "bundle exec rspec", failed_mode: :focus, all_on_pass: true do
|
31
37
|
require "guard/rspec/dsl"
|
data/README.md
CHANGED
@@ -2,67 +2,58 @@
|
|
2
2
|
|
3
3
|
KeyVortex provides a common abstraction around storing records in various datastores. It allows for the use of different adapters depending on the environment, and provides constraints to protect programmatically against differing constraints between them.
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
```ruby
|
8
|
-
require "key_vortex/record"
|
9
|
-
|
10
|
-
class ExampleRecord < KeyVortex::Record
|
11
|
-
field :a, String, length: 20
|
12
|
-
field :b, Integer, maximum: 100
|
13
|
-
end
|
14
|
-
```
|
5
|
+
## Installation
|
15
6
|
|
16
|
-
|
7
|
+
Add this line to your application's Gemfile:
|
17
8
|
|
18
|
-
|
19
|
-
> record = ExampleRecord.new(key: "foo", a: "bar", b: 10)
|
20
|
-
=> #<ExampleRecord:0x000055fe0b5fe538 @values={:key=>"foo", :a=>"bar", :b=>10}>
|
21
|
-
> record.a
|
22
|
-
=> "bar"
|
23
|
-
> record.a = "baz"
|
24
|
-
=> "baz"
|
25
|
-
> record.a
|
26
|
-
=> "baz"
|
27
|
-
> record.b = 1000
|
28
|
-
Invalid value 1000 for b (KeyVortex::Error)
|
29
|
-
```
|
9
|
+
gem 'key-vortex'
|
30
10
|
|
31
|
-
|
11
|
+
And then execute:
|
32
12
|
|
33
|
-
|
13
|
+
$ bundle install
|
34
14
|
|
35
|
-
|
36
|
-
> require "key_vortex/adapter/memory"
|
37
|
-
> vortex = KeyVortex.vortex(:memory, ExampleRecord)
|
38
|
-
> vortex.save(ExampleRecord.new(key: "foo", a: "a", b: 10))
|
39
|
-
> vortex.find("foo")
|
40
|
-
=> #<ExampleRecord:0x0000560781f480b0 @values={:key=>"foo", :a=>"a", :b=>10}>
|
41
|
-
```
|
15
|
+
Or install it yourself as:
|
42
16
|
|
43
|
-
|
17
|
+
$ gem install key-vortex
|
44
18
|
|
45
|
-
|
19
|
+
## Usage
|
46
20
|
|
47
|
-
|
21
|
+
To start using KeyVortex, you'll need to define a [record](https://rubydoc.info/gems/key-vortex/KeyVortex/Record/):
|
48
22
|
|
49
|
-
|
23
|
+
require "key_vortex/record"
|
50
24
|
|
51
|
-
|
52
|
-
|
53
|
-
|
25
|
+
class ExampleRecord < KeyVortex::Record
|
26
|
+
field :a, String, length: 20
|
27
|
+
field :b, Integer, maximum: 100
|
28
|
+
end
|
54
29
|
|
55
|
-
|
30
|
+
Now you can use this object in various ways:
|
56
31
|
|
57
|
-
|
32
|
+
> record = ExampleRecord.new(key: "foo", a: "bar", b: 10)
|
33
|
+
=> #<ExampleRecord:0x000055fe0b5fe538 @values={:key=>"foo", :a=>"bar", :b=>10}>
|
34
|
+
> record.a
|
35
|
+
=> "bar"
|
36
|
+
> record.a = "baz"
|
37
|
+
=> "baz"
|
38
|
+
> record.a
|
39
|
+
=> "baz"
|
40
|
+
> record.b = 1000
|
41
|
+
Invalid value 1000 for b (KeyVortex::Error)
|
58
42
|
|
59
|
-
|
43
|
+
You may notice that a `key` field was defined as well. This can be a String up to 36 characters long, to accomodate a GUID if that's what you wish to use.
|
60
44
|
|
61
|
-
|
45
|
+
In order to save the record somewhere, you'll need to choose an [adapter](https://rubydoc.info/gems/key-vortex/KeyVortex/Adapter).
|
62
46
|
|
63
|
-
|
47
|
+
> require "key_vortex/adapter/memory"
|
48
|
+
> vortex = KeyVortex.vortex(:memory, ExampleRecord)
|
49
|
+
> vortex.save(ExampleRecord.new(key: "foo", a: "a", b: 10))
|
50
|
+
> vortex.find("foo")
|
51
|
+
=> #<ExampleRecord:0x0000560781f480b0 @values={:key=>"foo", :a=>"a", :b=>10}>
|
52
|
+
> vortex.remove("foo")
|
53
|
+
> vortex.find("foo")
|
54
|
+
=> nil
|
64
55
|
|
65
|
-
|
56
|
+
As you can see, you have the ability to `save`, `find` and `remove` records. Once a record is saved, it can be referenced by the `key`.
|
66
57
|
|
67
58
|
## Development
|
68
59
|
|
@@ -72,7 +63,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
72
63
|
|
73
64
|
## Contributing
|
74
65
|
|
75
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
66
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Lambda-Null/key-vortex. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/Lambda-Null/key-vortex/blob/main/CODE_OF_CONDUCT.md).
|
76
67
|
|
77
68
|
## License
|
78
69
|
|
@@ -80,4 +71,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
80
71
|
|
81
72
|
## Code of Conduct
|
82
73
|
|
83
|
-
Everyone interacting in the KeyVortex project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
74
|
+
Everyone interacting in the KeyVortex project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Lambda-Null/key-vortex/blob/main/CODE_OF_CONDUCT.md).
|
@@ -5,25 +5,45 @@ require "key_vortex/adapter"
|
|
5
5
|
|
6
6
|
class KeyVortex
|
7
7
|
class Adapter
|
8
|
+
# An in-memory implementation of an adapter. While typically not
|
9
|
+
# useful in production, it can provide a convenient implementation
|
10
|
+
# for unit testing.
|
8
11
|
class Memory < KeyVortex::Adapter
|
12
|
+
# A pass-through to the constructor which is used by
|
13
|
+
# {KeyVortex.vortex}.
|
14
|
+
# @param items [Hash]
|
15
|
+
# @param limitations [Array of KeyVortex::Limitation]
|
16
|
+
# @return [KeyVortex::Adapter::Memory]
|
9
17
|
def self.build(items: {}, limitations: [])
|
10
18
|
new(items, limitations: limitations)
|
11
19
|
end
|
12
20
|
|
21
|
+
# Create a new instance of the in-memory adapter. By default, it
|
22
|
+
# has no limitations, but since it's most likely testing an
|
23
|
+
# adapter which is more restrictive, it accepts a list of
|
24
|
+
# limitations to apply.
|
25
|
+
# @param items [Hash] The seed values that should be considered already saved.
|
26
|
+
# @param limitations [Array of KeyVortex::Limitation]
|
13
27
|
def initialize(items, limitations: [])
|
14
28
|
super()
|
15
29
|
@items = items
|
16
30
|
limitations.each { |limit| register_limitation(limit) }
|
17
31
|
end
|
18
32
|
|
33
|
+
# Save the record to the provided hash using {Record#key} as the
|
34
|
+
# key.
|
35
|
+
# @param record [Record]
|
19
36
|
def save(record)
|
20
37
|
@items[record.key] = record
|
21
38
|
end
|
22
39
|
|
23
|
-
|
24
|
-
|
40
|
+
# @param key [String]
|
41
|
+
# @return [Record, nil] The record with {Record#key} set to key
|
42
|
+
def find(key)
|
43
|
+
@items[key]
|
25
44
|
end
|
26
45
|
|
46
|
+
# Remove the record with {Record#key} set to key
|
27
47
|
def remove(key)
|
28
48
|
@items.delete(key)
|
29
49
|
end
|
data/lib/key_vortex/adapter.rb
CHANGED
@@ -1,19 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class KeyVortex
|
4
|
+
# Adapters bridge the gap between the main logic of KeyVortex and
|
5
|
+
# individual backends. When using a backend, it can have various
|
6
|
+
# limitations on particular data types. KeyVortex will guarantee that
|
7
|
+
# these limitations are more permissive then the Records used to
|
8
|
+
# interact with it.
|
9
|
+
#
|
10
|
+
# To keep dependencies down, the subclasses you'll actually use will
|
11
|
+
# be implemented in other gems, but an in memory adapter does ship
|
12
|
+
# with this gem. Here are some that are available:
|
13
|
+
#
|
14
|
+
# * {https://github.com/Lambda-Null/key_vortex-stashify Stashify}: File/blob storage
|
15
|
+
#
|
16
|
+
# == Building Adapters
|
17
|
+
#
|
18
|
+
# For an adapter to work with KeyVortex, it needs to inherit from
|
19
|
+
# this class. In addition, it needs to override the following
|
20
|
+
# methods:
|
21
|
+
#
|
22
|
+
# * save({KeyVortex::Record}) => Anything
|
23
|
+
# * find(String) => {KeyVortex::Record}
|
24
|
+
# * remove(String) => Anything
|
25
|
+
#
|
26
|
+
# There is {https://github.com/Lambda-Null/key_vortex-contract a separate gem},
|
27
|
+
# which provides a set of shared examples enforcing the correct
|
28
|
+
# behavior of those methods. PRs to include your gem in the list in
|
29
|
+
# the previous section will be accepted if they conform to that
|
30
|
+
# contract.
|
4
31
|
class Adapter
|
5
32
|
def initialize
|
6
33
|
@limitations = {}
|
7
34
|
end
|
8
35
|
|
36
|
+
# Get the limitation for the type of a given field.
|
37
|
+
# @param field [KeyVortex::Field]
|
38
|
+
# @return [KeyVortex::Limitation]
|
9
39
|
def limitation_for(field)
|
10
40
|
@limitations[field.limitation.type]
|
11
41
|
end
|
12
42
|
|
43
|
+
# Apply the provided limitation to the adapter.
|
44
|
+
# @param limitation [KeyVortex::Limitation]
|
13
45
|
def register_limitation(limitation)
|
14
46
|
@limitations[limitation.type] = limitation
|
15
47
|
end
|
16
48
|
|
49
|
+
# Provide the symbol used when using the factory method
|
50
|
+
# {KeyVortex.vortex}.
|
51
|
+
# @return [Symbol] A symbol representing this class
|
17
52
|
def self.symbol
|
18
53
|
name.split(":").last.downcase.to_sym
|
19
54
|
end
|
@@ -1,16 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class KeyVortex
|
4
|
-
|
4
|
+
module Constraint
|
5
|
+
# Base class all other constraints inherit from. Does not define
|
6
|
+
# all of the properties necessary for a constraint, and so is
|
7
|
+
# inappropriate to be used directly.
|
5
8
|
class Base
|
9
|
+
# Comparing constraints is only valid when compared to other
|
10
|
+
# instances of the same constraint. This helps other parts of
|
11
|
+
# the system determine if this is true.
|
12
|
+
# @param constraint [Base]
|
13
|
+
# @return [Boolean]
|
6
14
|
def applies_to?(constraint)
|
7
15
|
attribute == constraint.attribute
|
8
16
|
end
|
9
17
|
|
18
|
+
# The individual constraint is responsible for making the
|
19
|
+
# ultimate determination of if it is within another
|
20
|
+
# constraint. What's common to all of them, though, is that they
|
21
|
+
# must be the same class.
|
10
22
|
def within?(constraint)
|
11
23
|
constraint.instance_of?(self.class)
|
12
24
|
end
|
13
25
|
|
26
|
+
# A text description of the constraint. Assumes subclasses
|
27
|
+
# define the methods attribute and limit.
|
28
|
+
# @return [String]
|
14
29
|
def to_s
|
15
30
|
"#{attribute}: #{limit}"
|
16
31
|
end
|
@@ -3,23 +3,32 @@
|
|
3
3
|
require "key_vortex/constraint/base"
|
4
4
|
|
5
5
|
class KeyVortex
|
6
|
-
|
6
|
+
module Constraint
|
7
|
+
# Enforces that objects which respond to #length less than the
|
8
|
+
# specified limit.
|
7
9
|
class Length < KeyVortex::Constraint::Base
|
10
|
+
# @return [Integer] The upper bound allowed when calling #length on a value
|
8
11
|
attr_reader :limit
|
9
12
|
|
13
|
+
# @param limit [Integer] The maximum allowed value
|
10
14
|
def initialize(limit)
|
11
15
|
super()
|
12
16
|
@limit = limit
|
13
17
|
end
|
14
18
|
|
19
|
+
# @return [Symbol] :length
|
15
20
|
def attribute
|
16
21
|
:length
|
17
22
|
end
|
18
23
|
|
24
|
+
# @param constraint [Length]
|
25
|
+
# @return [Boolean] True if limit <= constraint.limit
|
19
26
|
def within?(constraint)
|
20
27
|
super && limit <= constraint.limit
|
21
28
|
end
|
22
29
|
|
30
|
+
# @param value [Object] Must respond to #length
|
31
|
+
# @return [Boolean] True if length <= limit
|
23
32
|
def accepts?(value)
|
24
33
|
value.length <= limit
|
25
34
|
end
|
@@ -3,23 +3,32 @@
|
|
3
3
|
require "key_vortex/constraint/base"
|
4
4
|
|
5
5
|
class KeyVortex
|
6
|
-
|
6
|
+
module Constraint
|
7
|
+
# Enforces that objects which respond to #<= are less than the
|
8
|
+
# defined limit.
|
7
9
|
class Maximum < KeyVortex::Constraint::Base
|
10
|
+
# @return [Integer] The maximum allowed value
|
8
11
|
attr_reader :limit
|
9
12
|
|
13
|
+
# @param limit [Integer] The maximum allowed value
|
10
14
|
def initialize(limit)
|
11
15
|
super()
|
12
16
|
@limit = limit
|
13
17
|
end
|
14
18
|
|
19
|
+
# @return [Symbol] :maximum
|
15
20
|
def attribute
|
16
21
|
:maximum
|
17
22
|
end
|
18
23
|
|
24
|
+
# @param constraint [Maximum]
|
25
|
+
# @return [Boolean] True if limit <= constraint.limit
|
19
26
|
def within?(constraint)
|
20
27
|
super && limit <= constraint.limit
|
21
28
|
end
|
22
29
|
|
30
|
+
# @param value [Object] Must respond to #<=
|
31
|
+
# @return [Boolean] True if value <= limit
|
23
32
|
def accepts?(value)
|
24
33
|
value <= limit
|
25
34
|
end
|
@@ -3,23 +3,32 @@
|
|
3
3
|
require "key_vortex/constraint/base"
|
4
4
|
|
5
5
|
class KeyVortex
|
6
|
-
|
6
|
+
module Constraint
|
7
|
+
# Enforces that objects which respond to #>= are greater than the
|
8
|
+
# defined limit.
|
7
9
|
class Minimum < KeyVortex::Constraint::Base
|
10
|
+
# @return [Integer] The minimum allowed value
|
8
11
|
attr_reader :limit
|
9
12
|
|
13
|
+
# @param limit [Integer] The minimum allowed value
|
10
14
|
def initialize(limit)
|
11
15
|
super()
|
12
16
|
@limit = limit
|
13
17
|
end
|
14
18
|
|
19
|
+
# @return [Symbol] :minimum
|
15
20
|
def attribute
|
16
|
-
:
|
21
|
+
:minimum
|
17
22
|
end
|
18
23
|
|
24
|
+
# @param constraint [Minimum]
|
25
|
+
# @return [Boolean] True if limit >= constraint.limit
|
19
26
|
def within?(constraint)
|
20
27
|
super && limit >= constraint.limit
|
21
28
|
end
|
22
29
|
|
30
|
+
# @param value [Object] Must respond to #>=
|
31
|
+
# @return [Boolean] True if value >= limit
|
23
32
|
def accepts?(value)
|
24
33
|
value >= limit
|
25
34
|
end
|
@@ -6,18 +6,55 @@ require "key_vortex/constraint/maximum"
|
|
6
6
|
require "key_vortex/constraint/minimum"
|
7
7
|
|
8
8
|
class KeyVortex
|
9
|
-
|
10
|
-
|
9
|
+
# Constraints define a restriction on the values which can be used
|
10
|
+
# for {KeyVortex::Record} and {KeyVortex::Adapter}. Although they
|
11
|
+
# are somewhat varied, they do have some common properties.
|
12
|
+
#
|
13
|
+
# All constraints have some sort of attribute name and limit. In
|
14
|
+
# most situations, instead of the constructor, this pair will be
|
15
|
+
# used to specify the constraint. For example, you would typically
|
16
|
+
# specify a length like this:
|
17
|
+
#
|
18
|
+
# length: 10
|
19
|
+
#
|
20
|
+
# All constraints will inherit from
|
21
|
+
# {KeyVortex::Constraint::Base}. Because of the common ancestor,
|
22
|
+
# along with the need to have a single file loading all constraints,
|
23
|
+
# {KeyVortex::Constraint} is a module instead of the base class to
|
24
|
+
# avoid a circular dependency.
|
25
|
+
#
|
26
|
+
# All constraints can determine if a value is valid for them.
|
27
|
+
#
|
28
|
+
# All constraints, when compared with other instances of themselves,
|
29
|
+
# it can always be determined if ones limitations fit within the
|
30
|
+
# others. More formally, given an instance x of a constraint, an
|
31
|
+
# instance y of the same constraint fits within x if and only if for
|
32
|
+
# all values v that are valid for y, v is also valid for x.
|
33
|
+
module Constraint
|
34
|
+
# Factory enabling the specification of constraints by attribute
|
35
|
+
# names instead of constructors.
|
36
|
+
# @param attribute [Symbol]
|
37
|
+
# @param limit [Object]
|
38
|
+
# @return [KeyVortex::Constraint::Base] Subclass specified by attribute
|
39
|
+
# @raise [KeyVortex::Error] If the attribute is unknown
|
40
|
+
def self.build(attribute, limit)
|
11
41
|
case attribute
|
12
42
|
when :length
|
13
|
-
KeyVortex::Constraint::Length.new(
|
43
|
+
KeyVortex::Constraint::Length.new(limit)
|
14
44
|
when :maximum
|
15
|
-
KeyVortex::Constraint::Maximum.new(
|
45
|
+
KeyVortex::Constraint::Maximum.new(limit)
|
16
46
|
when :minimum
|
17
|
-
KeyVortex::Constraint::Minimum.new(
|
47
|
+
KeyVortex::Constraint::Minimum.new(limit)
|
18
48
|
else
|
19
49
|
raise KeyVortex::Error, "Unexpected attribute: #{attribute}"
|
20
50
|
end
|
21
51
|
end
|
22
52
|
end
|
23
53
|
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Adapters can also have constraints, though, and if a particular
|
57
|
+
# type is more constrained then the record KeyVortex will refuse to
|
58
|
+
# use that record. If you don't specify any constraints, for
|
59
|
+
# example, the adapter will throw an error unless it is also
|
60
|
+
# unconstrained on that type.
|
data/lib/key_vortex/field.rb
CHANGED
@@ -4,9 +4,23 @@ require "key_vortex/constraint"
|
|
4
4
|
require "key_vortex/limitation"
|
5
5
|
|
6
6
|
class KeyVortex
|
7
|
+
# Defines a value that can be set on a {Record}. This generally
|
8
|
+
# isn't built directly, you should use the {Record#field} directive
|
9
|
+
# most of the time. This class ties a name to a {Limitation}, and
|
10
|
+
# delegates most questions on to the latter.
|
7
11
|
class Field
|
8
|
-
|
12
|
+
# @return [Symbol] the name of the field
|
13
|
+
attr_reader :name
|
14
|
+
# @return [Limitation] the restrictions placed upon this field
|
15
|
+
attr_reader :limitation
|
9
16
|
|
17
|
+
# Creates an instance of this class and converts any
|
18
|
+
# attribute/limit constraint pairs into {KeyVortex::Constraint}
|
19
|
+
# objects.
|
20
|
+
# @param name [Symbol] Name of the field
|
21
|
+
# @param type [Class] Type used for limitation
|
22
|
+
# @param constraints_array [Constraint::Base] Any constraints already constructed can be passed in as well
|
23
|
+
# @param constraints_hash [Hash] key/value pairs that will be passed on to {Constraint#build}
|
10
24
|
def initialize(name, type, *constraints_array, **constraints_hash)
|
11
25
|
@name = name
|
12
26
|
@limitation = KeyVortex::Limitation.new(type)
|
@@ -17,15 +31,20 @@ class KeyVortex
|
|
17
31
|
end)
|
18
32
|
end
|
19
33
|
|
34
|
+
# @param adapter [Adapter]
|
35
|
+
# @return [Boolean] true if the limitations for this field fit within the corresponding limitations for the adapter
|
20
36
|
def within?(adapter)
|
21
37
|
limitation = adapter.limitation_for(self)
|
22
38
|
!limitation || self.limitation.within?(limitation)
|
23
39
|
end
|
24
40
|
|
41
|
+
# @param value [Object]
|
42
|
+
# @return [Boolean] true if the value is valid for {#limitation}
|
25
43
|
def accepts?(value)
|
26
44
|
limitation.accepts?(value)
|
27
45
|
end
|
28
46
|
|
47
|
+
# Enable JSON additions for the class used for this field.
|
29
48
|
def enable_json_additions
|
30
49
|
@limitation.enable_json_additions
|
31
50
|
end
|
@@ -1,14 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class KeyVortex
|
4
|
+
# Represents a collection of {KeyVortex::Constraint constraints}
|
5
|
+
# which apply to a given class. While these can be constructed
|
6
|
+
# directly, the intended mechanism for applying these to a record is
|
7
|
+
# through {KeyVortex::Record.field}.
|
8
|
+
#
|
9
|
+
# Two independent concepts which commonly use the same terminology
|
10
|
+
# exist in this project. Consider the term "allows", is that
|
11
|
+
# referring to what values are allowed or what constraints are
|
12
|
+
# allowed? To help navigate these two concepts, the following
|
13
|
+
# terminology convention is used throughout this codebase:
|
14
|
+
# * Encompass/Within: Used when comparing limitations and constraints to each other
|
15
|
+
# * Accepts/Rejects: Used when determining if a value is valid for constraints
|
4
16
|
class Limitation
|
5
|
-
|
17
|
+
# @return [Class] The class these constraints applies to.
|
18
|
+
attr_reader :type
|
6
19
|
|
20
|
+
# @return [Array] The constraints which apply to this class.
|
21
|
+
attr_reader :constraints
|
22
|
+
|
23
|
+
# @param type [Class]
|
24
|
+
# @param constraints [KeyValue::Constraint]
|
7
25
|
def initialize(type, *constraints)
|
8
26
|
@type = type
|
9
|
-
@constraints =
|
27
|
+
@constraints = []
|
28
|
+
add_constraint(*constraints)
|
10
29
|
end
|
11
30
|
|
31
|
+
# Add constraints to this limitation. It is also possible to do
|
32
|
+
# this through the constructor, but it is sometimes easier to do
|
33
|
+
# it one at a time.
|
34
|
+
# @param constraints [KeyVortex::Constraint::Base]
|
12
35
|
def add_constraint(*constraints)
|
13
36
|
constraints.each do |constraint|
|
14
37
|
unless constraint.is_a?(KeyVortex::Constraint::Base)
|
@@ -20,24 +43,42 @@ class KeyVortex
|
|
20
43
|
@constraints += constraints
|
21
44
|
end
|
22
45
|
|
23
|
-
|
24
|
-
|
25
|
-
|
46
|
+
# Determine if any of the constraints in the provided limitation
|
47
|
+
# exceed this limitation's constraints.
|
48
|
+
# @param limitation [KeyVortex::Limitation]
|
49
|
+
# @return [Boolean]
|
50
|
+
def within?(limitation)
|
51
|
+
limitation.constraints.all? do |constraint|
|
52
|
+
within_constraint?(constraint)
|
26
53
|
end
|
27
54
|
end
|
28
55
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
56
|
+
# Determine if a given value meets all of the constraints.
|
57
|
+
# @param value [type]
|
58
|
+
# @return [Boolean]
|
59
|
+
def accepts?(value)
|
60
|
+
value.is_a?(type) && @constraints.all? { |constraint| constraint.accepts?(value) }
|
33
61
|
end
|
34
62
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
63
|
+
# Ensure that JSON additions for {type} are loaded in case the
|
64
|
+
# record needs to be serialized.
|
65
|
+
def enable_json_additions
|
66
|
+
path = JSON_ADDITIONS[@type.class.name]
|
67
|
+
require path if path
|
68
|
+
end
|
69
|
+
|
70
|
+
# Return information about this limitation. The information will
|
71
|
+
# be formatted as follows:
|
72
|
+
# Limitation: String
|
73
|
+
# minimum: 10
|
74
|
+
# maximum: 100
|
75
|
+
# @return [String]
|
76
|
+
def to_s
|
77
|
+
"Limitation: #{@type}\n\t#{@constraints.join('\n\t')}"
|
39
78
|
end
|
40
79
|
|
80
|
+
private
|
81
|
+
|
41
82
|
def within_constraint?(constraint)
|
42
83
|
applicable_constraints(constraint).any? do |con|
|
43
84
|
con.within?(constraint)
|
@@ -50,10 +91,6 @@ class KeyVortex
|
|
50
91
|
end
|
51
92
|
end
|
52
93
|
|
53
|
-
def accepts?(value)
|
54
|
-
value.is_a?(type) && @constraints.all? { |constraint| constraint.accepts?(value) }
|
55
|
-
end
|
56
|
-
|
57
94
|
JSON_ADDITIONS = {
|
58
95
|
BigDecimal: "json/add/bigdecimal",
|
59
96
|
Complex: "json/add/complex",
|
@@ -69,14 +106,5 @@ class KeyVortex
|
|
69
106
|
Symbol: "json/add/symbol",
|
70
107
|
Time: "json/add/time"
|
71
108
|
}.freeze
|
72
|
-
|
73
|
-
def enable_json_additions
|
74
|
-
path = JSON_ADDITIONS[@type.class.name]
|
75
|
-
require path if path
|
76
|
-
end
|
77
|
-
|
78
|
-
def to_s
|
79
|
-
"Limitation: #{@type}\n\t#{@constraints.join('\n\t')}"
|
80
|
-
end
|
81
109
|
end
|
82
110
|
end
|
data/lib/key_vortex/record.rb
CHANGED
@@ -6,54 +6,135 @@ require "key_vortex/field"
|
|
6
6
|
require "key_vortex/limitation"
|
7
7
|
|
8
8
|
class KeyVortex
|
9
|
+
# To represent a structured piece of information you want to save,
|
10
|
+
# it has to be a subclass of KeyVortex::Record. There will always be
|
11
|
+
# field named {#key}, which is at most 36 characters long, but keys
|
12
|
+
# with any other name can also be added.
|
13
|
+
#
|
14
|
+
# class ExampleRecord < KeyVortex::Record
|
15
|
+
# field :a, Integer, minimum: 10, maximum: 100
|
16
|
+
# field :b, String, length: 100
|
17
|
+
# field :c, Integer
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# See {field} for more details on how those directives are
|
21
|
+
# structured. When you're ready to use the class, values can be
|
22
|
+
# provided either in the constructor like this:
|
23
|
+
#
|
24
|
+
# ExampleRecord.new(
|
25
|
+
# a: 15,
|
26
|
+
# b: "foo",
|
27
|
+
# c: 1000,
|
28
|
+
# )
|
29
|
+
#
|
30
|
+
# Or set after the object is created like this:
|
31
|
+
#
|
32
|
+
# r = ExampleRecord.new
|
33
|
+
# r.a = 15
|
34
|
+
# r.b = "foo"
|
35
|
+
# r.c = 1000
|
36
|
+
#
|
37
|
+
# In either case, the values can be retrieved as attributes after
|
38
|
+
# they're set:
|
39
|
+
#
|
40
|
+
# r.a # 15
|
41
|
+
# r.b # "foo"
|
42
|
+
# r.c # 1000
|
9
43
|
class Record
|
10
|
-
|
11
|
-
|
12
|
-
|
44
|
+
class << self
|
45
|
+
# @return [Array] All of the fields declared for this class.
|
46
|
+
def fields
|
47
|
+
field_hash.values
|
48
|
+
end
|
13
49
|
|
14
|
-
|
15
|
-
|
16
|
-
|
50
|
+
# Declares what values are tracked by this record.
|
51
|
+
# When declaring a field, it follows this general format:
|
52
|
+
#
|
53
|
+
# field :field_name, ClassName, constraint1: contraint1_value, constraint2: constraint2_value
|
54
|
+
#
|
55
|
+
# From a record's perspective any number of
|
56
|
+
# {KeyVortex::Constraint constraints} are valid, including none
|
57
|
+
# at all.
|
58
|
+
# @return [Field] The created field
|
59
|
+
def field(name, type, **constraints)
|
60
|
+
field = KeyVortex::Field.new(name, type, **constraints)
|
61
|
+
register_field(field)
|
62
|
+
field
|
63
|
+
end
|
17
64
|
|
18
|
-
|
19
|
-
|
20
|
-
|
65
|
+
# Convert a json parse result into a record for this class. This
|
66
|
+
# is what's called when this command is used:
|
67
|
+
#
|
68
|
+
# JSON.parse(json, create_additions: true)
|
69
|
+
#
|
70
|
+
# See {#to_json} for an example of the JSON structure this
|
71
|
+
# example expects.
|
72
|
+
# @return [Record] Subclasses will return an instance of themselves
|
73
|
+
def json_create(object)
|
74
|
+
new(object["values"])
|
75
|
+
end
|
21
76
|
|
22
|
-
|
23
|
-
@field_hash[field]
|
24
|
-
end
|
77
|
+
protected
|
25
78
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
79
|
+
def register_field(field)
|
80
|
+
field_hash[field.name] = field
|
81
|
+
field.enable_json_additions
|
82
|
+
define_getter(field)
|
83
|
+
define_setter(field)
|
84
|
+
end
|
32
85
|
|
33
|
-
|
34
|
-
define_method(field.name) { @values[field.name] }
|
35
|
-
end
|
86
|
+
private
|
36
87
|
|
37
|
-
|
38
|
-
|
39
|
-
|
88
|
+
def field_hash
|
89
|
+
@field_hash ||= {}
|
90
|
+
end
|
40
91
|
|
41
|
-
|
92
|
+
def define_getter(field)
|
93
|
+
define_method(field.name) { @values[field.name] }
|
42
94
|
end
|
43
|
-
end
|
44
95
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
96
|
+
def define_setter(field)
|
97
|
+
define_method("#{field.name}=") do |val|
|
98
|
+
raise KeyVortex::Error, "Invalid value #{val} for #{field.name}" unless field.accepts?(val)
|
99
|
+
|
100
|
+
@values[field.name] = val
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def inherited(subclass)
|
105
|
+
super
|
106
|
+
fields.each do |field|
|
107
|
+
subclass.register_field(field)
|
108
|
+
end
|
49
109
|
end
|
50
110
|
end
|
51
111
|
|
52
|
-
#
|
112
|
+
# @!attribute key
|
113
|
+
# The key used to save and retrieve this record. Every field has
|
114
|
+
# a key defined, it can be a String up to 36 characters long,
|
115
|
+
# which is enough to accomodate a GUID if that's what you want
|
116
|
+
# to use.
|
117
|
+
# @return [String]
|
53
118
|
field :key, String, length: 36
|
54
119
|
|
120
|
+
# A hash of all of the attributes, in which the keys are symbols.
|
121
|
+
# @example
|
122
|
+
# {
|
123
|
+
# a: 15,
|
124
|
+
# b: "foo",
|
125
|
+
# c: 1000,
|
126
|
+
# }
|
127
|
+
#
|
128
|
+
# @return [Hash]
|
55
129
|
attr_reader :values
|
56
130
|
|
131
|
+
# Values can optionally be specified when the object is created.
|
132
|
+
# @example
|
133
|
+
# ExampleRecord.new(
|
134
|
+
# a: 15,
|
135
|
+
# b: "foo",
|
136
|
+
# c: 1000,
|
137
|
+
# )
|
57
138
|
def initialize(values = {})
|
58
139
|
@values = {}
|
59
140
|
values.each do |name, value|
|
@@ -61,19 +142,33 @@ class KeyVortex
|
|
61
142
|
end
|
62
143
|
end
|
63
144
|
|
145
|
+
# Two records are equal if they are the same class and have the
|
146
|
+
# same values.
|
147
|
+
# @return [Boolean]
|
64
148
|
def ==(other)
|
65
149
|
self.class == other.class && values == other.values
|
66
150
|
end
|
67
151
|
|
152
|
+
# Converts the record into a JSON string appropriate for parsing
|
153
|
+
# using the create_additions option. Here is an example of the
|
154
|
+
# format that's produced.
|
155
|
+
#
|
156
|
+
# {
|
157
|
+
# "json_class": "ExampleRecord",
|
158
|
+
# "values": {
|
159
|
+
# "key": "foo",
|
160
|
+
# "a": 15,
|
161
|
+
# "b": "bar",
|
162
|
+
# "c": 1000
|
163
|
+
# }
|
164
|
+
# }
|
165
|
+
#
|
166
|
+
# @return [String] JSON representation of the object
|
68
167
|
def to_json(*args)
|
69
168
|
{
|
70
169
|
JSON.create_id => self.class.name,
|
71
170
|
"values" => @values
|
72
171
|
}.to_json(*args)
|
73
172
|
end
|
74
|
-
|
75
|
-
def self.json_create(object)
|
76
|
-
new(object["values"])
|
77
|
-
end
|
78
173
|
end
|
79
174
|
end
|
data/lib/key_vortex/version.rb
CHANGED
data/lib/key_vortex.rb
CHANGED
@@ -2,14 +2,29 @@
|
|
2
2
|
|
3
3
|
require_relative "key_vortex/version"
|
4
4
|
|
5
|
+
# Defines the API you'll interact with when using this gem. Much of
|
6
|
+
# the functionality is delegated to other classes.
|
5
7
|
class KeyVortex
|
8
|
+
# General purpose error used when issues arise within this gem.
|
6
9
|
class Error < StandardError; end
|
7
10
|
|
11
|
+
# Register an adapter class so that {.vortex} knows about it.
|
12
|
+
# @param adapter_class [Class]
|
8
13
|
def self.register(adapter_class)
|
9
14
|
@adapter_registry ||= {}
|
10
15
|
@adapter_registry[adapter_class.symbol] = adapter_class
|
11
16
|
end
|
12
17
|
|
18
|
+
# Creates an instance of {KeyVortex} with an appropriate
|
19
|
+
# {Adapter}. An adapter class must have been associated with the
|
20
|
+
# adapter_symbol by calling {.register}, this should have been done
|
21
|
+
# by the file which defined that class. So, when you require
|
22
|
+
# "key_vortex/adapter/memory" the :memory symbol will be defined for
|
23
|
+
# use here.
|
24
|
+
# @param adapter_symbol [Symbol]
|
25
|
+
# @param record_class [Class] The class must inherit from {Record}
|
26
|
+
# @param options [Hash] Options that will be passed through to .build on the adapter
|
27
|
+
# @return [KeyVortex]
|
13
28
|
def self.vortex(adapter_symbol, record_class, **options)
|
14
29
|
new(
|
15
30
|
@adapter_registry[adapter_symbol].build(**options),
|
@@ -17,8 +32,16 @@ class KeyVortex
|
|
17
32
|
)
|
18
33
|
end
|
19
34
|
|
20
|
-
|
35
|
+
# @return [Adapter]
|
36
|
+
attr_reader :adapter
|
37
|
+
# @return [Class] Subclass of {Record}
|
38
|
+
attr_reader :record_class
|
21
39
|
|
40
|
+
# While you are able to create the object directly, it's easier to
|
41
|
+
# do so through {.vortex}.
|
42
|
+
# @param adapter [Adapter] The backend that will be used to save and retrieve records
|
43
|
+
# @param record_class [Class] The subclass of {Record} which will be saved and retreived
|
44
|
+
# @raise [Error] If the constraints of the record_class do not fit within the constraints of the adapter
|
22
45
|
def initialize(adapter, record_class)
|
23
46
|
@adapter = adapter
|
24
47
|
@record_class = record_class
|
@@ -33,15 +56,25 @@ class KeyVortex
|
|
33
56
|
end
|
34
57
|
end
|
35
58
|
|
59
|
+
# Add the record to the {#adapter}. Once this is done, {#find} will
|
60
|
+
# return this record when passed the {Record#key}.
|
61
|
+
# @param record [Record] Must match {#record_class}
|
36
62
|
def save(record)
|
37
63
|
@adapter.save(record)
|
38
64
|
end
|
39
65
|
|
40
|
-
|
41
|
-
|
66
|
+
# Retrieve a record that has had {#save} called on it.
|
67
|
+
# @param key [String]
|
68
|
+
# @return [Record] The {Record} with {Record#key} set to key
|
69
|
+
# @return [nil] if no record is found
|
70
|
+
def find(key)
|
71
|
+
@adapter.find(key)
|
42
72
|
end
|
43
73
|
|
44
|
-
|
45
|
-
|
74
|
+
# Remove the {Record} with {Record#key} set to key from the
|
75
|
+
# {#adapter}.
|
76
|
+
# @param key [String]
|
77
|
+
def remove(key)
|
78
|
+
@adapter.remove(key)
|
46
79
|
end
|
47
80
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: key-vortex
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lambda Null
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-07-
|
11
|
+
date: 2023-07-16 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Defines abstractions that can be built on top of for key/value storage
|
14
14
|
on different technologies (file, s3, sql, redis, etc.)
|
@@ -21,6 +21,7 @@ files:
|
|
21
21
|
- ".reek.yml"
|
22
22
|
- ".rspec"
|
23
23
|
- ".rubocop.yml"
|
24
|
+
- ".yardopts"
|
24
25
|
- CODE_OF_CONDUCT.md
|
25
26
|
- Gemfile
|
26
27
|
- Gemfile.lock
|