teton 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4ae26f8067acdb56f89e1df1754b9e91e89ff3d96c6f6b30c3d46e3c0f7651f3
4
+ data.tar.gz: d6858d2075ec88c423bb463406f8adeef82b51ab796f76da3aeb021c60993a93
5
+ SHA512:
6
+ metadata.gz: 2d3fddc1585ea06a7323b7d146942db50c0cdfd224aa5dd6faa088b4735d0fc1b84ff3db9e7a90fb202f0b09b9033946bed31afabfabc995f9ac504321117f17
7
+ data.tar.gz: 8d5e24cb57f502f37db40e90adcf439100c5c9547fe9296344c088b28cd0eb2b59c308a388e5c0cccff9530d42c170e3925dbadd26d066fb67082df6ddb5037e
data/.editorconfig ADDED
@@ -0,0 +1,8 @@
1
+ # See http://editorconfig.org/
2
+
3
+ [*]
4
+ trim_trailing_whitespace = true
5
+ indent_style = space
6
+ indent_size = 2
7
+ insert_final_newline = true
8
+ end_of_line = lf
@@ -0,0 +1,31 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ verify:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby-version: ['2.7', '3.0']
15
+
16
+ steps:
17
+ - uses: actions/checkout@v2
18
+ - name: Set up Ruby
19
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
20
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
21
+ # uses: ruby/setup-ruby@v1
22
+ uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: ${{ matrix.ruby-version }}
25
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
26
+ - name: Lint
27
+ run: bin/rubocop
28
+ - name: Test
29
+ run: bin/rspec spec --format documentation
30
+ - name: Dependency Audit
31
+ run: bin/bundler-audit check --update
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ .DS_Store
2
+ *.gem
3
+ /tmp
4
+ /coverage
5
+ Gemfile.lock
6
+ /pkg
data/.rubocop.yml ADDED
@@ -0,0 +1,23 @@
1
+ require:
2
+ - rubocop-rake
3
+ - rubocop-rspec
4
+
5
+ AllCops:
6
+ NewCops: enable
7
+ TargetRubyVersion: 2.7
8
+ Exclude:
9
+ - bin/*
10
+ - vendor/bundle/**/*
11
+ - '*.gemspec'
12
+
13
+ Metrics/BlockLength:
14
+ Max: 30
15
+
16
+ Metrics/MethodLength:
17
+ Max: 25
18
+
19
+ RSpec/ExampleLength:
20
+ Max: 25
21
+
22
+ Metrics/ClassLength:
23
+ Max: 150
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.6
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 2.7.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+
2
+ #### 0.0.1 - TBD
3
+
4
+ * Initial proof-of-concept release.
@@ -0,0 +1,73 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported. All complaints will be reviewed and investigated and will result in a response that
59
+ is deemed necessary and appropriate to the circumstances. The project team is
60
+ obligated to maintain confidentiality with regard to the reporter of an incident.
61
+ Further details of specific enforcement policies may be posted separately.
62
+
63
+ Project maintainers who do not follow or enforce the Code of Conduct in good
64
+ faith may face temporary or permanent repercussions as determined by other
65
+ members of the project's leadership.
66
+
67
+ ## Attribution
68
+
69
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
70
+ available at [http://contributor-covenant.org/version/1/4][version]
71
+
72
+ [homepage]: http://contributor-covenant.org
73
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard :rspec, cmd: 'DISABLE_SIMPLECOV=true bundle exec rspec --format=documentation' do
4
+ require 'guard/rspec/dsl'
5
+ dsl = Guard::RSpec::Dsl.new(self)
6
+
7
+ # RSpec files
8
+ rspec = dsl.rspec
9
+ watch(rspec.spec_helper) { rspec.spec_dir }
10
+ watch(rspec.spec_support) { rspec.spec_dir }
11
+ watch(rspec.spec_files)
12
+
13
+ # Ruby files
14
+ ruby = dsl.ruby
15
+ dsl.watch_spec_files_for(ruby.lib_files)
16
+ end
data/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
2
+
3
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
4
+
5
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # Teton
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/teton.svg)](https://badge.fury.io/rb/teton) [![Ruby Gem CI](https://github.com/mattruggio/teton/actions/workflows/rubygem.yml/badge.svg)](https://github.com/mattruggio/teton/actions/workflows/rubygem.yml) [![Maintainability](https://api.codeclimate.com/v1/badges/787a5d512223e85efd69/maintainability)](https://codeclimate.com/github/mattruggio/teton/maintainability) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+
5
+ #### Hierarchical key-value object store interface
6
+
7
+ ---
8
+
9
+ Store key-value pair-based objects in a key-based hierarchy. Provides a pluggable interface for multiple back-ends.
10
+
11
+ ## Installation
12
+
13
+ To install through Rubygems:
14
+
15
+ ````
16
+ gem install teton
17
+ ````
18
+
19
+ You can also add this to your Gemfile using:
20
+
21
+ ````
22
+ bundle add teton
23
+ ````
24
+
25
+ ## Examples
26
+
27
+ The main API is made up of these instance methods:
28
+
29
+ Method | Description
30
+ ------------------- | -----------
31
+ `Db#set(key, data)` | Set an entries data to the passed in values.
32
+ `Db#get(key)` | Get the entry if it exists or nil if it does not. If the key is a resource then it will always return an array.
33
+ `Db#del(key)` | Delete the key and all children of the key from the store.
34
+ `Db#count(key)` | The number of entries directly under a key if the key is a resource. If they key is an entry then 1 if the entry exists and 0 if it does not exist.
35
+
36
+ #### Setting Up Database
37
+
38
+ ````ruby
39
+ db = Teton::Db.new
40
+ ````
41
+
42
+ #### Setting Objects
43
+
44
+ ````ruby
45
+ bozo_key = 'users/1'
46
+ inception_key = "#{bozo_key}/movies/1" # => users/1/movies/1
47
+ inception_actors_key = "#{inception_key}/actors" # => users/1/movies/1/actors
48
+ leo_key = "#{inception_actors_key}/1" # => users/1/movies/1/actors/1
49
+ tom_key = "#{inception_actors_key}/2" # => users/1/movies/1/actors/2
50
+
51
+ db.set(bozo_key, first: 'bozo', last: 'clown')
52
+ .set(inception_key, title: 'Inception', year: 2010)
53
+ .set(leo_key, first: 'Leonardo', last: 'DiCaprio', star: true)
54
+ .set(tom_key, first: 'Tom', last: 'Hardy', star: true)
55
+ ````
56
+
57
+ Note(s):
58
+
59
+ * `#set` returns self.
60
+ * If an inner key within the key does not exist then it will be added to the hierarchy.
61
+
62
+ #### Retrieving Objects
63
+
64
+ ````ruby
65
+ bozo = db.get(bozo_key) # => Teton::Entry
66
+ inception = db.get(inception_key) # => Teton::Entry
67
+ leo = db.get(leo_key) # => Teton::Entry
68
+ tom = db.get(tom_key) # => Teton::Entry
69
+ inception_actors = db.get(inception_actors_key) # => [Teton::Entry]
70
+ ````
71
+
72
+ Note(s):
73
+
74
+ * If a key does not exist then nil will be returned.
75
+ * If a key is for a resource then it will return an array.
76
+
77
+ #### Deleting Objects
78
+
79
+ ````ruby
80
+ db.del(leo_key)
81
+ .get(inception_key)
82
+ .del(bozo_key)
83
+ ````
84
+
85
+ Note(s):
86
+
87
+ * `#del` returns self.
88
+ * If an inner key is deleted then all child keys in the hierarchy are deleted.
89
+
90
+ #### Backends
91
+
92
+ The back-end: `Teton::Stores::Memory` will be used by default. You can also pass in another back-end if one exists:
93
+
94
+ ````ruby
95
+ store = Teton::Stores::MySQL.new(host: '127.0.0.1', db: 'teton_entries')
96
+ db = Teton::Db.new(store: store)
97
+ ````
98
+
99
+ Note(s):
100
+
101
+ * Each back-end may require specific configuration so it is up to you to check the desired back-end's documentation.
102
+ * Currently `Teton::Stores::MySQL` does not exist as an implementation but any store (i.e. MySQL, PostgeSQL, Redis, S3, traditional file systems) should all be possible.
103
+
104
+ Each back-end provides its own persistence mechanics. For example, `Teton::Stores::Memory` provides persistence/serialization methods:
105
+
106
+ Method | Description
107
+ ------------------- | -----------
108
+ `#load!(path(` | Load from a file on disk
109
+ `#save!(path)` | Save to a file on disk
110
+ `#from_json!` | Deserialize a passed in JSON string
111
+ `#to_json` | Return a serialized JSON string
112
+
113
+ ## Contributing
114
+
115
+ ### Development Environment Configuration
116
+
117
+ Basic steps to take to get this repository compiling:
118
+
119
+ 1. Install [Ruby](https://www.ruby-lang.org/en/documentation/installation/) (check teton.gemspec for versions supported)
120
+ 2. Install bundler (gem install bundler)
121
+ 3. Clone the repository (git clone git@github.com:mattruggio/teton.git)
122
+ 4. Navigate to the root folder (cd teton)
123
+ 5. Install dependencies (bundle)
124
+
125
+ ### Running Tests
126
+
127
+ To execute the test suite run:
128
+
129
+ ````zsh
130
+ bin/rspec spec --format documentation
131
+ ````
132
+
133
+ Alternatively, you can have Guard watch for changes:
134
+
135
+ ````zsh
136
+ bin/guard
137
+ ````
138
+
139
+ Also, do not forget to run Rubocop:
140
+
141
+ ````zsh
142
+ bin/rubocop
143
+ ````
144
+
145
+ And auditing the dependencies:
146
+
147
+ ````zsh
148
+ bin/bundler-audit check --update
149
+ ````
150
+
151
+ ### Publishing
152
+
153
+ Note: ensure you have proper authorization before trying to publish new versions.
154
+
155
+ After code changes have successfully gone through the Pull Request review process then the following steps should be followed for publishing new versions:
156
+
157
+ 1. Merge Pull Request into main
158
+ 2. Update `version.rb` using [semantic versioning](https://semver.org/)
159
+ 3. Install dependencies: `bundle`
160
+ 4. Update `CHANGELOG.md` with release notes
161
+ 5. Commit & push main to remote and ensure CI builds main successfully
162
+ 6. Run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
163
+
164
+ ## Code of Conduct
165
+
166
+ Everyone interacting in this codebase, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/mattruggio/teton/blob/main/CODE_OF_CONDUCT.md).
167
+
168
+ ## License
169
+
170
+ This project is MIT Licensed.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[rubocop spec]
data/bin/bundle-audit ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'bundle-audit' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("bundler-audit", "bundle-audit")
data/bin/bundler-audit ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'bundler-audit' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("bundler-audit", "bundler-audit")
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'teton'
6
+ require 'pry'
7
+
8
+ Pry.start
data/bin/guard ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'guard' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("guard", "guard")
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
data/bin/rubocop ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rubocop", "rubocop")
data/lib/teton/db.rb ADDED
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'entry'
4
+ require_relative 'key'
5
+ require_relative 'stores/memory'
6
+
7
+ module Teton
8
+ # The main interface for any store backend.
9
+ class Db
10
+ DEFAULT_SEPARATOR = '/'
11
+
12
+ attr_reader :separator, :store
13
+
14
+ def initialize(separator: DEFAULT_SEPARATOR, store: Stores::Memory.new)
15
+ raise ArgumentError, 'separator is required' if separator.to_s.empty?
16
+
17
+ @separator = separator.to_s
18
+ @store = store
19
+
20
+ freeze
21
+ end
22
+
23
+ def set(key, data)
24
+ key = key(key)
25
+
26
+ raise ArgumentError, "key: #{key} does not point to an entry" unless key.entry?
27
+
28
+ store.set(key, string_keys_and_values(data))
29
+
30
+ self
31
+ end
32
+
33
+ def get(key)
34
+ store.get(key(key))
35
+ end
36
+
37
+ def del(key)
38
+ tap { store.del(key(key)) }
39
+ end
40
+
41
+ def count(key)
42
+ store.count(key(key))
43
+ end
44
+
45
+ private
46
+
47
+ def key(key)
48
+ key.is_a?(Key) ? key : Key.new(key, separator: separator)
49
+ end
50
+
51
+ def string_keys_and_values(hash)
52
+ (hash || {}).to_h { |k, v| [k.to_s, v.to_s] }
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teton
4
+ # Describe what data should be returned when requested.
5
+ class Entry
6
+ attr_reader :key, :data, :created_at, :updated_at
7
+
8
+ def initialize(key, data: {}, created_at: Time.now.utc, updated_at: Time.now.utc)
9
+ @key = key.to_s
10
+ @data = (data || {}).transform_keys(&:to_s)
11
+ @created_at = created_at || Time.now.utc
12
+ @updated_at = updated_at || Time.now.utc
13
+
14
+ freeze
15
+ end
16
+
17
+ def [](data_key)
18
+ data[data_key]
19
+ end
20
+
21
+ def to_s
22
+ "[#{key} |> #{created_at} | #{updated_at}] #{data.map { |k, v| "#{k}: #{v}" }.join(', ')}"
23
+ end
24
+ end
25
+ end
data/lib/teton/key.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teton
4
+ # Understands a fully-qualified path to a resource or resources.
5
+ class Key
6
+ attr_reader :path, :parts, :separator
7
+
8
+ def initialize(path, separator:)
9
+ raise ArgumentError, 'separator is required' if separator.to_s.empty?
10
+ raise ArgumentError, 'path is required' if path.to_s.empty?
11
+
12
+ @path = path.to_s
13
+ @parts = path.to_s.split(separator)
14
+ @separator = separator.to_s
15
+
16
+ freeze
17
+ end
18
+
19
+ def to_s(suffix_keys = [])
20
+ suffix_keys = Array(suffix_keys)
21
+
22
+ (parts + suffix_keys).join(separator)
23
+ end
24
+
25
+ def traverse(&block)
26
+ parts.each_with_index(&block)
27
+ end
28
+
29
+ def entry?
30
+ parts.length.even?
31
+ end
32
+
33
+ def resource?
34
+ parts.length.odd?
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teton
4
+ module Stores
5
+ # Plugs in a light-weight store that can be used for modeling other stores.
6
+ class Memory
7
+ CREATED_AT_KEY = 'created_at'
8
+ IDS_KEY = 'ids'
9
+ INDICES_KEY = 'indices'
10
+ META_KEY = 'meta'
11
+ DATA_KEY = 'data'
12
+ UPDATED_AT_KEY = 'updated_at'
13
+
14
+ attr_reader :store
15
+
16
+ def initialize(store = {})
17
+ @store = store || {}
18
+ end
19
+
20
+ # Main Object API
21
+
22
+ def set(key, data)
23
+ pointer = store
24
+
25
+ key.traverse do |part, index|
26
+ if index < key.parts.length - 1
27
+ pointer = insert_traverse(index, pointer, part)
28
+ else
29
+ # last id
30
+ upsert(pointer, part, data)
31
+ end
32
+ end
33
+
34
+ self
35
+ end
36
+
37
+ def get(key)
38
+ pointer = store
39
+
40
+ key.traverse do |part, index|
41
+ break unless pointer
42
+
43
+ pointer =
44
+ if index < key.parts.length - 1
45
+ # not last part
46
+ traverse(index, pointer, part)
47
+ elsif key.resource?
48
+ # last part
49
+ entries(key, pointer, part)
50
+ else
51
+ # id
52
+ entry(key, pointer, part)
53
+ end
54
+ end
55
+
56
+ pointer
57
+ end
58
+
59
+ def del(key)
60
+ pointer = store
61
+
62
+ key.traverse do |part, index|
63
+ break unless pointer
64
+
65
+ if index < key.parts.length - 1
66
+ # not last part
67
+ pointer = traverse(index, pointer, part)
68
+ else
69
+ # last part
70
+ pointer.delete(part)
71
+ end
72
+ end
73
+
74
+ self
75
+ end
76
+
77
+ def count(key)
78
+ count = 0
79
+ pointer = store
80
+
81
+ key.traverse do |part, index|
82
+ break unless pointer
83
+
84
+ if index < key.parts.length - 1
85
+ # not last part
86
+ pointer = traverse(index, pointer, part)
87
+ elsif key.resource?
88
+ # last part
89
+ count = (pointer.dig(part, IDS_KEY) || {}).keys.length
90
+ else
91
+ # id
92
+ count = pointer.dig(part, DATA_KEY) ? 1 : 0
93
+ end
94
+ end
95
+
96
+ count
97
+ end
98
+
99
+ # Persistence API
100
+
101
+ def load!(path)
102
+ from_json!(File.read(path))
103
+ end
104
+
105
+ def save!(path)
106
+ dir = File.dirname(path)
107
+
108
+ FileUtils.mkdir_p(dir)
109
+
110
+ File.write(path, to_json)
111
+
112
+ self
113
+ end
114
+
115
+ def from_json!(json)
116
+ @store = JSON.parse(json)
117
+
118
+ self
119
+ end
120
+
121
+ def to_json(*_args)
122
+ store.to_json
123
+ end
124
+
125
+ private
126
+
127
+ def upsert(pointer, part, data)
128
+ pointer[part] = record_prototype unless pointer.key?(part)
129
+
130
+ pointer[part][DATA_KEY] = data
131
+ pointer[part][META_KEY][UPDATED_AT_KEY] = Time.now.utc
132
+
133
+ nil
134
+ end
135
+
136
+ def insert_traverse(index, pointer, part)
137
+ if index.even?
138
+ # index
139
+ pointer[part] = { IDS_KEY => {} } unless pointer.key?(part)
140
+
141
+ pointer[part][IDS_KEY]
142
+ else
143
+ # id
144
+ pointer[part] = record_prototype unless pointer.key?(part)
145
+
146
+ pointer[part][INDICES_KEY]
147
+ end
148
+ end
149
+
150
+ def traverse(index, pointer, part)
151
+ if index.even?
152
+ # index
153
+ pointer.dig(part, IDS_KEY)
154
+ else
155
+ # id
156
+ pointer.dig(part, INDICES_KEY)
157
+ end
158
+ end
159
+
160
+ def entry(key, pointer, part)
161
+ data = pointer.dig(part, DATA_KEY)
162
+ meta = pointer.dig(part, META_KEY)
163
+
164
+ return unless data
165
+
166
+ Entry.new(
167
+ key.to_s,
168
+ data: data,
169
+ created_at: meta[CREATED_AT_KEY],
170
+ updated_at: meta[UPDATED_AT_KEY]
171
+ )
172
+ end
173
+
174
+ def entries(key, pointer, part)
175
+ pointer = pointer.dig(part, IDS_KEY)
176
+
177
+ return [] unless pointer
178
+
179
+ pointer.map do |inner_part, value|
180
+ Entry.new(
181
+ key.to_s(inner_part),
182
+ data: value[DATA_KEY],
183
+ created_at: value[META_KEY][CREATED_AT_KEY],
184
+ updated_at: value[META_KEY][UPDATED_AT_KEY]
185
+ )
186
+ end
187
+ end
188
+
189
+ def record_prototype
190
+ {
191
+ DATA_KEY => {},
192
+ INDICES_KEY => {},
193
+ META_KEY => {
194
+ CREATED_AT_KEY => Time.now.utc,
195
+ UPDATED_AT_KEY => Time.now.utc
196
+ }
197
+ }
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teton
4
+ VERSION = '0.0.1'
5
+ end
data/lib/teton.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'time'
6
+
7
+ require_relative 'teton/db'
data/teton.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './lib/teton/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'teton'
7
+ s.version = Teton::VERSION
8
+ s.summary = 'Hierarchical key-value object store interface.'
9
+
10
+ s.description = 'Store key-value pair objects in a discoverable hierarchy. Provides a pluggable interface for multiple back-ends.'
11
+
12
+ s.authors = ['Matthew Ruggio']
13
+ s.email = ['mattruggio@icloud.com']
14
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
15
+ s.bindir = 'exe'
16
+ s.executables = %w[]
17
+ s.homepage = 'https://github.com/mattruggio/teton'
18
+ s.license = 'MIT'
19
+ s.metadata = {
20
+ 'bug_tracker_uri' => 'https://github.com/mattruggio/teton/issues',
21
+ 'changelog_uri' => 'https://github.com/mattruggio/teton/blob/main/CHANGELOG.md',
22
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/teton',
23
+ 'homepage_uri' => s.homepage,
24
+ 'source_code_uri' => s.homepage,
25
+ 'rubygems_mfa_required' => 'true'
26
+ }
27
+
28
+ s.required_ruby_version = '>= 2.7.6'
29
+
30
+ s.add_development_dependency('bundler-audit')
31
+ s.add_development_dependency('guard-rspec')
32
+ s.add_development_dependency('pry')
33
+ s.add_development_dependency('rake')
34
+ s.add_development_dependency('rspec')
35
+ s.add_development_dependency('rubocop')
36
+ s.add_development_dependency('rubocop-rake')
37
+ s.add_development_dependency('rubocop-rspec')
38
+ s.add_development_dependency('simplecov')
39
+ s.add_development_dependency('simplecov-console')
40
+ end
metadata ADDED
@@ -0,0 +1,217 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: teton
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Ruggio
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-09-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler-audit
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: guard-rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: simplecov-console
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: Store key-value pair objects in a discoverable hierarchy. Provides a
154
+ pluggable interface for multiple back-ends.
155
+ email:
156
+ - mattruggio@icloud.com
157
+ executables: []
158
+ extensions: []
159
+ extra_rdoc_files: []
160
+ files:
161
+ - ".editorconfig"
162
+ - ".github/workflows/rubygem.yml"
163
+ - ".gitignore"
164
+ - ".rubocop.yml"
165
+ - ".ruby-version"
166
+ - ".tool-versions"
167
+ - CHANGELOG.md
168
+ - CODE_OF_CONDUCT.md
169
+ - Gemfile
170
+ - Guardfile
171
+ - LICENSE
172
+ - README.md
173
+ - Rakefile
174
+ - bin/bundle-audit
175
+ - bin/bundler-audit
176
+ - bin/console
177
+ - bin/guard
178
+ - bin/rake
179
+ - bin/rspec
180
+ - bin/rubocop
181
+ - lib/teton.rb
182
+ - lib/teton/db.rb
183
+ - lib/teton/entry.rb
184
+ - lib/teton/key.rb
185
+ - lib/teton/stores/memory.rb
186
+ - lib/teton/version.rb
187
+ - teton.gemspec
188
+ homepage: https://github.com/mattruggio/teton
189
+ licenses:
190
+ - MIT
191
+ metadata:
192
+ bug_tracker_uri: https://github.com/mattruggio/teton/issues
193
+ changelog_uri: https://github.com/mattruggio/teton/blob/main/CHANGELOG.md
194
+ documentation_uri: https://www.rubydoc.info/gems/teton
195
+ homepage_uri: https://github.com/mattruggio/teton
196
+ source_code_uri: https://github.com/mattruggio/teton
197
+ rubygems_mfa_required: 'true'
198
+ post_install_message:
199
+ rdoc_options: []
200
+ require_paths:
201
+ - lib
202
+ required_ruby_version: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: 2.7.6
207
+ required_rubygems_version: !ruby/object:Gem::Requirement
208
+ requirements:
209
+ - - ">="
210
+ - !ruby/object:Gem::Version
211
+ version: '0'
212
+ requirements: []
213
+ rubygems_version: 3.1.6
214
+ signing_key:
215
+ specification_version: 4
216
+ summary: Hierarchical key-value object store interface.
217
+ test_files: []