superstore 1.2.0 → 2.0.0

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