sqlcached_client 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
+ SHA1:
3
+ metadata.gz: 2f642ec5bff84d1fb7738aff7fedad4d695e8224
4
+ data.tar.gz: 0168729e04f2d4eca81b5159e09efaa8c0cedd5f
5
+ SHA512:
6
+ metadata.gz: 5a96a6163594e8a34a86e93b503552d453bd67b0ff96ef4da4b97386da954fdf767504acf3876b1903bf06e18e3e519bc940ca60b3ded5f63fc2bbc34e813361
7
+ data.tar.gz: d494b4126fa2e8fb60ddfa7562675b48e6f94e9fd9474f2712d51631a11117905c3d9934fe2250b1f1ea4f9bfed498916697d672ec065c8bcbfd632e442f8097
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sqlcached_client.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Roberto Maestroni
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # SqlcachedClient
2
+
3
+ A Ruby client for [Sqlcached](https://github.com/rmaestroni/sqlcached).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'sqlcached_client'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install sqlcached_client
18
+
19
+ ## Usage
20
+
21
+ Create your models as you would do with `ActiveRecord`.
22
+
23
+ ```ruby
24
+ require 'sqlcached_client'
25
+
26
+ class Base < SqlcachedClient::Entity
27
+ # things shared amongst all models
28
+ server({ host: 'localhost', port: 8081 })
29
+ end
30
+
31
+
32
+ class User < Base
33
+ entity_name 'user'
34
+
35
+ query <<-SQL
36
+ SELECT * FROM users WHERE id IN ({{ ids }})
37
+ SQL
38
+
39
+ has_many :posts, where: { user_id: :id }
40
+
41
+ has_many :pictures, class_name: 'Image',
42
+ where: { imageable_id: :id, imageable_type: 'User' }
43
+
44
+ has_one :address, where: { user_id: :id }
45
+ end
46
+
47
+
48
+ class Post < Base
49
+ entity_name 'post'
50
+
51
+ query <<-SQL
52
+ SELECT * FROM posts WHERE author_id = {{ user_id }}
53
+ SQL
54
+ end
55
+
56
+ ```
57
+
58
+ Run some queries:
59
+ ```ruby
60
+ users = User.where(ids: '1, 2, 3, 4')
61
+
62
+ users[0].pictures.each # you can navigate through the associations
63
+ ```
64
+
65
+ Load in memory every associated set recursively:
66
+ ```ruby
67
+ users.build_associations # for each entity in the resultset, or...
68
+ users[0].build_associations # for a single entity
69
+ ```
70
+
71
+ ## Contributing
72
+
73
+ 1. Fork it ( https://github.com/rmaestroni/sqlcached_client/fork )
74
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
75
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
76
+ 4. Push to the branch (`git push origin my-new-feature`)
77
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,152 @@
1
+ require 'sqlcached_client/hash_struct'
2
+ require 'sqlcached_client/resultset'
3
+ require 'sqlcached_client/server'
4
+
5
+ module SqlcachedClient
6
+ class Entity < HashStruct
7
+
8
+ attr_reader :attribute_names
9
+
10
+ # @param attributes [Hash]
11
+ def initialize(attributes)
12
+ @attribute_names = attributes.keys
13
+ super(attributes)
14
+ end
15
+
16
+
17
+ class << self
18
+ attr_reader :query_id
19
+
20
+ def entity_name(value)
21
+ @query_id = value
22
+ end
23
+
24
+ def query(sql_string = nil)
25
+ sql_string.nil? ? @query : @query = sql_string.strip
26
+ end
27
+
28
+ def server(config = nil)
29
+ if config.nil?
30
+ @server ||
31
+ if (superclass = ancestors[1]).respond_to?(:server)
32
+ superclass.server
33
+ else
34
+ nil
35
+ end
36
+ else
37
+ @server =
38
+ if config.is_a?(Hash)
39
+ Server.new(config)
40
+ else
41
+ config
42
+ end
43
+ end
44
+ end
45
+
46
+ def where(params, dry_run = false)
47
+ request = server.format_request(query_id, query, params)
48
+ if dry_run
49
+ request
50
+ else
51
+ data = server.run_query(server.build_request_body([request]))
52
+ data = data[0] if data.is_a?(Array)
53
+ Resultset.new(self, data)
54
+ end
55
+ end
56
+
57
+ def has_many(accessor_name, options)
58
+ foreign_class_name =
59
+ if options[:class_name].present?
60
+ options[:class_name]
61
+ else
62
+ accessor_name.to_s.singularize.camelize
63
+ end
64
+ # the query to run to get the data
65
+ association = -> (this, foreign_entity, join_attributes, dry_run) {
66
+ foreign_entity.where(Hash[ join_attributes.map do |attr_names|
67
+ attr_value =
68
+ if attr_names[1].is_a?(Symbol)
69
+ this.send(attr_names[1])
70
+ else
71
+ attr_names[1]
72
+ end
73
+ [ attr_names[0], attr_value ]
74
+ end ], dry_run)
75
+ }
76
+ # get the attributes to define the foreign scope
77
+ join_attributes = (options[:where] || []).to_a
78
+ # memoize the associated resultset
79
+ memoize_var = "@has_many_#{accessor_name}"
80
+ # define the accessor method
81
+ define_method(accessor_name) do |dry_run = false|
82
+ # get the associated entity class
83
+ foreign_entity = Module.const_get(foreign_class_name)
84
+ if dry_run
85
+ association.call(self, foreign_entity, join_attributes, true)
86
+ else
87
+ instance_variable_get(memoize_var) ||
88
+ instance_variable_set(memoize_var,
89
+ association.call(self, foreign_entity, join_attributes, false))
90
+ end
91
+ end
92
+ # define the setter method
93
+ define_method("#{accessor_name}=") do |array|
94
+ # get the associated entity class
95
+ foreign_entity = Module.const_get(foreign_class_name)
96
+ instance_variable_set(memoize_var,
97
+ Resultset.new(foreign_entity, array))
98
+ end
99
+ # save the newly created association
100
+ register_association(accessor_name)
101
+ end
102
+
103
+ def has_one(accessor_name, options)
104
+ plural_accessor_name = "s_#{accessor_name}".to_s.pluralize
105
+ class_name = accessor_name.to_s.camelize
106
+ has_many(plural_accessor_name, { class_name: class_name }.merge(options))
107
+ define_method(accessor_name) do
108
+ send(plural_accessor_name).first
109
+ end
110
+ end
111
+
112
+ def registered_associations
113
+ @registered_associations || []
114
+ end
115
+
116
+ private
117
+
118
+ def register_association(association_name)
119
+ @registered_associations ||= []
120
+ @registered_associations << association_name.to_sym
121
+ end
122
+ end # class << self
123
+
124
+ def get_association_requests
125
+ self.class.registered_associations.map do |a_name|
126
+ send(a_name, true)
127
+ end
128
+ end
129
+
130
+ def set_associations_data(associations_data)
131
+ self.class.registered_associations.map.with_index do |a_name, i|
132
+ send("#{a_name}=", associations_data[i])
133
+ end
134
+ end
135
+
136
+ def get_associations
137
+ self.class.registered_associations.map do |a_name|
138
+ send(a_name)
139
+ end
140
+ end
141
+
142
+ def build_associations(max_depth = false)
143
+ Resultset.new(self.class, [self]).build_associations(max_depth)
144
+ end
145
+
146
+ def to_h
147
+ @_to_h ||=
148
+ Hash[ attribute_names.map { |a_name| [a_name, send(a_name)] } ]
149
+ end
150
+
151
+ end # class Entity
152
+ end
@@ -0,0 +1,70 @@
1
+ module SqlcachedClient
2
+ # Maps recursively hashes into objects with lazy evaluation. For example
3
+ #
4
+ # hs = HashStruct.new({
5
+ # a: ['foo', { a1: [{ a2: 'one' }, 'two', 'three'] }, 'bar'],
6
+ # b: 'baz',
7
+ # c: 4,
8
+ # d: { d1: { d2: { d3: 'hi' } } }
9
+ # })
10
+ # # => #<HashStruct:0x0000000f188008>
11
+ #
12
+ # hs.b
13
+ # # => "baz"
14
+ #
15
+ # hs.c
16
+ # # => 4
17
+ #
18
+ # hs.a
19
+ # # => ["foo", #<HashStruct:0x0000000f1dc798>, "bar"]
20
+ #
21
+ # hs.a[1].a1
22
+ # # => [#<HashStruct:0x0000000f275ce0>, "two", "three"]
23
+ #
24
+ # hs.a[1].a1[0].a2
25
+ # # => "one"
26
+ #
27
+ # # values are memoized
28
+ # hs
29
+ # # => #<HashStruct:0x0000000f188008
30
+ # # @_a=["foo", #<HashStruct:0x0000000f1dc798 @_a1=[#<HashStruct:0x0000000f275ce0 @_a2="one">, "two", "three"]>, "bar"],
31
+ # # @_b="baz",
32
+ # # @_c=4>
33
+ class HashStruct
34
+
35
+ def initialize(hash)
36
+ hash.each do |key, value|
37
+ define_singleton_method(key.to_sym, &HashStruct.build(value, key))
38
+ end
39
+ end
40
+
41
+ class << self
42
+
43
+ # @return [Proc]
44
+ def build(value, accessor_name = nil)
45
+ lambda =
46
+ if value.is_a?(Hash)
47
+ -> (aself, value) { HashStruct.new(value) }
48
+ elsif value.is_a?(Array)
49
+ -> (aself, value) {
50
+ value.map { |item| aself.instance_eval(&HashStruct.build(item)) }
51
+ }
52
+ else
53
+ -> (aself, value) { value }
54
+ end
55
+ if !accessor_name.nil?
56
+ memoize_v = "@_#{accessor_name}"
57
+ Proc.new {
58
+ if instance_variable_defined?(memoize_v)
59
+ instance_variable_get(memoize_v)
60
+ else
61
+ instance_variable_set(memoize_v, lambda.call(self, value))
62
+ end
63
+ }
64
+ else
65
+ Proc.new { lambda.call(self, value) }
66
+ end
67
+ end
68
+ end # class << self
69
+ end # class HashStruct
70
+ end
@@ -0,0 +1,73 @@
1
+ module SqlcachedClient
2
+ class Resultset
3
+ include Enumerable
4
+
5
+ attr_reader :entity_class, :entities, :count
6
+
7
+ # @param entity_class [Class]
8
+ # @param entities [Array]
9
+ def initialize(entity_class, entities)
10
+ @entity_class = entity_class
11
+ @entities = (entities || []).map do |item|
12
+ if item.is_a?(Hash)
13
+ entity_class.new(item)
14
+ elsif item.is_a?(entity_class)
15
+ item
16
+ else
17
+ raise "Cannot handle instances of #{item.class.name}"
18
+ end
19
+ end
20
+ @count = @entities.size
21
+ end
22
+
23
+ class << self
24
+
25
+ def build_associations(resultsets, server, max_depth, current_depth = 0)
26
+ if resultsets.any?
27
+ batch = resultsets.map { |r| r._get_entities_association_requests }
28
+ if batch.flatten.any?
29
+ next_batch =
30
+ server.run_query(
31
+ server.build_request_body(
32
+ batch
33
+ )
34
+ ).map.with_index do |resultset_data, i|
35
+ resultsets[i]._fill_associations(resultset_data)
36
+ end.flatten!
37
+ if !max_depth || current_depth < max_depth
38
+ build_associations(next_batch, server, max_depth, current_depth + 1)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end # class << self
44
+
45
+ def each(&block)
46
+ block ? entities.each(&block) : entities.each
47
+ end
48
+
49
+ def [](i)
50
+ entities[i]
51
+ end
52
+
53
+ def uncache
54
+ # TODO
55
+ end
56
+
57
+ def build_associations(max_depth = false)
58
+ self.class.build_associations([self], entity_class.server, max_depth)
59
+ end
60
+
61
+ def _fill_associations(data)
62
+ data.map.with_index do |entity_assoc_data, i|
63
+ entities[i].set_associations_data(entity_assoc_data)
64
+ end
65
+ end
66
+
67
+ def _get_entities_association_requests
68
+ entities.map do |entity|
69
+ entity.get_association_requests
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,76 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+
5
+ module SqlcachedClient
6
+ class Server
7
+ attr_reader :host, :port
8
+
9
+ def initialize(config)
10
+ @host = config[:host]
11
+ @port = config[:port]
12
+ end
13
+
14
+
15
+ def run_query(http_req_body)
16
+ url = server_url
17
+ Net::HTTP.start(url.host, url.port) do |http|
18
+ req = Net::HTTP::Post.new(data_batch_url)
19
+ req.set_content_type('application/json')
20
+ req.body = http_req_body.to_json
21
+ resp = http.request(req)
22
+ if 'application/json' == resp['Content-Type']
23
+ resp_body = parse_response_body(JSON.parse(resp.body))
24
+ else
25
+ resp_body = resp.body
26
+ end
27
+ if 200 == resp.code.to_i
28
+ resp_body
29
+ else
30
+ raise "Got http response #{resp.code} from server - #{resp_body.inspect}"
31
+ end
32
+ end
33
+ end
34
+
35
+
36
+ def parse_response_body(body)
37
+ if body.is_a?(Array)
38
+ body.map { |item| parse_response_body(item) }
39
+ elsif body.is_a?(Hash)
40
+ if (resultset = body['resultset']).is_a?(String)
41
+ JSON.parse(resultset)
42
+ else
43
+ resultset
44
+ end
45
+ else
46
+ body
47
+ end
48
+ end
49
+
50
+
51
+ def build_request_body(ary)
52
+ { batch: ary }
53
+ end
54
+
55
+
56
+ def format_request(query_id, query_template, params)
57
+ {
58
+ queryId: query_id,
59
+ queryTemplate: query_template,
60
+ queryParams: params
61
+ }
62
+ end
63
+
64
+ private
65
+
66
+ def server_url
67
+ URI("http://#{host}:#{port}")
68
+ end
69
+
70
+ def data_batch_url
71
+ url = server_url
72
+ url.path = "/data-batch"
73
+ url
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,3 @@
1
+ module SqlcachedClient
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,5 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/string'
3
+
4
+ require 'sqlcached_client/version'
5
+ require 'sqlcached_client/entity'
@@ -0,0 +1,26 @@
1
+ require 'sqlcached_client/hash_struct'
2
+
3
+ RSpec.describe SqlcachedClient::HashStruct do
4
+ let(:hash_struct) do
5
+ SqlcachedClient::HashStruct.new({
6
+ a: ['foo', { a1: [{ a2: 'one' }, 'two', 'three'] }, 'bar'],
7
+ b: 'baz',
8
+ c: 4,
9
+ d: { d1: { d2: { d3: 'hi' } } }
10
+ })
11
+ end
12
+
13
+ it "should map keys into methods" do
14
+ [:a, :b, :c, :d].each do |m|
15
+ expect(hash_struct.respond_to?(m)).to eq(true)
16
+ end
17
+ end
18
+
19
+ it "should map recoursively" do
20
+ ary = hash_struct.a
21
+ expect(ary[0]).to eq('foo')
22
+ expect(ary[1].a1[0].a2).to eq('one')
23
+ expect(hash_struct.d.d1.d2.d3).to eq('hi')
24
+ expect(hash_struct.c).to eq(4)
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sqlcached_client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sqlcached_client"
8
+ spec.version = SqlcachedClient::VERSION
9
+ spec.authors = ["Roberto Maestroni"]
10
+ spec.email = ["r.maestroni@gmail.com"]
11
+ spec.summary = %q{A Ruby client for sqlcached}
12
+ spec.description = %q{A Ruby client for sqlcached}
13
+ spec.homepage = "https://github.com/rmaestroni/sqlcached_client"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport", "~> 4.2"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.6"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sqlcached_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Roberto Maestroni
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
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: rspec
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
+ description: A Ruby client for sqlcached
70
+ email:
71
+ - r.maestroni@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - Gemfile
78
+ - LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - lib/sqlcached_client.rb
82
+ - lib/sqlcached_client/entity.rb
83
+ - lib/sqlcached_client/hash_struct.rb
84
+ - lib/sqlcached_client/resultset.rb
85
+ - lib/sqlcached_client/server.rb
86
+ - lib/sqlcached_client/version.rb
87
+ - spec/sqlcached_client/hash_struct_spec.rb
88
+ - sqlcached_client.gemspec
89
+ homepage: https://github.com/rmaestroni/sqlcached_client
90
+ licenses:
91
+ - MIT
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 2.2.2
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: A Ruby client for sqlcached
113
+ test_files:
114
+ - spec/sqlcached_client/hash_struct_spec.rb