datasource 0.0.1 → 0.0.2

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.
@@ -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