surus 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ module Surus
2
+ module JSON
3
+ module Model
4
+ def find_json(id, options={})
5
+ sql = RowQuery.new(where(id: id), options).to_sql
6
+ connection.select_value sql
7
+ end
8
+
9
+ def all_json(options={})
10
+ sql = ArrayAggQuery.new(scoped, options).to_sql
11
+ connection.select_value sql
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ ActiveRecord::Base.extend Surus::JSON::Model
@@ -0,0 +1,78 @@
1
+ module Surus
2
+ module JSON
3
+ class Query
4
+ attr_reader :original_scope
5
+ attr_reader :options
6
+
7
+ def initialize(original_scope, options={})
8
+ @original_scope = original_scope
9
+ @options = options
10
+ end
11
+
12
+ private
13
+ def klass
14
+ original_scope.klass
15
+ end
16
+
17
+ def subquery_sql
18
+ select(columns.map(&:to_s).join(', ')).to_sql
19
+ end
20
+
21
+ def columns
22
+ selected_columns + association_columns
23
+ end
24
+
25
+ def table_columns
26
+ klass.columns
27
+ end
28
+
29
+ def selected_columns
30
+ if options.key? :columns
31
+ options[:columns]
32
+ else
33
+ table_columns.map(&:name)
34
+ end
35
+ end
36
+
37
+ def association_columns
38
+ included_associations_name_and_options.map do |association_name, association_options|
39
+ association = klass.reflect_on_association association_name
40
+ subquery = case association.source_macro
41
+ when :belongs_to
42
+ association_scope = BelongsToScopeBuilder.new(original_scope, association).scope
43
+ RowQuery.new(association_scope, association_options).to_sql
44
+ when :has_many
45
+ association_scope = HasManyScopeBuilder.new(original_scope, association).scope
46
+ ArrayAggQuery.new(association_scope, association_options).to_sql
47
+ when :has_and_belongs_to_many
48
+ association_scope = HasAndBelongsToManyScopeBuilder.new(original_scope, association).scope
49
+ ArrayAggQuery.new(association_scope, association_options).to_sql
50
+ end
51
+ "(#{subquery}) #{association_name}"
52
+ end
53
+ end
54
+
55
+ def included_associations_name_and_options
56
+ _include = options[:include]
57
+ if _include.nil?
58
+ {}
59
+ elsif _include.kind_of?(::Hash)
60
+ _include
61
+ elsif _include.kind_of?(::Array)
62
+ _include.each_with_object({}) do |e, hash|
63
+ if e.kind_of?(Hash)
64
+ hash.merge!(e)
65
+ else
66
+ hash[e] = {}
67
+ end
68
+ end
69
+ else
70
+ {_include => {}}
71
+ end
72
+ end
73
+
74
+ delegate :connection, :quoted_table_name, to: :klass
75
+ delegate :select, to: :original_scope
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,9 @@
1
+ module Surus
2
+ module JSON
3
+ class RowQuery < Query
4
+ def to_sql
5
+ "select row_to_json(t) from (#{subquery_sql}) t"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,3 +1,3 @@
1
1
  module Surus
2
- VERSION = "0.3.2"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -7,17 +7,17 @@ describe Surus::Array::DecimalSerializer do
7
7
  [[BigDecimal("0.0")], "single element"],
8
8
  [[BigDecimal("1.0"), BigDecimal("2.0")], "multiple elements"],
9
9
  [[BigDecimal("-1.0"), BigDecimal("-2.0")], "negative element"],
10
- [[BigDecimal("1232493289348929843.323422349274382923")], "high magnitude element"],
10
+ [[BigDecimal("1232493289348929843.323422349274382923")], "high magnitude element"],
11
11
  [[BigDecimal("1.0"), BigDecimal("2.0"), BigDecimal("2.0")], "duplicated elements"],
12
12
  [[BigDecimal("1.0"), nil], "an element is nil"],
13
13
  [(10_000.times.map { |n| n * BigDecimal("1.13312") }).to_a, "huge array"]
14
14
  ]
15
-
15
+
16
16
  round_trip_examples.each do |value, description|
17
17
  it "round trips when #{description}" do
18
18
  r = DecimalArrayRecord.create! :decimals => value
19
19
  r.reload
20
- r.decimals.should == value
21
- end
20
+ expect(r.decimals).to eq(value)
21
+ end
22
22
  end
23
23
  end
@@ -7,17 +7,17 @@ describe Surus::Array::FloatSerializer do
7
7
  [[0.0], "single element"],
8
8
  [[1.0, 2.0], "multiple elements"],
