datasource 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +1 -0
- data/Rakefile +8 -0
- data/lib/datasource/adapters/active_record.rb +126 -0
- data/lib/datasource/attributes/computed_attribute.rb +41 -0
- data/lib/datasource/attributes/query_attribute.rb +28 -0
- data/lib/datasource/base.rb +99 -0
- data/lib/datasource/serializer/composite.rb +119 -0
- data/lib/datasource.rb +8 -0
- data/test/active_record_helper.rb +10 -0
- data/test/schema.rb +33 -0
- data/test/test_datasource.rb +47 -0
- data/test/test_helper.rb +12 -0
- data/test/test_serializer_composite.rb +48 -0
- metadata +91 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 617cef0cb48d4953a9da29c7476ef583c5d7487e
|
4
|
+
data.tar.gz: bf76cef2be532f75c71729dbf3fa11fa4022ea37
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3d4738bb6e26eae45f63df27fdfe52a7725d49d1e20569eace2af7d71314e20529f3a29fb913979b1f88f960f337418319b2f2dd88ef978ed57d8d72cda5688e
|
7
|
+
data.tar.gz: 3079481e54532538dd4aeb9df57ec2abe0f412f62369caf91ec3ebe16ab5106036321f3070d02be015972a81da9f6bb087de997f38610ba94c843e9cd4425e95
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) <year> <copyright holders>
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Datasource
|
data/Rakefile
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
ActiveRecord::Calculations
|
4
|
+
module ActiveRecord
|
5
|
+
module Calculations
|
6
|
+
def pluck_hash(*column_names)
|
7
|
+
column_names.map! do |column_name|
|
8
|
+
if column_name.is_a?(Symbol) && attribute_alias?(column_name)
|
9
|
+
attribute_alias(column_name)
|
10
|
+
else
|
11
|
+
column_name.to_s
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
if has_include?(column_names.first)
|
16
|
+
construct_relation_for_association_calculations.pluck(*column_names)
|
17
|
+
else
|
18
|
+
relation = spawn
|
19
|
+
relation.select_values = column_names.map { |cn|
|
20
|
+
columns_hash.key?(cn) ? arel_table[cn] : cn
|
21
|
+
}
|
22
|
+
result = klass.connection.select_all(relation.arel, nil, bind_values)
|
23
|
+
columns = result.columns.map do |key|
|
24
|
+
klass.column_types.fetch(key) {
|
25
|
+
result.column_types.fetch(key) { result.identity_type }
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
result.rows.map do |values|
|
30
|
+
{}.tap do |hash|
|
31
|
+
values.zip(columns, result.columns).each do |v|
|
32
|
+
single_attr_hash = { v[2] => v[0] }
|
33
|
+
hash[v[2]] = v[1].type_cast klass.initialize_attributes(single_attr_hash).values.first
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module Datasource
|
43
|
+
module Adapters
|
44
|
+
module ActiveRecord
|
45
|
+
ID_KEY = "id"
|
46
|
+
|
47
|
+
def to_query(scope)
|
48
|
+
ActiveRecord::Base.uncached do
|
49
|
+
scope.select(*get_select_values(scope)).to_sql
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_rows(scope)
|
54
|
+
scope.pluck_hash(*get_select_values(scope))
|
55
|
+
end
|
56
|
+
|
57
|
+
def included_datasource_rows(att, datasource_data, rows)
|
58
|
+
ds_select = datasource_data[:select]
|
59
|
+
unless ds_select.include?(att[:foreign_key])
|
60
|
+
ds_select += [att[:foreign_key]]
|
61
|
+
end
|
62
|
+
ds_scope = datasource_data[:scope]
|
63
|
+
column = "#{ds_scope.klass.table_name}.#{att[:foreign_key]}"
|
64
|
+
ds_scope = ds_scope.where("#{column} IN (?)",
|
65
|
+
rows.map { |row| row[att[:id_key]] })
|
66
|
+
grouped_results = att[:klass].new(ds_scope)
|
67
|
+
.select(ds_select)
|
68
|
+
.results.group_by do |row|
|
69
|
+
row[att[:foreign_key]]
|
70
|
+
end
|
71
|
+
unless datasource_data[:select].include?(att[:foreign_key])
|
72
|
+
grouped_results.each_pair do |k, rows|
|
73
|
+
rows.each do |row|
|
74
|
+
row.delete(att[:foreign_key])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
grouped_results
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_select_values(scope)
|
82
|
+
select_values = Set.new
|
83
|
+
select_values.add("#{scope.klass.table_name}.#{self.class.adapter::ID_KEY}")
|
84
|
+
|
85
|
+
self.class._attributes.each do |att|
|
86
|
+
if attribute_exposed?(att[:name])
|
87
|
+
if att[:klass] == nil
|
88
|
+
select_values.add("#{scope.klass.table_name}.#{att[:name]}")
|
89
|
+
elsif att[:klass].ancestors.include?(Attributes::ComputedAttribute)
|
90
|
+
att[:klass]._depends.keys.map(&:to_s).each do |name|
|
91
|
+
next if name == scope.klass.table_name
|
92
|
+
ensure_table_join!(scope, name, att)
|
93
|
+
end
|
94
|
+
att[:klass]._depends.each_pair do |table, names|
|
95
|
+
Array(names).each do |name|
|
96
|
+
select_values.add("#{table}.#{name}")
|
97
|
+
end
|
98
|
+
# TODO: handle depends on virtual attribute
|
99
|
+
end
|
100
|
+
elsif att[:klass].ancestors.include?(Attributes::QueryAttribute)
|
101
|
+
select_values.add("(#{att[:klass].new.select_value}) as #{att[:name]}")
|
102
|
+
att[:klass]._depends.each do |name|
|
103
|
+
next if name == scope.klass.table_name
|
104
|
+
ensure_table_join!(scope, name, att)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
select_values.to_a
|
110
|
+
end
|
111
|
+
|
112
|
+
def ensure_table_join!(scope, name, att)
|
113
|
+
join_value = scope.joins_values.find do |value|
|
114
|
+
if value.is_a?(Symbol)
|
115
|
+
value.to_s == att[:name]
|
116
|
+
elsif value.is_a?(String)
|
117
|
+
if value =~ /join (\w+)/i
|
118
|
+
$1 == att[:name]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
raise "Given scope does not join on #{name}, but it is required by #{att[:name]}" unless join_value
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Datasource
|
2
|
+
module Attributes
|
3
|
+
class ComputedAttribute
|
4
|
+
class << self
|
5
|
+
attr_accessor :_depends
|
6
|
+
|
7
|
+
def inherited(base)
|
8
|
+
base._depends = (_depends || {}).dup # TODO: deep dup?
|
9
|
+
end
|
10
|
+
|
11
|
+
def depends(*args)
|
12
|
+
args.each do |dep|
|
13
|
+
_depends.deep_merge!(dep)
|
14
|
+
dep.values.each do |names|
|
15
|
+
Array(names).each do |name|
|
16
|
+
define_method(name) do
|
17
|
+
@depend_values[name.to_s]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(depend_values)
|
26
|
+
@depend_values = depend_values
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Datasource::Base
|
32
|
+
def self.computed_attribute(name, deps, &block)
|
33
|
+
klass = Class.new(Attributes::ComputedAttribute) do
|
34
|
+
depends deps
|
35
|
+
|
36
|
+
define_method(:value, &block)
|
37
|
+
end
|
38
|
+
attribute name, klass
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Datasource
|
2
|
+
module Attributes
|
3
|
+
class QueryAttribute
|
4
|
+
class << self
|
5
|
+
attr_accessor :_depends
|
6
|
+
|
7
|
+
def inherited(base)
|
8
|
+
base._depends = (_depends || []).dup
|
9
|
+
end
|
10
|
+
|
11
|
+
def depends(*args)
|
12
|
+
self._depends += args.map(&:to_s)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Datasource::Base
|
19
|
+
def self.query_attribute(name, deps, &block)
|
20
|
+
klass = Class.new(Attributes::QueryAttribute) do
|
21
|
+
depends deps
|
22
|
+
|
23
|
+
define_method(:select_value, &block)
|
24
|
+
end
|
25
|
+
attribute name, klass
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Datasource
|
2
|
+
class Base
|
3
|
+
class << self
|
4
|
+
attr_accessor :_attributes, :_virtual_attributes, :_associations
|
5
|
+
attr_accessor :adapter
|
6
|
+
|
7
|
+
def inherited(base)
|
8
|
+
base._attributes = (_attributes || []).dup
|
9
|
+
@adapter ||= Datasource::Adapters::ActiveRecord
|
10
|
+
self.send :include, @adapter
|
11
|
+
end
|
12
|
+
|
13
|
+
def attributes(*attrs)
|
14
|
+
attrs.each { |name| attribute(name) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def attribute(name, klass = nil)
|
18
|
+
@_attributes.push name: name.to_s, klass: klass
|
19
|
+
end
|
20
|
+
|
21
|
+
def includes_many(name, klass, foreign_key)
|
22
|
+
@_attributes.push name: name.to_s, klass: klass, foreign_key: foreign_key.to_s, id_key: self::ID_KEY
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(scope)
|
27
|
+
@scope = scope
|
28
|
+
@expose_attributes = []
|
29
|
+
@datasource_data = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def select(*names)
|
33
|
+
names = names.flat_map do |name|
|
34
|
+
if name.kind_of?(Hash)
|
35
|
+
# datasource data
|
36
|
+
name.each_pair do |k, v|
|
37
|
+
@datasource_data[k.to_s] = v
|
38
|
+
end
|
39
|
+
name.keys
|
40
|
+
else
|
41
|
+
name
|
42
|
+
end
|
43
|
+
end
|
44
|
+
@expose_attributes = (@expose_attributes + names.map(&:to_s)).uniq
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def attribute_exposed?(name)
|
49
|
+
@expose_attributes.include?(name)
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_query
|
53
|
+
to_query(@scope)
|
54
|
+
end
|
55
|
+
|
56
|
+
def results
|
57
|
+
rows = get_rows(@scope)
|
58
|
+
|
59
|
+
attribute_map = self.class._attributes.inject({}) do |hash, att|
|
60
|
+
hash[att[:name]] = att
|
61
|
+
hash
|
62
|
+
end
|
63
|
+
|
64
|
+
computed_expose_attributes = []
|
65
|
+
datasources = {}
|
66
|
+
|
67
|
+
@expose_attributes.each do |name|
|
68
|
+
att = attribute_map[name]
|
69
|
+
klass = att[:klass]
|
70
|
+
next unless klass
|
71
|
+
|
72
|
+
if klass.ancestors.include?(Attributes::ComputedAttribute)
|
73
|
+
computed_expose_attributes.push(att)
|
74
|
+
elsif klass.ancestors.include?(Base)
|
75
|
+
datasources[att] =
|
76
|
+
included_datasource_rows(att, @datasource_data[att[:name]], rows)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# TODO: field names...
|
81
|
+
rows.each do |row|
|
82
|
+
computed_expose_attributes.each do |att|
|
83
|
+
klass = att[:klass]
|
84
|
+
if klass
|
85
|
+
row[att[:name]] = klass.new(row).value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
datasources.each_pair do |att, rows|
|
89
|
+
row[att[:name]] = Array(rows[row[att[:id_key]]])
|
90
|
+
end
|
91
|
+
row.delete_if do |key, value|
|
92
|
+
!attribute_exposed?(key)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
rows
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Datasource
|
2
|
+
module Serializer
|
3
|
+
class Composite
|
4
|
+
TemplatePart = Struct.new(:parent, :type, :value, :select) do
|
5
|
+
def initialize(parent, type = nil, value = nil)
|
6
|
+
super(parent, type, get_default_value(type, value), [])
|
7
|
+
end
|
8
|
+
|
9
|
+
def set_default_value(value = nil)
|
10
|
+
self.value = get_default_value(type, value)
|
11
|
+
end
|
12
|
+
private
|
13
|
+
def get_default_value(type, value)
|
14
|
+
case type
|
15
|
+
when :hash then {}
|
16
|
+
when :array then []
|
17
|
+
when :datasource then value
|
18
|
+
when nil then nil
|
19
|
+
else
|
20
|
+
fail "Unknown type #{type}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
attr_accessor :datasource_count, :template
|
27
|
+
|
28
|
+
def inherited(base)
|
29
|
+
base.datasource_count = 0
|
30
|
+
@cursor = base.template = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def with_new_cursor(type, value = nil, &block)
|
34
|
+
result = nil
|
35
|
+
new_cursor = TemplatePart.new(@cursor, type, value)
|
36
|
+
@cursor = @cursor.tap do
|
37
|
+
if template.nil?
|
38
|
+
self.template = @cursor = new_cursor
|
39
|
+
elsif @cursor.type == :datasource
|
40
|
+
@cursor = @cursor.parent
|
41
|
+
return with_new_cursor(type, value, &block)
|
42
|
+
elsif @cursor.type == :array
|
43
|
+
@cursor = new_cursor
|
44
|
+
@cursor.parent.push(@cursor)
|
45
|
+
elsif @cursor.type.nil?
|
46
|
+
# replace cursor
|
47
|
+
@cursor.type = type
|
48
|
+
@cursor.set_default_value(value)
|
49
|
+
else
|
50
|
+
fail "Invalid use of #{type}."
|
51
|
+
end
|
52
|
+
result = block.call
|
53
|
+
end
|
54
|
+
result
|
55
|
+
end
|
56
|
+
|
57
|
+
def hash(&block)
|
58
|
+
with_new_cursor(:hash, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
def key(name)
|
62
|
+
fail "Cannot use key outside hash." unless @cursor.type == :hash
|
63
|
+
@cursor = @cursor.tap do
|
64
|
+
@cursor = @cursor.value[name.to_s] = TemplatePart.new(@cursor)
|
65
|
+
yield
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def array(&block)
|
70
|
+
with_new_cursor(:array, &block)
|
71
|
+
end
|
72
|
+
|
73
|
+
def datasource(ds)
|
74
|
+
self.datasource_count += 1
|
75
|
+
@cursor = with_new_cursor(:datasource, ds) { @cursor }
|
76
|
+
end
|
77
|
+
|
78
|
+
def attributes(*attributes)
|
79
|
+
attributes.each { |name| attribute name }
|
80
|
+
end
|
81
|
+
|
82
|
+
def attribute(name)
|
83
|
+
fail "No datasource selected - use \"select_datasource Klass\" first." unless @cursor.type == :datasource
|
84
|
+
@cursor.select << name
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def initialize(*scopes)
|
89
|
+
@scopes = scopes
|
90
|
+
if @scopes.size != self.class.datasource_count
|
91
|
+
fail ArgumentError, "#{self.class.name} needs #{@scopes.size} scopes, you provided #{self.class.datasource_count}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def as_json
|
96
|
+
parse_template_part(self.class.template)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
def parse_template_part(part)
|
101
|
+
return nil unless part
|
102
|
+
case part.type
|
103
|
+
when :hash
|
104
|
+
{}.tap do |result|
|
105
|
+
part.value.each_pair do |k, v|
|
106
|
+
result[k] = parse_template_part(v)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
when :array
|
110
|
+
part.value.map { |v| parse_template_part(v) }
|
111
|
+
when :datasource
|
112
|
+
part.value.new(@scopes.shift).select(*part.select).results
|
113
|
+
else
|
114
|
+
fail "Unknown type #{type}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/datasource.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
|
2
|
+
ActiveRecord::Migration.verbose = false
|
3
|
+
load "schema.rb"
|
4
|
+
|
5
|
+
def clean_db
|
6
|
+
ActiveRecord::Base.connection.execute("DELETE FROM comments")
|
7
|
+
ActiveRecord::Base.connection.execute("DELETE FROM posts")
|
8
|
+
ActiveRecord::Base.connection.execute("DELETE FROM blogs")
|
9
|
+
ActiveRecord::Base.connection.execute("DELETE FROM sqlite_sequence")
|
10
|
+
end
|
data/test/schema.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 0) do
|
2
|
+
create_table :blogs, :force => true do |t|
|
3
|
+
t.string :title
|
4
|
+
end
|
5
|
+
|
6
|
+
create_table :posts, :force => true do |t|
|
7
|
+
t.integer :blog_id
|
8
|
+
t.string :title
|
9
|
+
t.string :author_first_name
|
10
|
+
t.string :author_last_name
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table :comments, :force => true do |t|
|
14
|
+
t.integer :post_id
|
15
|
+
t.text :comment
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Blog < ActiveRecord::Base
|
20
|
+
self.table_name = "blogs"
|
21
|
+
has_many :posts
|
22
|
+
end
|
23
|
+
|
24
|
+
class Post < ActiveRecord::Base
|
25
|
+
self.table_name = "posts"
|
26
|
+
belongs_to :blog
|
27
|
+
has_many :comments
|
28
|
+
end
|
29
|
+
|
30
|
+
class Comment < ActiveRecord::Base
|
31
|
+
self.table_name = "comments"
|
32
|
+
belongs_to :post
|
33
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'active_record_helper'
|
3
|
+
require 'pry'
|
4
|
+
|
5
|
+
class PostsDatasource < Datasource::Base
|
6
|
+
attributes :id, :title, :blog_id
|
7
|
+
|
8
|
+
computed_attribute :author, posts: [ :author_first_name, :author_last_name ] do
|
9
|
+
{ "name" => "#{author_first_name} #{author_last_name}" }
|
10
|
+
end
|
11
|
+
|
12
|
+
query_attribute :author_name, :posts do
|
13
|
+
"posts.author_first_name || ' ' || posts.author_last_name"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class BlogsDatasource < Datasource::Base
|
18
|
+
attributes :id
|
19
|
+
|
20
|
+
includes_many :posts, PostsDatasource, :blog_id
|
21
|
+
end
|
22
|
+
|
23
|
+
class DatasourceTest < ActiveSupport::TestCase
|
24
|
+
def test_basic
|
25
|
+
blog = Blog.create! title: "Blog 1"
|
26
|
+
blog.posts.create! title: "Post 1", author_first_name: "John", author_last_name: "Doe"
|
27
|
+
|
28
|
+
ds = PostsDatasource.new(Post.all)
|
29
|
+
|
30
|
+
assert_equal [{"id"=>1, "title"=>"Post 1", "author_name"=>"John Doe", "author"=>{"name"=>"John Doe"}}],
|
31
|
+
ds.select(:id, :title, :author, :author_name).results
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_assoc
|
35
|
+
blog = Blog.create! title: "Blog 1"
|
36
|
+
blog.posts.create! title: "Post 1", author_first_name: "John", author_last_name: "Doe"
|
37
|
+
|
38
|
+
ds = BlogsDatasource.new(Blog.all)
|
39
|
+
|
40
|
+
assert_equal [{"id"=>1, "posts"=>[{"id"=>1, "author_name"=>"John Doe"}]}],
|
41
|
+
ds.select(:id, posts: { scope: Post.all, select: [:id, :author_name] }).results
|
42
|
+
end
|
43
|
+
|
44
|
+
def teardown
|
45
|
+
clean_db
|
46
|
+
end
|
47
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Configure Rails Environment
|
2
|
+
ENV["RAILS_ENV"] = "test"
|
3
|
+
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'active_support/all'
|
6
|
+
require 'active_record'
|
7
|
+
require 'datasource'
|
8
|
+
|
9
|
+
# Load support files
|
10
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
11
|
+
|
12
|
+
# ActiveRecord::Base.logger = Logger.new STDOUT
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'active_record_helper'
|
3
|
+
require 'pry'
|
4
|
+
|
5
|
+
class PostsDatasource < Datasource::Base
|
6
|
+
attributes :id, :title, :blog_id
|
7
|
+
end
|
8
|
+
|
9
|
+
class BlogsDatasource < Datasource::Base
|
10
|
+
attributes :id, :title
|
11
|
+
includes_many :posts, PostsDatasource, :blog_id
|
12
|
+
end
|
13
|
+
|
14
|
+
class BlogsAndPostsSerializer < Datasource::Serializer::Composite
|
15
|
+
hash do
|
16
|
+
key :blogs do
|
17
|
+
datasource BlogsDatasource
|
18
|
+
attributes :id, :title,
|
19
|
+
posts: { select: [ :id ], scope: Post.all }
|
20
|
+
end
|
21
|
+
|
22
|
+
key :posts do
|
23
|
+
datasource PostsDatasource
|
24
|
+
attributes :id, :title
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class SerializerCompositeTest < ActiveSupport::TestCase
|
30
|
+
# SELECT blogs.id, blogs.title FROM "blogs"
|
31
|
+
# SELECT posts.id, posts.blog_id FROM "posts" WHERE (posts.blog_id IN (1,2))
|
32
|
+
# SELECT posts.id, posts.title FROM "posts"
|
33
|
+
def test_blogs_and_posts_serializer
|
34
|
+
blog = Blog.create! title: "Blog 1"
|
35
|
+
blog.posts.create! title: "Post 1", author_first_name: "John", author_last_name: "Doe"
|
36
|
+
blog.posts.create! title: "Post 2", author_first_name: "Maria", author_last_name: "Doe"
|
37
|
+
blog = Blog.create! title: "Blog 2"
|
38
|
+
|
39
|
+
serializer = BlogsAndPostsSerializer.new(Blog.all, Post.all)
|
40
|
+
|
41
|
+
assert_equal({"blogs"=>[{"id"=>1, "title"=>"Blog 1", "posts"=>[{"id"=>1}, {"id"=>2}]}, {"id"=>2, "title"=>"Blog 2", "posts"=>[]}], "posts"=>[{"id"=>1, "title"=>"Post 1"}, {"id"=>2, "title"=>"Post 2"}]},
|
42
|
+
serializer.as_json)
|
43
|
+
end
|
44
|
+
|
45
|
+
def teardown
|
46
|
+
clean_db
|
47
|
+
end
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: datasource
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jan Berdajs
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-11-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.9'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.9'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- mrbrdo@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- MIT-LICENSE
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- lib/datasource.rb
|
52
|
+
- lib/datasource/adapters/active_record.rb
|
53
|
+
- lib/datasource/attributes/computed_attribute.rb
|
54
|
+
- lib/datasource/attributes/query_attribute.rb
|
55
|
+
- lib/datasource/base.rb
|
56
|
+
- lib/datasource/serializer/composite.rb
|
57
|
+
- test/active_record_helper.rb
|
58
|
+
- test/schema.rb
|
59
|
+
- test/test_datasource.rb
|
60
|
+
- test/test_helper.rb
|
61
|
+
- test/test_serializer_composite.rb
|
62
|
+
homepage: https://github.com/mrbrdo/datasource
|
63
|
+
licenses:
|
64
|
+
- MIT
|
65
|
+
metadata: {}
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
requirements: []
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 2.2.2
|
83
|
+
signing_key:
|
84
|
+
specification_version: 4
|
85
|
+
summary: Ruby library for creating data source objects from database data
|
86
|
+
test_files:
|
87
|
+
- test/active_record_helper.rb
|
88
|
+
- test/schema.rb
|
89
|
+
- test/test_datasource.rb
|
90
|
+
- test/test_helper.rb
|
91
|
+
- test/test_serializer_composite.rb
|