datasource 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +211 -0
- data/lib/datasource/adapters/active_record.rb +188 -89
- data/lib/datasource/adapters/sequel.rb +200 -0
- data/lib/datasource/attributes/computed_attribute.rb +11 -15
- data/lib/datasource/attributes/loader.rb +65 -0
- data/lib/datasource/attributes/query_attribute.rb +8 -3
- data/lib/datasource/base.rb +144 -51
- data/lib/datasource/consumer_adapters/active_model_serializers.rb +66 -0
- data/lib/datasource/serializer.rb +117 -0
- data/lib/datasource.rb +21 -3
- data/lib/generators/datasource/install_generator.rb +8 -0
- data/lib/generators/datasource/templates/initializer.rb +11 -0
- data/test/active_record_helper.rb +13 -0
- data/test/schema.rb +1 -15
- data/test/test_helper.rb +4 -2
- data/test/test_loader.rb +79 -0
- data/test/test_scope.rb +50 -0
- data/test/test_serializer.rb +52 -0
- metadata +44 -8
- data/lib/datasource/serializer/composite.rb +0 -119
- data/test/test_datasource.rb +0 -47
- data/test/test_serializer_composite.rb +0 -48
@@ -0,0 +1,117 @@
|
|
1
|
+
module Datasource
|
2
|
+
class Serializer
|
3
|
+
TemplatePart = Struct.new(:parent, :type, :value, :select) do
|
4
|
+
def initialize(parent, type = nil, value = nil)
|
5
|
+
super(parent, type, get_default_value(type, value), [])
|
6
|
+
end
|
7
|
+
|
8
|
+
def set_default_value(value = nil)
|
9
|
+
self.value = get_default_value(type, value)
|
10
|
+
end
|
11
|
+
private
|
12
|
+
def get_default_value(type, value)
|
13
|
+
case type
|
14
|
+
when :hash then {}
|
15
|
+
when :array then []
|
16
|
+
when :datasource then value
|
17
|
+
when nil then nil
|
18
|
+
else
|
19
|
+
fail "Unknown type #{type}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
attr_accessor :datasource_count, :template
|
26
|
+
|
27
|
+
def inherited(base)
|
28
|
+
base.datasource_count = 0
|
29
|
+
@cursor = base.template = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def with_new_cursor(type, value = nil, &block)
|
33
|
+
result = nil
|
34
|
+
new_cursor = TemplatePart.new(@cursor, type, value)
|
35
|
+
@cursor = @cursor.tap do
|
36
|
+
if template.nil?
|
37
|
+
self.template = @cursor = new_cursor
|
38
|
+
elsif @cursor.type == :datasource
|
39
|
+
@cursor = @cursor.parent
|
40
|
+
return with_new_cursor(type, value, &block)
|
41
|
+
elsif @cursor.type == :array
|
42
|
+
@cursor = new_cursor
|
43
|
+
@cursor.parent.push(@cursor)
|
44
|
+
elsif @cursor.type.nil?
|
45
|
+
# replace cursor
|
46
|
+
@cursor.type = type
|
47
|
+
@cursor.set_default_value(value)
|
48
|
+
else
|
49
|
+
fail "Invalid use of #{type}."
|
50
|
+
end
|
51
|
+
result = block.call
|
52
|
+
end
|
53
|
+
result
|
54
|
+
end
|
55
|
+
|
56
|
+
def hash(&block)
|
57
|
+
with_new_cursor(:hash, &block)
|
58
|
+
end
|
59
|
+
|
60
|
+
def key(name)
|
61
|
+
fail "Cannot use key outside hash." unless template && @cursor.type == :hash
|
62
|
+
@cursor = @cursor.tap do
|
63
|
+
@cursor = @cursor.value[name.to_s] = TemplatePart.new(@cursor)
|
64
|
+
yield
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def array(&block)
|
69
|
+
with_new_cursor(:array, &block)
|
70
|
+
end
|
71
|
+
|
72
|
+
def datasource(ds)
|
73
|
+
self.datasource_count += 1
|
74
|
+
@cursor = with_new_cursor(:datasource, ds) { @cursor }
|
75
|
+
end
|
76
|
+
|
77
|
+
def attributes(*attributes)
|
78
|
+
attributes.each { |name| attribute name }
|
79
|
+
end
|
80
|
+
|
81
|
+
def attribute(name)
|
82
|
+
fail "No datasource selected - use \"select_datasource Klass\" first." unless template && @cursor.type == :datasource
|
83
|
+
@cursor.select << name
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def initialize(*scopes)
|
88
|
+
@scopes = scopes
|
89
|
+
if @scopes.size != self.class.datasource_count
|
90
|
+
fail ArgumentError, "#{self.class.name} needs #{self.class.datasource_count} scopes, you provided #{@scopes.size}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def as_json
|
95
|
+
parse_template_part(self.class.template)
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def parse_template_part(part)
|
100
|
+
return nil unless part
|
101
|
+
case part.type
|
102
|
+
when :hash
|
103
|
+
{}.tap do |result|
|
104
|
+
part.value.each_pair do |k, v|
|
105
|
+
result[k] = parse_template_part(v)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
when :array
|
109
|
+
part.value.map { |v| parse_template_part(v) }
|
110
|
+
when :datasource
|
111
|
+
part.value.new(@scopes.shift).select(*part.select).results
|
112
|
+
else
|
113
|
+
fail "Unknown type #{type}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/datasource.rb
CHANGED
@@ -1,8 +1,26 @@
|
|
1
|
+
module Datasource
|
2
|
+
Error = Class.new(StandardError)
|
3
|
+
RecursionError = Class.new(StandardError)
|
4
|
+
|
5
|
+
def self.load(adapter = nil)
|
6
|
+
unless adapter
|
7
|
+
adapter = if defined? ActiveRecord
|
8
|
+
:activerecord
|
9
|
+
elsif defined? Sequel
|
10
|
+
:sequel
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'datasource/adapters/active_record' if [:activerecord, :active_record].include?(adapter)
|
15
|
+
require 'datasource/adapters/sequel' if adapter == :sequel
|
16
|
+
require 'datasource/consumer_adapters/active_model_serializers' if defined? ActiveModel::Serializer
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
1
20
|
require 'datasource/base'
|
2
21
|
|
3
22
|
require 'datasource/attributes/computed_attribute'
|
4
23
|
require 'datasource/attributes/query_attribute'
|
24
|
+
require 'datasource/attributes/loader'
|
5
25
|
|
6
|
-
require 'datasource/
|
7
|
-
|
8
|
-
require 'datasource/serializer/composite'
|
26
|
+
require 'datasource/serializer'
|
@@ -0,0 +1,8 @@
|
|
1
|
+
class Datasource::InstallGenerator < ::Rails::Generators::Base
|
2
|
+
source_root File.expand_path("../templates", __FILE__)
|
3
|
+
desc 'Creates a Datasource Rails initializer'
|
4
|
+
|
5
|
+
def create_serializer_file
|
6
|
+
template 'initializer.rb', 'config/initializers/datasource.rb'
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
Datasource.load(:activerecord)
|
2
|
+
|
3
|
+
if defined? ActiveModel::Serializer
|
4
|
+
if ActiveModel::Serializer.respond_to?(:config)
|
5
|
+
ActiveModel::Serializer.config.array_serializer = Datasource::ArrayAMS
|
6
|
+
else
|
7
|
+
ActiveModel::Serializer.setup do |config|
|
8
|
+
config.array_serializer = Datasource::ArrayAMS
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -8,3 +8,16 @@ def clean_db
|
|
8
8
|
ActiveRecord::Base.connection.execute("DELETE FROM blogs")
|
9
9
|
ActiveRecord::Base.connection.execute("DELETE FROM sqlite_sequence")
|
10
10
|
end
|
11
|
+
|
12
|
+
def assert_query_count(count)
|
13
|
+
old_logger = ActiveRecord::Base.logger
|
14
|
+
logger = StringIO.new
|
15
|
+
ActiveRecord::Base.logger = Logger.new(logger)
|
16
|
+
begin
|
17
|
+
yield
|
18
|
+
ensure
|
19
|
+
ActiveRecord::Base.logger = old_logger
|
20
|
+
# puts logger.string
|
21
|
+
end
|
22
|
+
assert_equal count, logger.string.lines.count
|
23
|
+
end
|
data/test/schema.rb
CHANGED
@@ -16,18 +16,4 @@ ActiveRecord::Schema.define(:version => 0) do
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
|
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
|
19
|
+
ActiveRecord::Base.send :include, Datasource::Adapters::ActiveRecord::Model
|
data/test/test_helper.rb
CHANGED
@@ -5,8 +5,10 @@ require 'minitest/autorun'
|
|
5
5
|
require 'active_support/all'
|
6
6
|
require 'active_record'
|
7
7
|
require 'datasource'
|
8
|
+
require 'active_model_serializers'
|
9
|
+
require 'pry'
|
10
|
+
|
11
|
+
Datasource.load(:activerecord)
|
8
12
|
|
9
13
|
# Load support files
|
10
14
|
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
11
|
-
|
12
|
-
# ActiveRecord::Base.logger = Logger.new STDOUT
|
data/test/test_loader.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'active_record_helper'
|
3
|
+
|
4
|
+
class LoaderTest < ActiveSupport::TestCase
|
5
|
+
class Comment < ActiveRecord::Base
|
6
|
+
self.table_name = "comments"
|
7
|
+
belongs_to :post
|
8
|
+
end
|
9
|
+
|
10
|
+
class Post < ActiveRecord::Base
|
11
|
+
self.table_name = "posts"
|
12
|
+
has_many :comments
|
13
|
+
|
14
|
+
datasource_module do
|
15
|
+
loader :newest_comment, group_by: :post_id, one: true do |post_ids|
|
16
|
+
Comment.for_serializer.where(post_id: post_ids)
|
17
|
+
.group("post_id")
|
18
|
+
.having("id = MAX(id)")
|
19
|
+
.datasource_select(:post_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
loader :newest_comment_text, array_to_hash: true do |post_ids|
|
23
|
+
Comment.where(post_id: post_ids)
|
24
|
+
.group("post_id")
|
25
|
+
.having("id = MAX(id)")
|
26
|
+
.pluck("post_id, comment")
|
27
|
+
end
|
28
|
+
|
29
|
+
loader :ordered_comments, group_by: :post_id do |post_ids|
|
30
|
+
Comment.for_serializer(CommentSerializer).where(post_id: post_ids)
|
31
|
+
.order("post_id, id desc")
|
32
|
+
.datasource_select(:post_id)
|
33
|
+
end
|
34
|
+
|
35
|
+
computed :newest_comment, loaders: :newest_comment
|
36
|
+
computed :newest_comment_text, loaders: :newest_comment_text
|
37
|
+
computed :ordered_comments, loaders: :ordered_comments
|
38
|
+
end
|
39
|
+
|
40
|
+
def name_initials
|
41
|
+
return unless author_first_name && author_last_name
|
42
|
+
author_first_name[0].upcase + author_last_name[0].upcase
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class CommentSerializer < ActiveModel::Serializer
|
47
|
+
attributes :id, :comment
|
48
|
+
end
|
49
|
+
|
50
|
+
class PostSerializer < ActiveModel::Serializer
|
51
|
+
attributes :id, :title, :newest_comment, :newest_comment_text, :ordered_comments
|
52
|
+
|
53
|
+
def newest_comment
|
54
|
+
CommentSerializer.new(object.loaded_values[:newest_comment]).as_json
|
55
|
+
end
|
56
|
+
|
57
|
+
def newest_comment_text
|
58
|
+
object.loaded_values[:newest_comment_text]
|
59
|
+
end
|
60
|
+
|
61
|
+
def ordered_comments
|
62
|
+
Datasource::ArrayAMS.new(object.loaded_values[:ordered_comments]).as_json
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_loader
|
67
|
+
post = Post.create! title: "First Post"
|
68
|
+
2.times { |i| post.comments.create! comment: "Comment #{i+1}" }
|
69
|
+
|
70
|
+
assert_query_count(4) do
|
71
|
+
assert_equal Datasource::ArrayAMS.new(Post.all).as_json,
|
72
|
+
[{:id=>1, :title=>"First Post", :newest_comment=>{"comment"=>{:id=>2, :comment=>"Comment 2"}}, :newest_comment_text=>"Comment 2", :ordered_comments=>[{:id=>2, :comment=>"Comment 2"}, {:id=>1, :comment=>"Comment 1"}]}]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def teardown
|
77
|
+
clean_db
|
78
|
+
end
|
79
|
+
end
|
data/test/test_scope.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'active_record_helper'
|
3
|
+
|
4
|
+
class ScopeTest < ActiveSupport::TestCase
|
5
|
+
class Post < ActiveRecord::Base
|
6
|
+
belongs_to :blog
|
7
|
+
|
8
|
+
datasource_module do
|
9
|
+
query :author_name do
|
10
|
+
"posts.author_first_name || ' ' || posts.author_last_name"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class PostSerializer < ActiveModel::Serializer
|
16
|
+
attributes :id, :title, :author_name
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_first
|
20
|
+
Post.create! title: "The Post", author_first_name: "John", author_last_name: "Doe", blog_id: 10
|
21
|
+
post = Post.for_serializer.first
|
22
|
+
|
23
|
+
assert_equal("The Post", post.title)
|
24
|
+
assert_equal("John Doe", post.author_name)
|
25
|
+
assert_raises(ActiveModel::MissingAttributeError) { post.blog_id }
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_find
|
29
|
+
post = Post.create! title: "The Post", author_first_name: "John", author_last_name: "Doe", blog_id: 10
|
30
|
+
post = Post.for_serializer.find(post.id)
|
31
|
+
|
32
|
+
assert_equal("The Post", post.title)
|
33
|
+
assert_equal("John Doe", post.author_name)
|
34
|
+
assert_raises(ActiveModel::MissingAttributeError) { post.blog_id }
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_each
|
38
|
+
post = Post.create! title: "The Post", author_first_name: "John", author_last_name: "Doe", blog_id: 10
|
39
|
+
|
40
|
+
Post.for_serializer.each do |post|
|
41
|
+
assert_equal("The Post", post.title)
|
42
|
+
assert_equal("John Doe", post.author_name)
|
43
|
+
assert_raises(ActiveModel::MissingAttributeError) { post.blog_id }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def teardown
|
48
|
+
clean_db
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'active_record_helper'
|
3
|
+
|
4
|
+
class SerializerTest < ActiveSupport::TestCase
|
5
|
+
class Post < ActiveRecord::Base
|
6
|
+
belongs_to :blog
|
7
|
+
|
8
|
+
datasource_module do
|
9
|
+
query :author_name do
|
10
|
+
"posts.author_first_name || ' ' || posts.author_last_name"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Blog < ActiveRecord::Base
|
16
|
+
has_many :posts
|
17
|
+
end
|
18
|
+
|
19
|
+
class BlogSerializer < ActiveModel::Serializer
|
20
|
+
attributes :id, :title
|
21
|
+
|
22
|
+
has_many :posts
|
23
|
+
end
|
24
|
+
|
25
|
+
class PostSerializer < ActiveModel::Serializer
|
26
|
+
attributes :id, :title, :author_name
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_blogs_and_posts_serializer
|
30
|
+
blog = Blog.create! title: "Blog 1"
|
31
|
+
blog.posts.create! title: "Post 1", author_first_name: "John", author_last_name: "Doe"
|
32
|
+
blog.posts.create! title: "Post 2", author_first_name: "Maria", author_last_name: "Doe"
|
33
|
+
blog = Blog.create! title: "Blog 2"
|
34
|
+
|
35
|
+
expected_result = [
|
36
|
+
{:id =>1, :title =>"Blog 1", :posts =>[
|
37
|
+
{:id =>1, :title =>"Post 1", :author_name =>"John Doe"},
|
38
|
+
{:id =>2, :title =>"Post 2", :author_name =>"Maria Doe"}
|
39
|
+
]},
|
40
|
+
{:id =>2, :title =>"Blog 2", :posts =>[]}
|
41
|
+
]
|
42
|
+
|
43
|
+
assert_query_count(2) do
|
44
|
+
serializer = Datasource::ArrayAMS.new(Blog.all)
|
45
|
+
assert_equal(expected_result, serializer.as_json)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def teardown
|
50
|
+
clean_db
|
51
|
+
end
|
52
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: datasource
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jan Berdajs
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-12-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: active_model_serializers
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.9'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.9'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: activerecord
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -17,7 +31,7 @@ dependencies:
|
|
17
31
|
- - "~>"
|
18
32
|
- !ruby/object:Gem::Version
|
19
33
|
version: '4'
|
20
|
-
type: :
|
34
|
+
type: :development
|
21
35
|
prerelease: false
|
22
36
|
version_requirements: !ruby/object:Gem::Requirement
|
23
37
|
requirements:
|
@@ -38,6 +52,20 @@ dependencies:
|
|
38
52
|
- - "~>"
|
39
53
|
- !ruby/object:Gem::Version
|
40
54
|
version: '0.9'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4'
|
41
69
|
description:
|
42
70
|
email:
|
43
71
|
- mrbrdo@gmail.com
|
@@ -50,15 +78,21 @@ files:
|
|
50
78
|
- Rakefile
|
51
79
|
- lib/datasource.rb
|
52
80
|
- lib/datasource/adapters/active_record.rb
|
81
|
+
- lib/datasource/adapters/sequel.rb
|
53
82
|
- lib/datasource/attributes/computed_attribute.rb
|
83
|
+
- lib/datasource/attributes/loader.rb
|
54
84
|
- lib/datasource/attributes/query_attribute.rb
|
55
85
|
- lib/datasource/base.rb
|
56
|
-
- lib/datasource/
|
86
|
+
- lib/datasource/consumer_adapters/active_model_serializers.rb
|
87
|
+
- lib/datasource/serializer.rb
|
88
|
+
- lib/generators/datasource/install_generator.rb
|
89
|
+
- lib/generators/datasource/templates/initializer.rb
|
57
90
|
- test/active_record_helper.rb
|
58
91
|
- test/schema.rb
|
59
|
-
- test/test_datasource.rb
|
60
92
|
- test/test_helper.rb
|
61
|
-
- test/
|
93
|
+
- test/test_loader.rb
|
94
|
+
- test/test_scope.rb
|
95
|
+
- test/test_serializer.rb
|
62
96
|
homepage: https://github.com/mrbrdo/datasource
|
63
97
|
licenses:
|
64
98
|
- MIT
|
@@ -86,6 +120,8 @@ summary: Ruby library for creating data source objects from database data
|
|
86
120
|
test_files:
|
87
121
|
- test/active_record_helper.rb
|
88
122
|
- test/schema.rb
|
89
|
-
- test/test_datasource.rb
|
90
123
|
- test/test_helper.rb
|
91
|
-
- test/
|
124
|
+
- test/test_loader.rb
|
125
|
+
- test/test_scope.rb
|
126
|
+
- test/test_serializer.rb
|
127
|
+
has_rdoc:
|
@@ -1,119 +0,0 @@
|
|
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/test/test_datasource.rb
DELETED
@@ -1,47 +0,0 @@
|
|
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
|