9
9
  [[-1.0, -2.0], "negative element"],
10
- [[4.4325349e+45, 1.2324323e+77, 1.1242342e-57, 3e99], "high magnitude elements"],
10
+ [[4.4325349e+45, 1.2324323e+77, 1.1242342e-57, 3e99], "high magnitude elements"],
11
11
  [[1.0, 2.0, 2.0, 2.0], "duplicated elements"],
12
12
  [[1.0, nil], "an element is nil"],
13
13
  [(10_000.times.map { |n| n * 1.0 }).to_a, "huge array"]
14
14
  ]
15
-
15
+
16
16
  round_trip_examples.each do |value, description|
17
17
  it "round trips when #{description}" do
18
18
  r = FloatArrayRecord.create! :floats => value
19
19
  r.reload
20
- r.floats.should == value
21
- end
20
+ expect(r.floats).to eq(value)
21
+ end
22
22
  end
23
23
  end
@@ -11,12 +11,12 @@ describe Surus::Array::IntegerSerializer do
11
11
  [[1, nil], "an element is nil"],
12
12
  [(1..10_000).to_a, "huge array"]
13
13
  ]
14
-
14
+
15
15
  round_trip_examples.each do |value, description|
16
16
  it "round trips when #{description}" do
17
17
  r = IntegerArrayRecord.create! :integers => value
18
18
  r.reload
19
- r.integers.should == value
20
- end
19
+ expect(r.integers).to eq(value)
20
+ end
21
21
  end
22
22
  end
