active-dynamo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1946cead605cf9a2c458ede322635850d0ed2301b315bd03b9687cc667307b22
4
+ data.tar.gz: 168c5b0352194c504f75339b7209be825599f83d2854369195362974f3078027
5
+ SHA512:
6
+ metadata.gz: fbffb427e806bf4f3226a9c28a3a93fb4cbdb7b407c891eb165e98762689093ed3f3b6124fff877f1ddf14efdc93477ff17276dd93894edd3b6beecb230f31f8
7
+ data.tar.gz: b7d1db1d93b20fe29f62b532803cb56665356e7b7b477bf849261e8c74212a3a907802f5d7fcee8607e47988a394f563a381cfd64ebcc492c4c70742dc252b84
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .byebug_history
3
+ .bundle/
4
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
9
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Ahmed Saleh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,62 @@
1
+ active-dynamo
2
+ ---
3
+
4
+ An ActiveRecord like ODM for AWS DynamoDB
5
+
6
+ ## Installation
7
+
8
+ ```sh
9
+ gem install active-dynamo
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ Currently, the supported operations are as follows:
15
+
16
+ - Define a model in a way similar to ActiveRecord, calling `table_name` and
17
+ `attributes` functions:
18
+
19
+ ```ruby
20
+ class Account < ActiveDynamo::Base
21
+ table name: 'account', partition_key: :no, sort_key: :balance
22
+ attributes no: Integer, balance: Integer, kind: String
23
+ end
24
+ ```
25
+
26
+ - Create a new record:
27
+
28
+ ```ruby
29
+ account = Account.new(no: 123, balance: 2000, kind: 'current')
30
+ account.save
31
+
32
+ # or use `create`
33
+ account = Account.create(no: 123, balance: 2000, kind: 'current')
34
+ ```
35
+
36
+ - Query the table using methods such as:
37
+
38
+ ```ruby
39
+ Account.all
40
+
41
+ Account.where(no: 123, balance: 2000)
42
+ Account.where("no = 123 and balance >= 2000 and kind = 'current'")
43
+
44
+ Account.find(no: 123, balance: 2000)
45
+ ```
46
+
47
+ - Update a record
48
+
49
+ ```ruby
50
+ account = Account.first
51
+ account.update(kind: 'savings')
52
+ ```
53
+
54
+ - Delete a record
55
+
56
+ ```ruby
57
+ Account.destroy(no: 123, balance: 2000)
58
+ ```
59
+
60
+ ## License
61
+
62
+ See [LICENSE](https://github.com/aonemd/active-dynamo/blob/master/LICENSE).
@@ -0,0 +1,16 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ end
8
+ task :default => :test
9
+
10
+ desc "Make a new build and publish it to RubyGems"
11
+ task :publish do
12
+ build_name_and_version = "nin-#{ActiveDynamo::VERSION}.gem"
13
+
14
+ system "gem build nin.gemspec --silent --output #{build_name_and_version}"
15
+ system "gem push #{build_name_and_version}"
16
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ require File.expand_path('../lib/active_dynamo/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Ahmed Saleh"]
6
+ gem.email = 'aonemdsaleh@gmail.com'
7
+ gem.description = "An ActiveRecord like ODM for AWS DynamoDB"
8
+ gem.summary = "An ActiveRecord like ODM for AWS DynamoDB"
9
+ gem.homepage = 'https://github.com/aonemd/active-dynamo'
10
+
11
+ gem.files = `git ls-files`.split($\).reject do |f|
12
+ f.match(%r{^(test|spec|features)/})
13
+ end
14
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
+
16
+ gem.name = 'active-dynamo'
17
+ gem.version = ActiveDynamo::VERSION
18
+ gem.license = 'MIT'
19
+
20
+ gem.add_dependency 'aws-sdk-dynamodb'
21
+
22
+ gem.add_development_dependency 'rake'
23
+ gem.add_development_dependency 'minitest'
24
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "active_dynamo"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,10 @@
1
+ require 'aws-sdk-dynamodb'
2
+
3
+ require 'active_dynamo/version'
4
+ require 'active_dynamo/extensions/class_extensions'
5
+ require 'active_dynamo/extensions/hash_extensions'
6
+
7
+ require 'active_dynamo/base'
8
+
9
+ module ActiveDynamo
10
+ end
@@ -0,0 +1,63 @@
1
+ require 'active_dynamo/query'
2
+ require 'active_dynamo/persistence'
3
+
4
+ module ActiveDynamo
5
+ class Base
6
+ include Query
7
+ include Persistence
8
+
9
+ class << self
10
+ def table(options = {})
11
+ @@table_name = options.fetch(:name, snake_name)
12
+ @@partition_key = options.fetch(:partition_key, nil)
13
+ @@sort_key = options.fetch(:sort_key, nil)
14
+ @@primary_key = [@@partition_key, @@sort_key].compact
15
+ @@db_conn = options.fetch(:db_conn, Aws::DynamoDB::Client.new)
16
+
17
+ class_variables.each do |var|
18
+ define_singleton_method(var.to_s.delete('@')) do
19
+ class_variable_get(var)
20
+ end
21
+ end
22
+ end
23
+
24
+ def attributes(**attrs)
25
+ @@attr_types = attrs
26
+ @@attrs = attrs.keys
27
+ attr_reader(*@@attrs)
28
+
29
+ define_method(:initialize) do |**args|
30
+ @@attrs.each do |name|
31
+ _type_parser = method(@@attr_types.fetch(name).to_s)
32
+ _value = _type_parser.call(args[name])
33
+ instance_variable_set("@#{name}", _value)
34
+ end
35
+ end
36
+
37
+ define_singleton_method('attrs') do
38
+ class_variable_get('@@attrs')
39
+ end
40
+
41
+ define_singleton_method('attr_types') do
42
+ class_variable_get('@@attr_types')
43
+ end
44
+ end
45
+ end
46
+
47
+ def attributes
48
+ @@attrs.inject({}) do |h, attr|
49
+ h.update(attr => self.instance_variable_get("@#{attr}"))
50
+ end
51
+ end
52
+
53
+ def primary_key_attributes
54
+ self.attributes.slice(*@@primary_key)
55
+ end
56
+
57
+ private
58
+
59
+ def update_primary_key(keys)
60
+ @@primary_key ||= keys
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,5 @@
1
+ class Class
2
+ def snake_name
3
+ name.gsub('::', '_').downcase
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ class Hash
2
+ def symbolize_keys
3
+ self.inject({}) do |h, (k, v)|
4
+ if v.is_a? Hash
5
+ h[k.to_sym] = v.symbolize_keys
6
+ else
7
+ h[k.to_sym] = v
8
+ end
9
+
10
+ h
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ require_relative 'persistence/updater'
2
+
3
+ module ActiveDynamo
4
+ module Persistence
5
+ def self.included(klass)
6
+ klass.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def create(**args)
11
+ obj = new(**args)
12
+ obj.save
13
+ obj
14
+ end
15
+
16
+ def destroy(**key_value)
17
+ db_conn.delete_item({
18
+ table_name: table_name,
19
+ key: key_value
20
+ })
21
+ end
22
+ end
23
+
24
+ def save
25
+ self.class.db_conn.put_item({
26
+ table_name: self.class.table_name,
27
+ item: self.attributes
28
+ })
29
+ end
30
+
31
+ def update(**args)
32
+ Updater.new(self).call(**args)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ module ActiveDynamo
2
+ module Persistence
3
+ class Updater
4
+ def initialize(initiator)
5
+ @initiator = initiator
6
+ end
7
+
8
+ def call(**args)
9
+ updated_attrs = @initiator.class.db_conn.update_item({
10
+ table_name: @initiator.class.table_name,
11
+ key: @initiator.primary_key_attributes,
12
+ update_expression: update_expression(args),
13
+ expression_attribute_values: expression_attribute_values(args),
14
+ return_values: "UPDATED_NEW"
15
+ }).attributes
16
+
17
+ updated_attrs.each do |key, value|
18
+ @initiator.instance_variable_set("@#{key}", value)
19
+ end
20
+
21
+ @initiator
22
+ end
23
+
24
+ private
25
+
26
+ def update_expression(args)
27
+ args.each_with_index.map do |(key, _), index|
28
+ "#{key} = :#{key}#{index}"
29
+ end.join(", ").prepend("SET ")
30
+ end
31
+
32
+ def expression_attribute_values(args)
33
+ args.each_with_index.inject({}) do |expr, ((key, value), index)|
34
+ expr.update(":#{key}#{index}" => value)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ require_relative 'query/query_generator'
2
+
3
+ module ActiveDynamo
4
+ module Query
5
+ def self.included(klass)
6
+ klass.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def all
11
+ db_conn.scan({ table_name: table_name }).items.map do |item|
12
+ new(item.symbolize_keys)
13
+ end
14
+ end
15
+
16
+ def where(args)
17
+ query = QueryGenerator.new(self).call(args)
18
+
19
+ db_conn.query(query).items.map do |item|
20
+ new(item.symbolize_keys)
21
+ end
22
+ end
23
+
24
+ def find(**key_value)
25
+ obj_hash = db_conn
26
+ .get_item({ table_name: table_name, key: key_value }).item
27
+ .symbolize_keys
28
+
29
+ obj = new(obj_hash)
30
+ obj.send(:update_primary_key, key_value.keys)
31
+ obj
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,109 @@
1
+ module ActiveDynamo
2
+ module Query
3
+ class QueryGenerator
4
+ def initialize(initiator)
5
+ @initiator = initiator
6
+ end
7
+
8
+ def call(args)
9
+ augmented_args = if args.is_a? String
10
+ tokenize_string_query(args).map(&:symbolize_keys)
11
+ else
12
+ args.reduce([]) do |arr, (k, v)|
13
+ arr << { key: k, value: v, operator: '=' }
14
+ end
15
+ end
16
+
17
+ key_condition_expression_args = []
18
+ filter_expression_args = []
19
+ expression_attribute_names = {}
20
+ expression_attribute_values = {}
21
+
22
+ augmented_args.each_with_index do |arg, index|
23
+ key = arg[:key].to_sym
24
+ operator = arg[:operator] || '='
25
+
26
+ key_alias = "#key_#{key}_#{index}"
27
+ expression_attribute_names.update(key_alias => key)
28
+
29
+ key_type_parser = method(@initiator.attr_types.fetch(key).to_s)
30
+
31
+ unless operator == 'BETWEEN'
32
+ value = key_type_parser.call(arg[:value])
33
+ value_alias = ":key_#{key}_#{index}_value"
34
+
35
+ sub_expression = [key_alias, value_alias].join(" #{operator} ")
36
+ expression_attribute_values.update(value_alias => value)
37
+ else
38
+ value1 = key_type_parser.call(arg[:value1])
39
+ value2 = key_type_parser.call(arg[:value2])
40
+
41
+ value1_alias = ":key_#{key}_#{index}_value_1"
42
+ value2_alias = ":key_#{key}_#{index}_value_2"
43
+
44
+ sub_expression = [key_alias, [value1_alias, value2_alias].join(' AND ')].join(" #{operator} ")
45
+
46
+ expression_attribute_values.update(value1_alias => value1)
47
+ expression_attribute_values.update(value2_alias => value2)
48
+ end
49
+
50
+ if key == @initiator.partition_key || key == @initiator.sort_key
51
+ key_condition_expression_args.push(sub_expression)
52
+ else
53
+ filter_expression_args.push(sub_expression)
54
+ end
55
+ end
56
+
57
+ query = {
58
+ table_name: @initiator.table_name,
59
+ key_condition_expression: key_condition_expression_args.join(' and '),
60
+ expression_attribute_names: expression_attribute_names,
61
+ expression_attribute_values: expression_attribute_values
62
+ }
63
+
64
+ unless filter_expression_args.empty?
65
+ query[:filter_expression] = filter_expression_args.join(' and ')
66
+ end
67
+
68
+ query
69
+ end
70
+
71
+ def tokenize_string_query(query)
72
+ Lexer.new(query).call()
73
+ end
74
+
75
+ class Lexer
76
+ # S -> PARTION_EXPRESSION SORT_EXPRESSION FILTER_EXPRESSION $
77
+ # PARTION_EXPRESSION -> partition_key = :value
78
+ # SORT_EXPRESSION -> 'and' sort_key = :value | E
79
+ # FILTER_EXPRESSION -> 'and' (SINGLE_EXPRESSION | DOUBLE_EXPRESSION) FILTER_EXPRESSION | E
80
+ # SINGLE_EXPRESSION -> key (=|<|>|<=|>=) :value
81
+ # DOUBLE_EXPRESSION -> key 'BETWEEN' :value1 'AND' :value2
82
+
83
+ def initialize(query)
84
+ @query = query
85
+ @tokens = []
86
+ end
87
+
88
+ def call
89
+ grammar = [
90
+ /(?<key>[\w\d_.-]+)\s*(?<op>BETWEEN)\s*(?<value1>[\w\d_.-]+)\s*AND\s*(?<value2>[\w\d_.-]+)/,
91
+ /(?<key>[\w\d_.-]+)\s*(?<op>(>=|<=|>|<))\s*(?<value>[\w\d_.-]+)/,
92
+ /(?<key>[\w\d_.-]+)\s*(?<op>(=))\s*(?<value>[\w\d_.-]+)/
93
+ ]
94
+
95
+ @query.split('and').each do |sub_query|
96
+ grammar.each do |g|
97
+ if g.match(sub_query)
98
+ @tokens << g.match(sub_query).named_captures
99
+ break
100
+ end
101
+ end
102
+ end
103
+
104
+ @tokens
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveDynamo
2
+ VERSION='0.0.1'
3
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active-dynamo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ahmed Saleh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-10-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-dynamodb
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
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: rake
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: minitest
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
+ description: An ActiveRecord like ODM for AWS DynamoDB
56
+ email: aonemdsaleh@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - ".gitignore"
62
+ - Gemfile
63
+ - LICENSE
64
+ - README.md
65
+ - Rakefile
66
+ - active-dynamo.gemspec
67
+ - bin/console
68
+ - lib/active_dynamo.rb
69
+ - lib/active_dynamo/base.rb
70
+ - lib/active_dynamo/extensions/class_extensions.rb
71
+ - lib/active_dynamo/extensions/hash_extensions.rb
72
+ - lib/active_dynamo/persistence.rb
73
+ - lib/active_dynamo/persistence/updater.rb
74
+ - lib/active_dynamo/query.rb
75
+ - lib/active_dynamo/query/query_generator.rb
76
+ - lib/active_dynamo/version.rb
77
+ homepage: https://github.com/aonemd/active-dynamo
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.1.2
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: An ActiveRecord like ODM for AWS DynamoDB
100
+ test_files: []