datasource 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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/adapters/active_record'
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
- 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
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
@@ -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
@@ -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.1
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-08 00:00:00.000000000 Z
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: :runtime
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/serializer/composite.rb
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/test_serializer_composite.rb
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/test_serializer_composite.rb
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
@@ -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