@@ -18,12 +18,12 @@ describe Surus::Array::TextSerializer do
18
18
  [[%q~foo \\ / " ; ' ( ) {}bar \\'~, "bar"], "an element many special characters"],
19
19
  [("aaa".."zzz").to_a, "huge array"]
20
20
  ]
21
-
21
+
22
22
  round_trip_examples.each do |value, description|
23
23
  it "round trips when #{description}" do
24
24
  r = TextArrayRecord.create! :texts => value
25
25
  r.reload
26
- r.texts.should == value
27
- end
26
+ expect(r.texts).to eq(value)
27
+ end
28
28
  end
29
29
  end
@@ -36,9 +36,59 @@ CREATE TABLE float_array_records(
36
36
 
37
37
 
38
38
 
39
- DROP TABLE IF EXISTS float_array_records;
39
+ DROP TABLE IF EXISTS decimal_array_records;
40
40
 
41
41
  CREATE TABLE decimal_array_records(
42
42
  id serial PRIMARY KEY,
43
43
  decimals decimal[]
44
+ );
45
+
46
+
47
+
48
+ DROP TABLE IF EXISTS users CASCADE;
49
+
50
+ CREATE TABLE users(
51
+ id serial PRIMARY KEY,
52
+ name varchar NOT NULL,
53
+ email varchar NOT NULL
54
+ );
55
+
56
+
57
+
58
+ DROP TABLE IF EXISTS forums CASCADE;
59
+
60
+ CREATE TABLE forums(
61
+ id serial PRIMARY KEY,
62
+ name varchar NOT NULL
63
+ );
64
+
65
+
66
+
67
+ DROP TABLE IF EXISTS posts CASCADE;
68
+
69
+ CREATE TABLE posts(
70
+ id serial PRIMARY KEY,
71
+ forum_id integer NOT NULL REFERENCES forums,
72
+ author_id integer NOT NULL REFERENCES users,
73
+ subject varchar NOT NULL,
74
+ body varchar NOT NULL
75
+ );
76
+
77
+
78
+
79
+ DROP TABLE IF EXISTS tags CASCADE;
80
+
81
+ CREATE TABLE tags(
82
+ id serial PRIMARY KEY,
83
+ name varchar NOT NULL UNIQUE
84
+ );
85
+
86
+
87
+
88
+ DROP TABLE IF EXISTS posts_tags CASCADE;
89
+
90
+ CREATE TABLE posts_tags(
91
+ post_id integer NOT NULL REFERENCES posts,
92
+ tag_id integer NOT NULL REFERENCES tags,
93
+ PRIMARY KEY (post_id, tag_id)
44
94
  );
@@ -0,0 +1,21 @@
1
+ FactoryGirl.define do
2
+ factory :forum do
3
+ name { Faker::Lorem.sentence }
4
+ end
5
+
6
+ factory :post do
7
+ forum
8
+ author
9
+ subject { Faker::Lorem.sentence }
10
+ body { Faker::Lorem.paragraph }
11
+ end
12
+
13
+ factory :tag do
14
+ sequence(:name) { |n| "#{Faker::Lorem.word} - #{n}" }
15
+ end
16
+
17
+ factory :user, aliases: [:author] do
18
+ name { Faker::Internet.user_name }
19
+ email { Faker::Internet.email }
20
+ end
21
+ end
@@ -8,7 +8,7 @@ describe Surus::Hstore::Serializer do
8
8
  [{"foo" => "bar", "baz" => "quz"}, "multiple key/value pairs"],
9
9
  [{"foo" => nil}, "value is nil"],
10
10
  ]
11
-
11
+
12
12
  [
13
13
  ['"', 'double quote (")'],
14
14
  ["'", "single quote (')"],
@@ -22,13 +22,13 @@ describe Surus::Hstore::Serializer do
22
22
  round_trip_examples << [{"foo#{value}foo" => "bar"}, "key with #{description} in middle"]
23
23
  round_trip_examples << [{"foo#{value}" => "bar"}, "key with #{description} at end"]
24
24
  round_trip_examples << [{value => "bar"}, "key is #{description}"]
25
-
25
+
26
26
  round_trip_examples << [{"foo" => "#{value}bar"}, "value with #{description} at beginning"]
27
27
  round_trip_examples << [{"foo" => "bar#{value}bar"}, "value with #{description} in middle"]
28
28
  round_trip_examples << [{"foo" => "bar#{value}"}, "value with #{description} at end"]
29
29
  round_trip_examples << [{"foo" => value}, "value is #{description}"]
30
30
  end
31
-
31
+
32
32
  [
33
33
  [:foo, "symbol"],
34
34
  [0, "integer 0"],
@@ -55,12 +55,12 @@ describe Surus::Hstore::Serializer do
55
55
  round_trip_examples << [{value => "bar"}, "key is #{description}"]
56
56
  round_trip_examples << [{value => value}, "key and value are each #{description}"]
57
57
  end
58
-
58
+
59
59
  round_trip_examples.each do |value, description|
60
60
  it "round trips when #{description}" do
61
61
  r = HstoreRecord.create! :properties => value
62
62
  r.reload
63
- r.properties.should == value
64
- end
63
+ expect(r.properties).to eq(value)
64
+ end
65
65
  end
66
66
  end
@@ -0,0 +1,153 @@
1
+ require 'spec_helper'
2
+ require 'oj'
3
+ ActiveRecord::Base.include_root_in_json = false
4
+
5
+ describe 'json' do
6
+ describe 'find_json' do
7
+ context 'with only id parameter' do
8
+ it 'is entire row as json' do
9
+ user = FactoryGirl.create :user
10
+ to_json = Oj.load user.to_json
11
+ find_json = Oj.load User.find_json(user.id)
12
+ expect(find_json).to eq(to_json)
13
+ end
14
+ end
15
+
16
+ context 'with columns parameter' do
17
+ it 'is selected row columns as json' do
18
+ user = FactoryGirl.create :user
19
+ to_json = Oj.load user.to_json only: [:id, :name]
20
+ find_json = Oj.load User.find_json(user.id, columns: [:id, :name])
21
+ expect(find_json).to eq(to_json)
22
+ end
23
+ end
24
+
25
+ context 'with includes option' do
26
+ it 'includes entire belongs_to object' do
27
+ post = FactoryGirl.create :post
28
+ to_json = Oj.load post.to_json(include: :author)
29
+ find_json = Oj.load Post.find_json(post.id, include: :author)
30
+ expect(find_json).to eq(to_json)
31
+ end
32
+
33
+ it 'filters by belongs_to conditions' do
34
+ post = FactoryGirl.create :post
35
+ find_json = Oj.load Post.find_json(post.id, include: :forum_with_impossible_conditions)
36
+ expect(find_json.fetch('forum_with_impossible_conditions')).to be_nil
37
+ end
38
+
39
+ it 'includes multiple entire belongs_to objects' do
40
+ post = FactoryGirl.create :post
41
+ to_json = Oj.load post.to_json(include: [:author, :forum])
42
+ find_json = Oj.load Post.find_json(post.id, include: [:author, :forum])
43
+ expect(find_json).to eq(to_json)
44
+ end
45
+
46
+ it 'includes only selected columns of belongs_to object' do
47
+ post = FactoryGirl.create :post
48
+ to_json = Oj.load post.to_json(include: {author: {only: [:id, :name]}})
49
+ find_json = Oj.load Post.find_json(post.id, include: {author: {columns: [:id, :name]}})
50
+ expect(find_json).to eq(to_json)
51
+ end
52
+
53
+ it 'includes entire has_many association' do
54
+ user = FactoryGirl.create :user
55
+ posts = FactoryGirl.create_list :post, 2, author: user
56
+ user.reload
57
+ to_json = Oj.load user.to_json(include: :posts)
58
+ find_json = Oj.load User.find_json(user.id, include: :posts)
59
+ expect(find_json).to eq(to_json)
60
+ end
61
+
62
+ it 'preserves has_many order' do
63
+ user = FactoryGirl.create :user
64
+ posts = FactoryGirl.create_list :post, 2, author: user
65
+ user.reload
66
+ to_json = Oj.load user.to_json(include: :posts_with_order)
67
+ find_json = Oj.load User.find_json(user.id, include: :posts_with_order)
68
+ expect(find_json).to eq(to_json)
69
+ end
70
+
71
+ it 'filters by has_many conditions' do
72
+ user = FactoryGirl.create :user
73
+ FactoryGirl.create :post, author: user, subject: 'foo'
74
+ FactoryGirl.create :post, author: user
75
+ user.reload
76
+ to_json = Oj.load user.to_json(include: :posts_with_conditions)
77
+ find_json = Oj.load User.find_json(user.id, include: :posts_with_conditions)
78
+ expect(find_json).to eq(to_json)
79
+ end
80
+
81
+ it 'includes only select columns of has_many association' do
82
+ user = FactoryGirl.create :user
83
+ posts = FactoryGirl.create_list :post, 2, author: user
84
+ user.reload
85
+ to_json = Oj.load user.to_json(include: {posts: {only: [:id, :subject]}})
86
+ find_json = Oj.load User.find_json(user.id, include: {posts: {columns: [:id, :subject]}})
87
+ expect(find_json).to eq(to_json)
88
+ end
89
+
90
+ it 'includes empty array for empty has_many association' do
91
+ user = FactoryGirl.create :user
92
+ to_json = Oj.load user.to_json(include: :posts)
93
+ find_json = Oj.load User.find_json(user.id, include: :posts)
94
+ expect(find_json).to eq(to_json)
95
+ end
96
+
97
+ it 'includes entire has_and_belongs_to_many association' do
98
+ post = FactoryGirl.create :post
99
+ tag = FactoryGirl.create :tag
100
+ post.tags << tag
101
+ to_json = Oj.load post.to_json(include: :tags)
102
+ find_json = Oj.load Post.find_json(post.id, include: :tags)
103
+ expect(find_json).to eq(to_json)
104
+ end
105
+
106
+ it 'excludes other has_and_belongs_to_many association records' do
107
+ post = FactoryGirl.create :post
108
+ tag = FactoryGirl.create :tag
109
+ post.tags << tag
110
+ other_post = FactoryGirl.create :post
111
+ other_tag = FactoryGirl.create :tag
112
+ other_post.tags << other_tag
113
+ to_json = Oj.load post.to_json(include: :tags)
114
+ find_json = Oj.load Post.find_json(post.id, include: :tags)
115
+ expect(find_json).to eq(to_json)
116
+ end
117
+
118
+ it 'includes empty array for empty has_and_belongs_to_many' do
119
+ post = FactoryGirl.create :post
120
+ to_json = Oj.load post.to_json(include: :tags)
121
+ find_json = Oj.load Post.find_json(post.id, include: :tags)
122
+ expect(find_json).to eq(to_json)
123
+ end
124
+
125
+ it 'includes nested associations' do
126
+ user = FactoryGirl.create :user
127
+ post = FactoryGirl.create :post, author: user
128
+ user.reload
129
+ to_json = Oj.load user.to_json(include: {posts: {include: :forum}})
130
+ find_json = Oj.load User.find_json(user.id, include: {posts: {include: :forum}})
131
+ expect(find_json).to eq(to_json)
132
+ end
133
+
134
+ it 'includes nested associations with columns' do
135
+ user = FactoryGirl.create :user
136
+ post = FactoryGirl.create :post, author: user
137
+ user.reload
138
+ to_json = Oj.load user.to_json(include: {posts: {only: [:id], include: :forum}})
139
+ find_json = Oj.load User.find_json(user.id, include: {posts: {columns: [:id], include: :forum}})
140
+ expect(find_json).to eq(to_json)
141
+ end
142
+ end
143
+ end
144
+
145
+ describe 'all_json' do
146
+ it 'is all rows as array' do
147
+ users = FactoryGirl.create_list :user, 3
148
+ to_json = Oj.load users.to_json
149
+ all_json = Oj.load User.all_json
150
+ expect(all_json).to eq(to_json)
151
+ end
152
+ end
153
+ end