superstore 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -13
  3. data/CHANGELOG.md +16 -0
  4. data/Gemfile +0 -5
  5. data/README.md +15 -33
  6. data/lib/superstore/adapters/jsonb_adapter.rb +245 -0
  7. data/lib/superstore/associations/association.rb +38 -0
  8. data/lib/superstore/associations/belongs_to.rb +35 -0
  9. data/lib/superstore/associations/builder/association.rb +38 -0
  10. data/lib/superstore/associations/builder/belongs_to.rb +7 -0
  11. data/lib/superstore/associations/builder/has_many.rb +7 -0
  12. data/lib/superstore/associations/builder/has_one.rb +7 -0
  13. data/lib/superstore/associations/has_many.rb +26 -0
  14. data/lib/superstore/associations/has_one.rb +24 -0
  15. data/lib/superstore/associations/reflection.rb +65 -0
  16. data/lib/superstore/associations.rb +72 -0
  17. data/lib/superstore/attribute_methods/definition.rb +5 -10
  18. data/lib/superstore/attribute_methods/dirty.rb +12 -2
  19. data/lib/superstore/attribute_methods/typecasting.rb +6 -12
  20. data/lib/superstore/base.rb +3 -4
  21. data/lib/superstore/connection.rb +3 -5
  22. data/lib/superstore/core.rb +0 -5
  23. data/lib/superstore/model.rb +32 -33
  24. data/lib/superstore/persistence.rb +4 -10
  25. data/lib/superstore/railtie.rb +2 -20
  26. data/lib/superstore/scope/batches.rb +17 -22
  27. data/lib/superstore/scope/finder_methods.rb +33 -35
  28. data/lib/superstore/scope/query_methods.rb +38 -44
  29. data/lib/superstore/scope.rb +24 -0
  30. data/lib/superstore/type.rb +3 -3
  31. data/lib/superstore/types/array_type.rb +2 -9
  32. data/lib/superstore/types/base_type.rb +4 -7
  33. data/lib/superstore/types/boolean_type.rb +2 -1
  34. data/lib/superstore/types/float_type.rb +6 -5
  35. data/lib/superstore/types/integer_type.rb +3 -3
  36. data/lib/superstore/types/json_type.rb +0 -21
  37. data/lib/superstore.rb +16 -5
  38. data/superstore.gemspec +2 -1
  39. data/test/support/jsonb.rb +8 -0
  40. data/test/support/{issue.rb → models.rb} +9 -0
  41. data/test/support/pg.rb +11 -15
  42. data/test/test_helper.rb +7 -6
  43. data/test/unit/{belongs_to_test.rb → associations/belongs_to_test.rb} +1 -10
  44. data/test/unit/associations/has_many_test.rb +13 -0
  45. data/test/unit/associations/has_one_test.rb +14 -0
  46. data/test/unit/{belongs_to → associations}/reflection_test.rb +2 -2
  47. data/test/unit/attribute_methods/definition_test.rb +6 -3
  48. data/test/unit/attribute_methods/dirty_test.rb +17 -14
  49. data/test/unit/attribute_methods/typecasting_test.rb +0 -14
  50. data/test/unit/base_test.rb +3 -3
  51. data/test/unit/connection_test.rb +0 -4
  52. data/test/unit/persistence_test.rb +4 -4
  53. data/test/unit/schema_test.rb +9 -17
  54. data/test/unit/scope/query_methods_test.rb +10 -1
  55. data/test/unit/types/array_type_test.rb +12 -10
  56. data/test/unit/types/base_type_test.rb +2 -10
  57. data/test/unit/types/boolean_type_test.rb +15 -13
  58. data/test/unit/types/date_type_test.rb +3 -3
  59. data/test/unit/types/float_type_test.rb +14 -7
  60. data/test/unit/types/integer_type_test.rb +11 -9
  61. data/test/unit/types/json_type_test.rb +0 -23
  62. data/test/unit/types/string_type_test.rb +6 -6
  63. data/test/unit/types/time_type_test.rb +7 -7
  64. metadata +35 -26
  65. data/CHANGELOG +0 -0
  66. data/lib/superstore/adapters/cassandra_adapter.rb +0 -203
  67. data/lib/superstore/adapters/hstore_adapter.rb +0 -170
  68. data/lib/superstore/belongs_to/association.rb +0 -65
  69. data/lib/superstore/belongs_to/builder.rb +0 -40
  70. data/lib/superstore/belongs_to/reflection.rb +0 -38
  71. data/lib/superstore/belongs_to.rb +0 -63
  72. data/lib/superstore/cassandra_schema/statements.rb +0 -52
  73. data/lib/superstore/cassandra_schema/tasks.rb +0 -47
  74. data/lib/superstore/cassandra_schema.rb +0 -9
  75. data/lib/superstore/log_subscriber.rb +0 -44
  76. data/lib/superstore/railties/controller_runtime.rb +0 -45
  77. data/lib/superstore/tasks/ks.rake +0 -59
  78. data/test/support/cassandra.rb +0 -46
  79. data/test/support/hstore.rb +0 -24
  80. data/test/support/user.rb +0 -2
  81. data/test/unit/cassandra_schema/statements_test.rb +0 -47
  82. data/test/unit/cassandra_schema/tasks_test.rb +0 -31
  83. data/test/unit/log_subscriber_test.rb +0 -26
  84. data/test/unit/railties/controller_runtime_test.rb +0 -48
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ed0f1d714b1ce3f33ed3fd382d6460dfb4e09024
4
- data.tar.gz: 643444b1b23de94b5f3e1ec08b787c40fcee253a
3
+ metadata.gz: 2e09c0c96fee4aa9bbe63b30515cea262876069d
4
+ data.tar.gz: f045d35fdb783ec088617d8b3e1fff9c32c26394
5
5
  SHA512:
6
- metadata.gz: 59e383fdb469396d92e243b51b0247ac8ff6184c373ce92a62ee0d32776c8d9ba77a23db139ac5b80988b4919ad5efd3fd38c5e351e6c44a4b48291bc394fff1
7
- data.tar.gz: 34effeb3db2d42fa94c2d4bece8b4bb5982f70251b8c7d1f0409ab7e86ae043e2ad6bf6d031a35edf06398012dae1bb944ef7ef72e185adce7d399fcdd92e465
6
+ metadata.gz: 7d479634ee3bf4b165a495038f19e8c0ba50ea2f4bbcba6674306d71d2cd3142266a22e4971de3af4c2af7241cbcfe2147425a0c4b2bcb76f6e2f6eaef64a9bd
7
+ data.tar.gz: 972a07eeb6a39fbd07a7cfef12203d23a34dd1a616e87434e8d5b3b5b1995872c237256d47165e7f3a48dfd55dd57b185bebd1d55982c76c9d6e59d1d60cf37f
data/.travis.yml CHANGED
@@ -1,16 +1,9 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
- - 2.0.0
5
- - 2.1.0
6
- - 2.2.0
3
+ - 2.0.0-p647
4
+ - 2.1.7
5
+ - 2.2.3
7
6
  sudo: false
8
- env:
9
- global:
10
- - CQLSH=/usr/local/cassandra/bin/cqlsh
11
- - KS_VERSION=1.2.19
12
- - KS_NAME=apache-cassandra-$KS_VERSION
13
- before_install:
14
- - if [[ ! -f $KS_NAME/conf/cassandra.yaml ]]; then wget http://archive.apache.org/dist/cassandra/$KS_VERSION/$KS_NAME-bin.tar.gz; tar xf $KS_NAME-bin.tar.gz; sh -c "echo 'JVM_OPTS=\"\${JVM_OPTS} -Xss256k -Djava.net.preferIPv4Stack=false\"' >> $KS_NAME/conf/cassandra-env.sh"; fi
15
- - sed -i -e 's:/var/.*\?/cassandra:/tmp/cassandra-store:g' $KS_NAME/conf/{cassandra.yaml,log4j-server.properties}
16
- - (cd $KS_NAME && ./bin/cassandra 2>&1 >> cassandra.log)
7
+ cache: bundler
8
+ addons:
9
+ postgresql: '9.4'
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # 2.0.0
2
+
3
+ Released YYYY-MM-DD.
4
+
5
+ ## New Features
6
+
7
+ * **Added Postgres JSONB adapter.** (#9)
8
+ * Added support for `has_many` association. (#13)
9
+ * Added support for `has_one` association. (#14)
10
+
11
+ ## Breaking Changes
12
+
13
+ * **Removed Cassandra and Postgres hstore adapters.** (#12)
14
+ * `find_each` now uses SQL cursors, so it's not constrained by the limitations of ActiveRecord's
15
+ implementation. (#11)
16
+ * Default values have been re-implemented correctly. (#15)
data/Gemfile CHANGED
@@ -2,7 +2,6 @@ source "http://rubygems.org"
2
2
  gemspec
3
3
 
4
4
  gem 'rake'
5
- gem 'thin'
6
5
 
7
6
  group :test do
8
7
  gem 'rails'
@@ -10,7 +9,3 @@ group :test do
10
9
  gem 'activerecord', '~> 4.2.0'
11
10
  gem 'mocha', require: false
12
11
  end
13
-
14
- group :cassandra do
15
- gem 'cassandra-cql', "1.1.4"
16
- end
data/README.md CHANGED
@@ -1,17 +1,15 @@
1
1
  # Superstore
2
- [![Build Status](https://secure.travis-ci.org/data-axle/superstore.png?rvm=2.0.0)](http://travis-ci.org/data-axle/superstore) [![Code Climate](https://codeclimate.com/github/data-axle/superstore.png)](https://codeclimate.com/github/data-axle/superstore)
2
+ [![Build Status](https://secure.travis-ci.org/data-axle/superstore.png?rvm=2.0.0)](http://travis-ci.org/data-axle/superstore) [![Code Climate](https://codeclimate.com/github/data-axle/superstore/badges/gpa.svg)](https://codeclimate.com/github/data-axle/superstore)
3
3
 
4
- Cassandra Object uses ActiveModel to mimic much of the behavior in ActiveRecord.
4
+ Superstore uses ActiveModel to mimic much of the behavior in ActiveRecord.
5
5
 
6
6
  ## Installation
7
7
 
8
- Add the following to your Gemfile:
8
+ Add the following to the `Gemfile`:
9
9
  ```ruby
10
10
  gem 'superstore'
11
11
  ```
12
12
 
13
- Change the version of Cassandra accordingly. Recent versions have not been backward compatible.
14
-
15
13
  ## Defining Models
16
14
 
17
15
  ```ruby
@@ -30,51 +28,34 @@ end
30
28
  ```
31
29
 
32
30
  The table name defaults to the case-sensitive, pluralized name of the model class. To specify a
33
- custom name, set the ```table_name``` attribute on the class:
31
+ custom name, set the `table_name` attribute on the class:
34
32
 
35
33
  ```ruby
36
34
  class MyWidget < Superstore::Base
37
35
  table_name = 'my_widgets'
38
36
  end
39
37
  ```
40
- ## Using with Cassandra
41
-
42
- Add the cassandra-cql gem to Gemfile:
43
-
44
- ```ruby
45
- gem 'cassandra-cql'
46
- ```
47
-
48
- Add a config/superstore.yml:
49
-
50
- ```yaml
51
- development:
52
- adapter: cassandra
53
- keyspace: my_app_development
54
- servers: 127.0.0.1:9160
55
- thrift:
56
- timeout: 20
57
- retries: 2
58
- ```
59
38
 
60
- ## Using with Postgres HStore
39
+ ## Using the PostgreSQL JSONB adapter
61
40
 
62
- Add the pg gem to your Gemfile:
41
+ Add the `pg` gem to the `Gemfile`:
63
42
 
64
43
  ```ruby
65
44
  gem 'pg'
66
45
  ```
67
46
 
68
- And a config/superstore.yml:
47
+ Add a `config/superstore.yml`:
69
48
 
70
49
  ```yaml
71
50
  development:
72
- adapter: hstore
51
+ adapter: jsonb
73
52
  ```
74
53
 
54
+ Superstore will share the existing ActiveRecord database connection.
55
+
75
56
  ## Creating and updating records
76
57
 
77
- Cassandra Object has equivalent methods as ActiveRecord:
58
+ Superstore has equivalent methods to ActiveRecord:
78
59
 
79
60
  ```ruby
80
61
  widget = Widget.new
@@ -102,8 +83,9 @@ end
102
83
  ## Scoping
103
84
 
104
85
  Some lightweight scoping features are available:
86
+
105
87
  ```ruby
106
- Widget.where('color' => 'red')
107
- Widget.select(['name', 'color'])
108
- Widget.limit(10)
88
+ Widget.where('color' => 'red')
89
+ Widget.select(['name', 'color'])
90
+ Widget.limit(10)
109
91
  ```
@@ -0,0 +1,245 @@
1
+ gem 'pg'
2
+ require 'pg'
3
+
4
+ module Superstore
5
+ module Adapters
6
+ class JsonbAdapter < AbstractAdapter
7
+ class QueryBuilder
8
+ def initialize(adapter, scope)
9
+ @adapter = adapter
10
+ @scope = scope
11
+ end
12
+
13
+ def to_query
14
+ [
15
+ "SELECT #{select_string} FROM #{@scope.klass.table_name}",
16
+ where_string,
17
+ order_string,
18
+ limit_string
19
+ ].delete_if(&:blank?) * ' '
20
+ end
21
+
22
+ def select_string
23
+ if @scope.select_values.any?
24
+ "id, jsonb_slice(document, #{@adapter.fields_to_postgres_array(@scope.select_values)}) as document"
25
+ else
26
+ '*'
27
+ end
28
+ end
29
+
30
+ def where_string
31
+ wheres = where_values_as_strings
32
+
33
+ if @scope.id_values.any?
34
+ wheres << @adapter.create_ids_where_clause(@scope.id_values)
35
+ end
36
+
37
+ if wheres.any?
38
+ "WHERE #{wheres * ' AND '}"
39
+ end
40
+ end
41
+
42
+ def order_string
43
+ if @scope.order_values.any?
44
+ orders = @scope.order_values.join(', ')
45
+ "ORDER BY #{orders}"
46
+ elsif @scope.id_values.many?
47
+ id_orders = @scope.id_values.map { |id| "ID=#{@adapter.quote(id)} DESC" }.join(',')
48
+ "ORDER BY #{id_orders}"
49
+ end
50
+ end
51
+
52
+ def limit_string
53
+ if @scope.limit_value
54
+ "LIMIT #{@scope.limit_value}"
55
+ end
56
+ end
57
+
58
+ def where_values_as_strings
59
+ @scope.where_values.map do |where_value|
60
+ if where_value.is_a?(Hash)
61
+ key = where_value.keys.first
62
+ value = where_value.values.first
63
+
64
+ if value.nil?
65
+ "(document->>'#{key}') IS NULL"
66
+ elsif value.is_a?(Array)
67
+ typecasted_values = value.map { |v| "'#{v}'" }.join(',')
68
+ "document->>'#{key}' IN (#{typecasted_values})"
69
+ else
70
+ "document->>'#{key}' = '#{value}'"
71
+ end
72
+ else
73
+ where_value
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def primary_key_column
80
+ 'id'
81
+ end
82
+
83
+ def connection
84
+ active_record_klass.connection
85
+ end
86
+
87
+ def active_record_klass=(klass)
88
+ @active_record_klass = klass
89
+ end
90
+
91
+ def active_record_klass
92
+ @active_record_klass ||= ActiveRecord::Base
93
+ end
94
+
95
+ def execute(statement)
96
+ connection.execute statement
97
+ end
98
+
99
+ def select(scope)
100
+ statement = QueryBuilder.new(self, scope).to_query
101
+
102
+ connection.execute(statement).each do |result|
103
+ yield result[primary_key_column], Oj.compat_load(result['document'])
104
+ end
105
+ end
106
+
107
+ def scroll(scope, batch_size)
108
+ statement = QueryBuilder.new(self, scope).to_query
109
+ cursor_name = "cursor_#{SecureRandom.hex(6)}"
110
+ fetch_sql = "FETCH FORWARD #{batch_size} FROM #{cursor_name}"
111
+
112
+ connection.transaction do
113
+ connection.execute "DECLARE #{cursor_name} NO SCROLL CURSOR FOR (#{statement})"
114
+
115
+ while (batch = connection.execute(fetch_sql)).any?
116
+ batch.each do |result|
117
+ yield result[primary_key_column], Oj.compat_load(result['document'])
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ def insert(table, id, attributes)
124
+ not_nil_attributes = attributes.reject { |key, value| value.nil? }
125
+ statement = "INSERT INTO #{table} (#{primary_key_column}, document) VALUES (#{quote(id)}, #{to_quoted_jsonb(not_nil_attributes)})"
126
+ execute_batchable statement
127
+ end
128
+
129
+ def update(table, id, attributes)
130
+ return if attributes.empty?
131
+
132
+ not_nil_attributes = attributes.reject { |key, value| value.nil? }
133
+ nil_attributes = attributes.select { |key, value| value.nil? }
134
+
135
+ if not_nil_attributes.any? && nil_attributes.any?
136
+ value_update = "jsonb_merge(jsonb_delete(document, #{fields_to_postgres_array(nil_attributes.keys)}), #{to_quoted_jsonb(not_nil_attributes)})"
137
+ elsif not_nil_attributes.any?
138
+ value_update = "jsonb_merge(document, #{to_quoted_jsonb(not_nil_attributes)})"
139
+ elsif nil_attributes.any?
140
+ value_update = "jsonb_delete(document, #{fields_to_postgres_array(nil_attributes.keys)})"
141
+ end
142
+
143
+ statement = "UPDATE #{table} SET document = #{value_update} WHERE #{primary_key_column} = #{quote(id)}"
144
+ execute_batchable statement
145
+ end
146
+
147
+ def delete(table, ids)
148
+ statement = "DELETE FROM #{table} WHERE #{create_ids_where_clause(ids)}"
149
+
150
+ execute_batchable statement
151
+ end
152
+
153
+ def execute_batch(statements)
154
+ connection.transaction do
155
+ execute(statements * ";\n")
156
+ end
157
+ end
158
+
159
+ def create_table(table_name, options = {})
160
+ ActiveRecord::Migration.create_table table_name, id: false do |t|
161
+ t.string :id, null: false
162
+ t.jsonb :document, null: false
163
+ end
164
+ connection.execute "ALTER TABLE \"#{table_name}\" ADD CONSTRAINT #{table_name}_pkey PRIMARY KEY (id)"
165
+ end
166
+
167
+ def drop_table(table_name)
168
+ ActiveRecord::Migration.drop_table table_name
169
+ end
170
+
171
+ def create_ids_where_clause(ids)
172
+ ids = ids.first if ids.is_a?(Array) && ids.one?
173
+
174
+ if ids.is_a?(Array)
175
+ id_list = ids.map { |id| quote(id) }.join(',')
176
+ "#{primary_key_column} IN (#{id_list})"
177
+ else
178
+ "#{primary_key_column} = #{quote(ids)}"
179
+ end
180
+ end
181
+
182
+ def quote(value)
183
+ connection.quote(value)
184
+ end
185
+
186
+ def fields_to_postgres_array(fields)
187
+ quoted_fields = fields.map { |field| quote(field) }.join(',')
188
+ "ARRAY[#{quoted_fields}]"
189
+ end
190
+
191
+ OJ_OPTIONS = {mode: :compat}
192
+ def to_quoted_jsonb(data)
193
+ "#{quote(Oj.dump(data, OJ_OPTIONS))}::JSONB"
194
+ end
195
+
196
+ JSON_FUNCTIONS = {
197
+ # SELECT jsonb_slice('{"b": 2, "c": 3, "a": 4}', '{b, c}');
198
+ 'jsonb_slice(data jsonb, keys text[])' => %{
199
+ SELECT json_object_agg(key, value)::jsonb
200
+ FROM (
201
+ SELECT * FROM jsonb_each(data)
202
+ ) t
203
+ WHERE key =ANY(keys);
204
+ },
205
+
206
+ # SELECT jsonb_merge('{"a": 1}', '{"b": 2, "c": 3, "a": 4}');
207
+ 'jsonb_merge(data jsonb, merge_data jsonb)' => %{
208
+ SELECT json_object_agg(key, value)::jsonb
209
+ FROM (
210
+ WITH to_merge AS (
211
+ SELECT * FROM jsonb_each(merge_data)
212
+ )
213
+ SELECT *
214
+ FROM jsonb_each(data)
215
+ WHERE key NOT IN (SELECT key FROM to_merge)
216
+ UNION ALL
217
+ SELECT * FROM to_merge
218
+ ) t;
219
+ },
220
+
221
+ # SELECT jsonb_delete('{"b": 2, "c": 3, "a": 4}', '{b, c}');
222
+ 'jsonb_delete(data jsonb, keys text[])' => %{
223
+ SELECT json_object_agg(key, value)::jsonb
224
+ FROM (
225
+ SELECT * FROM jsonb_each(data)
226
+ WHERE key <>ALL(keys)
227
+ ) t;
228
+ },
229
+ }
230
+ def define_jsonb_functions!
231
+ JSON_FUNCTIONS.each do |signature, body|
232
+ connection.execute %{
233
+ CREATE OR REPLACE FUNCTION public.#{signature}
234
+ RETURNS jsonb
235
+ IMMUTABLE
236
+ LANGUAGE sql
237
+ AS $$
238
+ #{body}
239
+ $$;
240
+ }
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,38 @@
1
+ module Superstore
2
+ module Associations
3
+ class Association
4
+ attr_reader :owner, :reflection
5
+ delegate :options, to: :reflection
6
+
7
+ def initialize(owner, reflection)
8
+ @owner = owner
9
+ @reflection = reflection
10
+ end
11
+
12
+ def association_class
13
+ association_class_name.constantize
14
+ end
15
+
16
+ def association_class_name
17
+ reflection.polymorphic? ? owner.send(reflection.polymorphic_column) : reflection.class_name
18
+ end
19
+
20
+ def target=(target)
21
+ @target = target
22
+ loaded!
23
+ end
24
+
25
+ def target
26
+ @target
27
+ end
28
+
29
+ def loaded?
30
+ @loaded
31
+ end
32
+
33
+ def loaded!
34
+ @loaded = true
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,35 @@
1
+ module Superstore
2
+ module Associations
3
+ class BelongsTo < Association
4
+ def reader
5
+ unless loaded?
6
+ self.target = get_record
7
+ end
8
+
9
+ target
10
+ end
11
+
12
+ def writer(record)
13
+ self.target = record
14
+ owner.send("#{reflection.foreign_key}=", record.try(reflection.primary_key))
15
+ if reflection.polymorphic?
16
+ owner.send("#{reflection.polymorphic_column}=", record.class.name)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def get_record
23
+ record_id = owner.send(reflection.foreign_key).presence
24
+ return unless record_id
25
+
26
+ if reflection.default_primary_key?
27
+ association_class.find_by_id(record_id)
28
+ else
29
+ association_class.find_by(reflection.primary_key => record_id)
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ module Superstore::Associations::Builder
2
+ class Association
3
+ def self.build(model, name, options)
4
+ new(model, name, options).build
5
+ end
6
+
7
+ attr_reader :model, :name, :options
8
+ def initialize(model, name, options)
9
+ @model, @name, @options = model, name, options
10
+ end
11
+
12
+ def build
13
+ define_writer
14
+ define_reader
15
+
16
+ reflection = Superstore::Associations::Reflection.new(macro, name, model, options)
17
+ model.association_reflections = model.association_reflections.merge(name => reflection)
18
+ end
19
+
20
+ def mixin
21
+ model.generated_association_methods
22
+ end
23
+
24
+ def define_writer
25
+ name = self.name
26
+ mixin.redefine_method("#{name}=") do |records|
27
+ association(name).writer(records)
28
+ end
29
+ end
30
+
31
+ def define_reader
32
+ name = self.name
33
+ mixin.redefine_method(name) do
34
+ association(name).reader
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,7 @@
1
+ module Superstore::Associations::Builder
2
+ class BelongsTo < Association
3
+ def macro
4
+ :belongs_to
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Superstore::Associations::Builder
2
+ class HasMany < Association
3
+ def macro
4
+ :has_many
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Superstore::Associations::Builder
2
+ class HasOne < Association
3
+ def macro
4
+ :has_one
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ module Superstore
2
+ module Associations
3
+ class HasMany < Association
4
+ def reader
5
+ unless loaded?
6
+ self.target = load_collection
7
+ end
8
+
9
+ target
10
+ end
11
+
12
+ def writer(records)
13
+ reader.instance_variable_set :@records, records
14
+ reader.instance_variable_set :@loaded, true
15
+ loaded!
16
+ end
17
+
18
+ private
19
+
20
+ def load_collection
21
+ association_class.where(reflection.foreign_key => owner.try(reflection.primary_key))
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ module Superstore
2
+ module Associations
3
+ class HasOne < Association
4
+ def reader
5
+ unless loaded?
6
+ self.target = load_target
7
+ end
8
+
9
+ target
10
+ end
11
+
12
+ def writer(record)
13
+ self.target = record
14
+ end
15
+
16
+ private
17
+
18
+ def load_target
19
+ association_class.where(reflection.foreign_key => owner.try(reflection.primary_key)).first
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,65 @@
1
+ module Superstore
2
+ module Associations
3
+ class Reflection
4
+ attr_reader :macro, :name, :model, :options
5
+ def initialize(macro, name, model, options)
6
+ @macro = macro
7
+ @name = name
8
+ @model = model
9
+ @options = options
10
+ end
11
+
12
+ def association_class
13
+ case macro
14
+ when :belongs_to
15
+ Superstore::Associations::BelongsTo
16
+ when :has_many
17
+ Superstore::Associations::HasMany
18
+ when :has_one
19
+ Superstore::Associations::HasOne
20
+ end
21
+
22
+ end
23
+
24
+ def instance_variable_name
25
+ "@#{name}"
26
+ end
27
+
28
+ def foreign_key
29
+ @foreign_key ||= options[:foreign_key] || derive_foreign_key
30
+ end
31
+
32
+ def primary_key
33
+ options[:primary_key] || "id"
34
+ end
35
+
36
+ def default_primary_key?
37
+ primary_key == "id"
38
+ end
39
+
40
+ def polymorphic_column
41
+ "#{name}_type"
42
+ end
43
+
44
+ def polymorphic?
45
+ options[:polymorphic]
46
+ end
47
+
48
+ def class_name
49
+ @class_name ||= (options[:class_name] || name.to_s.classify)
50
+ end
51
+
52
+ private
53
+
54
+ def derive_foreign_key
55
+ case macro
56
+ when :has_many, :has_one
57
+ model.name.foreign_key
58
+ when :belongs_to
59
+ "#{name}_id"
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+ end