praxis-mapper 4.1.2 → 4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5a54434e8e0f9299b8ae645f800e93ebbc1b0fae
4
- data.tar.gz: 78806cff2fdeda7b7375f8ccc1a68f26addddbfb
3
+ metadata.gz: 3ff56d63e3412602d1542aefa5a0ddeb2c6f053d
4
+ data.tar.gz: e55ac6835ca3be6495ccb699e72197502812664f
5
5
  SHA512:
6
- metadata.gz: 668be9af356c5099402fdcfb46533004c5a3cc629085235417e73bdd8688ca061c1c440686aabd515fbe05f82a7acb879770f43037fb6ee9955caf7620d8ed11
7
- data.tar.gz: 96effa292125c2d3f3ce5d20eba3f338cf7ef0c4d8c7486c62054626e4840162080a9975942d8fef5989fd19065c4a060c97397b7bcaaf404c606a3f50f65601
6
+ metadata.gz: 54bb23566ca0385a869114087e2fd91420f3e1b7e5ce890672dbbdb649d4d0fe7f63bf5b947e4d7c2af781996363d185e3ee0de160836e70d265bdd4b6a8673e
7
+ data.tar.gz: 088259294b44ba826033c1a39e279899be35f0c482827f0cc6deac8aa3282dd6323160ff0877b68d15b2a7ebe92e3df8c38ffc985ad050de775daee9b4388ba4
@@ -2,6 +2,42 @@
2
2
 
3
3
  ## next
4
4
 
5
+ ## 4.2
6
+
7
+ * Added `Resource.property` to specify the fields and associations from the
8
+ underlying model that are needed by methods added to the `Resource`.
9
+ * For example:
10
+ * `property :full_name, dependencies: [:first, :last]` specifies that the `full_name` method on the resource depends upon the values of the `first`
11
+ and `last` fields from the model.
12
+ * `property :owner_name, dependencies: ['owner.first', 'owner.last']`
13
+ defines a dependency on the `first` and `last` fields from the associated
14
+ `owner`.
15
+ * Dependencies must always be an array. Symbol values should be used for
16
+ fields or associations from the same resource, and strings can be used
17
+ for fields from associated resources.
18
+ * `:*` or `'association.*'` may be used to specify a
19
+ dependency on all of the fields of the resource (analagous to 'SELECT *').
20
+ * Additionally, property dependencies may specify other properties on the
21
+ same or related resources.
22
+ * Added `IdentityMap#add_selectors(resource, field_selector)` to define fields to
23
+ `select` and associations to `track` given `resource` and `field_selectors`.
24
+ * For example:
25
+ * `identity_map#add_selectors(PersonResource, full_name: true)` would
26
+ ensure any loads done for the model the `PersonResource` describes will
27
+ select any fields required by the `full_name` property (`first` and `last`
28
+ in the example above).
29
+ * `identity_map#add_selectors(PersonResource, id: true, address: true)`
30
+ could result in selecting the `id` and `address_id` fields of `PersonModel`,
31
+ as well as tracking the `address` association, for any queries involving
32
+ `PersonModel`.
33
+ * Added the ability to explicitly perform a "SELECT *" by specifying `select :*`
34
+ or `select '*'` in a query.
35
+ * Added `:through` option to many_to_many associations to define the series of
36
+ associations necessary to resolve it. This is unused by Sequel, but is required
37
+ by the SelectorGenerator to handle such associations. Specify it as an array
38
+ of symbols.
39
+ * For example: `many_to_many :commented_posts, through: [:comments, :post], ...`
40
+
5
41
  ## 4.1.2
6
42
 
7
43
  * Remove finalizer from IdentityMap to fix memory leak that was preventing them from being GC'd properly.
data/README.md CHANGED
@@ -1,7 +1,11 @@
1
- # Praxis::Mapper [![TravisCI][travis-img-url]][travis-ci-url]
1
+ # Praxis::Mapper [![TravisCI][travis-img-url]][travis-ci-url] [![Coverage Status][coveralls-img-url]][coveralls-url] [![Dependency Status][gemnasium-img-url]][gemnasium-url]
2
2
 
3
3
  [travis-img-url]:https://travis-ci.org/rightscale/praxis-mapper.svg?branch=master
4
4
  [travis-ci-url]:https://travis-ci.org/rightscale/praxis-mapper
5
+ [coveralls-img-url]:https://coveralls.io/repos/rightscale/praxis-mapper/badge.svg?branch=master&service=github
6
+ [coveralls-url]:https://coveralls.io/github/rightscale/praxis-mapper?branch=master
7
+ [gemnasium-img-url]:https://gemnasium.com/rightscale/praxis-mapper.svg
8
+ [gemnasium-url]:https://gemnasium.com/rightscale/praxis-mapper
5
9
 
6
10
 
7
11
  Praxis::Mapper is a library that allows for large amounts of data to be loaded for a tree of associated models,
@@ -20,4 +24,4 @@ To run unit tests:
20
24
 
21
25
  This software is released under the [MIT License](http://www.opensource.org/licenses/MIT). Please see [LICENSE](LICENSE) for further details.
22
26
 
23
- Copyright (c) 2014 RightScale
27
+ Copyright (c) 2014 RightScale
@@ -51,7 +51,7 @@ require 'praxis-mapper/identity_map'
51
51
  require 'praxis-mapper/model'
52
52
  require 'praxis-mapper/query_statistics'
53
53
 
54
- require 'praxis-mapper/sequel_compat'
54
+ require 'praxis-mapper/sequel_compat'
55
55
 
56
56
  require 'praxis-mapper/connection_manager'
57
57
 
@@ -64,5 +64,6 @@ require 'praxis-mapper/query/base'
64
64
  require 'praxis-mapper/query/sql'
65
65
  require 'praxis-mapper/query/sequel'
66
66
 
67
-
68
67
  require 'praxis-mapper/config_hash'
68
+
69
+ require 'praxis-mapper/selector_generator'
@@ -10,7 +10,10 @@ module Praxis::Mapper
10
10
  class UnsupportedModel < StandardError; end;
11
11
  class UnknownIdentity < StandardError; end;
12
12
 
13
- attr_reader :unloaded, :queries, :blueprint_cache
13
+ attr_reader :unloaded
14
+ attr_reader :queries
15
+ attr_reader :blueprint_cache
16
+
14
17
  attr_accessor :scope
15
18
 
16
19
  class << self
@@ -69,9 +72,14 @@ module Praxis::Mapper
69
72
  def initialize(scope={})
70
73
  @connection_manager = ConnectionManager.new
71
74
  @scope = scope
75
+ @selector_generator = Praxis::Mapper::SelectorGenerator.new
72
76
  clear!
73
77
  end
74
78
 
79
+ def selectors
80
+ @selector_generator.selectors
81
+ end
82
+
75
83
  def clear!
76
84
  @rows = Hash.new { |h,k| h[k] = Array.new }
77
85
 
@@ -315,7 +323,6 @@ module Praxis::Mapper
315
323
  @row_keys[model][key].fetch(value) do
316
324
  raise UnloadedRecordException, "Did not load #{model} with #{key} = #{value.inspect}."
317
325
  end
318
-
319
326
  end
320
327
 
321
328
 
@@ -368,12 +375,12 @@ module Praxis::Mapper
368
375
  end
369
376
  else
370
377
  if @row_keys[model].has_key?(key)
371
- values.collect do |value|
372
- @row_keys[model][key][value]
378
+ values.collect do |v|
379
+ @row_keys[model][key][v]
373
380
  end.compact
374
381
  else
375
- values.each_with_object(Array.new) do |value, results|
376
- results.push *index(model, key, value)
382
+ values.each_with_object(Array.new) do |v, results|
383
+ results.push(*index(model, key, v))
377
384
  end
378
385
  end
379
386
  end
@@ -477,8 +484,6 @@ module Praxis::Mapper
477
484
  end
478
485
  end
479
486
 
480
- model = records.first.class
481
-
482
487
  tracked_associations = if (query = records.first._query)
483
488
  query.tracked_associations.each do |tracked_association|
484
489
  associated_model = tracked_association[:model]
@@ -490,7 +495,6 @@ module Praxis::Mapper
490
495
 
491
496
  tracked_associations.each do |tracked_association|
492
497
  associated_model = tracked_association[:model]
493
- association_type = tracked_association[:type]
494
498
 
495
499
  association_key, row_keys = stage_for!(tracked_association, records)
496
500
  row_keys.each do |row_key|
@@ -549,5 +553,9 @@ module Praxis::Mapper
549
553
  QueryStatistics.new(queries)
550
554
  end
551
555
 
556
+ def add_selectors(resource, fields)
557
+ @selector_generator.add(resource, fields)
558
+ end
559
+
552
560
  end
553
561
  end
@@ -18,6 +18,7 @@ module Praxis::Mapper
18
18
  # @param model [Praxis::Mapper::Model] handle to a Praxis::Mapper model
19
19
  # @param &block [Block] will be instance_eval'ed here
20
20
  def initialize(identity_map, model, &block)
21
+
21
22
  @identity_map = identity_map
22
23
  @model = model
23
24
 
@@ -32,9 +33,24 @@ module Praxis::Mapper
32
33
 
33
34
  @statistics = Hash.new(0) # general-purpose hash
34
35
 
36
+ if (selector = identity_map.selectors[model])
37
+ self.apply_selector(selector)
38
+ end
39
+
35
40
  if block_given?
36
41
  self.instance_eval(&block)
37
42
  end
43
+
44
+ end
45
+
46
+ def apply_selector(selector)
47
+ if selector[:select]
48
+ self.select(*selector[:select])
49
+ end
50
+
51
+ if selector[:track]
52
+ self.track(*selector[:track])
53
+ end
38
54
  end
39
55
 
40
56
  # @return handle to configured data store
@@ -51,11 +67,20 @@ module Praxis::Mapper
51
67
  # @example select(:account_id, "user_id", {"create_time" => :created_at})
52
68
  def select(*fields)
53
69
  if fields.any?
54
- @select = {} if @select.nil?
70
+ return @select if @select == true
71
+
72
+ if @select.nil?
73
+ @select = default_select
74
+ end
55
75
  fields.each do |field|
56
76
  case field
57
77
  when Symbol, String
58
- @select[field] = nil
78
+ if field == :* || field == "*"
79
+ @select = true
80
+ break
81
+ else
82
+ @select[field] = nil
83
+ end
59
84
  when Hash
60
85
  field.each do |alias_name, column_name|
61
86
  @select[alias_name] = column_name
@@ -69,6 +94,11 @@ module Praxis::Mapper
69
94
  end
70
95
  end
71
96
 
97
+ def default_select
98
+ model.identities.each_with_object({}).each do |identity, hash|
99
+ hash[identity] = nil
100
+ end
101
+ end
72
102
 
73
103
  # Gets or sets an SQL-like 'WHERE' clause to this query.
74
104
  #
@@ -130,8 +160,8 @@ module Praxis::Mapper
130
160
  raise "context #{name.inspect} not found for #{model}"
131
161
  end
132
162
 
133
- select *spec[:select]
134
- track *spec[:track]
163
+ select(*spec[:select])
164
+ track(*spec[:track])
135
165
  end
136
166
 
137
167
 
@@ -161,7 +191,7 @@ module Praxis::Mapper
161
191
  rows = []
162
192
 
163
193
  original_select = @select
164
- self.select *select.flatten.uniq if select
194
+ self.select(*select.flatten.uniq) if select
165
195
 
166
196
  values.each_slice(MULTI_GET_BATCH_SIZE) do |batch|
167
197
  rows += _multi_get(identity, batch)
@@ -15,7 +15,7 @@ module Praxis::Mapper
15
15
  ds = connection[model.table_name.to_sym]
16
16
 
17
17
  # TODO: support column aliases
18
- if @select
18
+ if @select && @select != true
19
19
  ds = ds.select(*@select.keys)
20
20
  end
21
21
 
@@ -110,7 +110,7 @@ module Praxis::Mapper
110
110
  # @return [String] SQL 'SELECT' clause
111
111
  def select_clause
112
112
  columns = []
113
- if select
113
+ if select && select != true
114
114
  select.each do |alias_name, column_name|
115
115
  if column_name
116
116
  # alias_name is always a String, not a Symbol
@@ -29,9 +29,12 @@ module Praxis::Mapper
29
29
 
30
30
  attr_accessor :record
31
31
 
32
+ @properties = {}
33
+
32
34
  class << self
33
35
  attr_reader :model_map
34
36
  attr_reader :decorations
37
+ attr_reader :properties
35
38
  end
36
39
 
37
40
  # TODO: also support an attribute of sorts on the versioned resource module. ie, V1::Resources.api_version.
@@ -46,10 +49,11 @@ module Praxis::Mapper
46
49
  if self.superclass == Praxis::Mapper::Resource
47
50
  @model_map = Hash.new
48
51
  else
49
- @model_map = self.superclass.model_map
52
+ @model_map = self.superclass.model_map
50
53
  end
51
54
 
52
55
  @decorations = {}
56
+ @properties = self.superclass.properties.clone
53
57
  end
54
58
 
55
59
  end
@@ -69,6 +73,10 @@ module Praxis::Mapper
69
73
  self.decorations[name] = Class.new(ResourceDecorator, &block)
70
74
  end
71
75
 
76
+ def self.property(name, **options)
77
+ self.properties[name] = options
78
+ end
79
+
72
80
  def self._finalize!
73
81
  finalize_resource_delegates
74
82
  define_model_accessors
@@ -108,10 +116,10 @@ module Praxis::Mapper
108
116
 
109
117
  def self.define_decorator(name, block)
110
118
  unless self.instance_methods.include?(name)
111
- # assume it'll be a regular accessor and create it
119
+ # assume it'll be a regular accessor and create it
112
120
  self.define_accessor(name)
113
121
  end
114
- # alias original method and wrap it
122
+ # alias original method and wrap it
115
123
  raw_name = "_raw_#{name}"
116
124
  alias_method(raw_name.to_sym, name)
117
125
 
@@ -137,17 +145,16 @@ module Praxis::Mapper
137
145
  end
138
146
 
139
147
 
140
- def self.wrap(records)
141
- case records
142
- when nil
143
- return []
144
- when Enumerable
145
- return records.compact.collect { |record| self.for_record(record) }
146
- else
147
- return self.for_record(records)
148
+ def self.wrap(records)
149
+ case records
150
+ when nil
151
+ return []
152
+ when Enumerable
153
+ return records.compact.collect { |record| self.for_record(record) }
154
+ else
155
+ return self.for_record(records)
156
+ end
148
157
  end
149
- end
150
-
151
158
 
152
159
 
153
160
  def self.get(condition)
@@ -0,0 +1,98 @@
1
+ module Praxis::Mapper
2
+ # Generates a set of selectors given a resource and
3
+ # list of resource attributes.
4
+ class SelectorGenerator
5
+ attr_reader :selectors
6
+
7
+ def initialize
8
+ @selectors = Hash.new do |hash, key|
9
+ hash[key] = {select: Set.new, track: Set.new}
10
+ end
11
+ end
12
+
13
+ def add(resource, fields)
14
+ fields.each do |name, field|
15
+ map_property(resource, name, field)
16
+ end
17
+ end
18
+
19
+ def select_all(resource)
20
+ selectors[resource.model][:select] = true
21
+ end
22
+
23
+ def map_property(resource, name, field)
24
+ if resource.properties.key?(name)
25
+ add_property(resource, name)
26
+ elsif resource.model.associations.key?(name)
27
+ add_association(resource, name, field)
28
+ else
29
+ add_select(resource, name)
30
+ end
31
+ end
32
+
33
+ def add_select(resource, name)
34
+ return select_all(resource) if name == :*
35
+ return if selectors[resource.model][:select] == true
36
+
37
+ selectors[resource.model][:select] << name
38
+ end
39
+
40
+ def add_track(resource, name)
41
+ selectors[resource.model][:track] << name
42
+ end
43
+
44
+ def add_association(resource, name, fields)
45
+ association = resource.model.associations.fetch(name) do
46
+ raise "missing association for #{resource} with name #{name}"
47
+ end
48
+ associated_resource = resource.model_map[association[:model]]
49
+
50
+ case association[:type]
51
+ when :many_to_one
52
+ add_track(resource, name)
53
+ add_select(resource, association[:key])
54
+ when :one_to_many
55
+ add_track(resource, name)
56
+ add_select(associated_resource, association[:key])
57
+ when :many_to_many
58
+ head, *tail = association.fetch(:through) do
59
+ raise "Association #{name} on #{resource.model} must specify the " +
60
+ "':through' option. "
61
+ end
62
+ new_fields = tail.reverse.inject(fields) do |thing, step|
63
+ {step => thing}
64
+ end
65
+ return add_association(resource, head, new_fields)
66
+ else
67
+ raise "no select applicable for #{association[:type].inspect}"
68
+ end
69
+
70
+ unless fields == true
71
+ # recurse into the field
72
+ add(associated_resource,fields)
73
+ end
74
+ end
75
+
76
+ def add_property(resource, name)
77
+ dependencies = resource.properties[name][:dependencies]
78
+ return if dependencies.nil?
79
+
80
+ dependencies.each do |dependency|
81
+ apply_dependency(resource, dependency)
82
+ end
83
+ end
84
+
85
+ def apply_dependency(resource, dependency)
86
+ case dependency
87
+ when Symbol
88
+ map_property(resource, dependency, {})
89
+ when String
90
+ head, tail = dependency.split('.').collect(&:to_sym)
91
+ raise "String dependencies can not be singular" if tail.nil?
92
+
93
+ add_association(resource, head, {tail => true})
94
+ end
95
+ end
96
+
97
+ end
98
+ end
@@ -1,5 +1,5 @@
1
1
  module Praxis
2
2
  module Mapper
3
- VERSION = "4.1.2"
3
+ VERSION = "4.2"
4
4
  end
5
5
  end
@@ -36,4 +36,5 @@ Gem::Specification.new do |spec|
36
36
  spec.add_development_dependency(%q<fuubar>, ["~> 1"])
37
37
  spec.add_development_dependency('sqlite3')
38
38
  spec.add_development_dependency('factory_girl')
39
+ spec.add_development_dependency('coveralls')
39
40
  end
@@ -2,8 +2,14 @@ FactoryGirl.define do
2
2
 
3
3
  to_create { |i| i.save }
4
4
 
5
- factory :user, class: UserModel, aliases: [:author] do
6
- name { /[:name:]/.gen }
5
+ factory :blog, class: BlogModel do
6
+ name { /\w+/.gen }
7
+ owner
8
+ end
9
+
10
+ factory :user, class: UserModel, aliases: [:author, :owner] do
11
+ first_name { /[:first_name:]/.gen }
12
+ last_name { /[:last_name]/.gen }
7
13
  email { /[:email:]/.gen }
8
14
  end
9
15
 
@@ -3,15 +3,16 @@ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
3
3
  describe Praxis::Mapper::Query::Base do
4
4
  let(:scope) { {} }
5
5
  let(:unloaded_ids) { [1, 2, 3] }
6
+ let(:selectors) { {} }
7
+
6
8
  let(:connection) { double("connection") }
7
- let(:identity_map) { double("identity_map", :scope => scope, :get_unloaded => unloaded_ids) }
9
+ let(:identity_map) { double("identity_map", scope: scope, get_unloaded: unloaded_ids, selectors: selectors) }
8
10
 
9
11
  let(:model) { SimpleModel }
10
12
 
11
13
  let(:expected_ids_condition) { "id IN (#{unloaded_ids.join(", ")})" }
12
14
 
13
- let(:query) { Praxis::Mapper::Query::Base.new(identity_map, model) }
14
- subject { query }
15
+ subject(:query) { Praxis::Mapper::Query::Base.new(identity_map, model) }
15
16
 
16
17
  let(:rows) { [
17
18
  {:id => 1, :name => "george jr", :parent_id => 1, :description => "one"},
@@ -110,6 +111,11 @@ describe Praxis::Mapper::Query::Base do
110
111
  subject.select.should include(:id => nil, :name => nil)
111
112
  end
112
113
 
114
+ it 'adds model identities if necessary' do
115
+ subject.select :id
116
+ subject.select.should eq(id: nil, name: nil)
117
+ end
118
+
113
119
  it "accepts an array of strings" do
114
120
  subject.select "id", "name"
115
121
  subject.select.should include("id" => nil, "name" => nil)
@@ -135,7 +141,6 @@ describe Praxis::Mapper::Query::Base do
135
141
 
136
142
  context "with symbols for the field definitions" do
137
143
  it "and symbols to specify the field aliases" do
138
- definition = {:my_id => :id, :name => :name}
139
144
  subject.select :my_id => :id, :name => :name
140
145
  subject.select.should include :my_id => :id, :name => :name
141
146
  end
@@ -163,6 +168,27 @@ describe Praxis::Mapper::Query::Base do
163
168
  subject.select.should include(definition)
164
169
  end
165
170
 
171
+ context 'selecting all fields' do
172
+ it 'maps select :* to true' do
173
+ subject.select :*
174
+ subject.select.should be(true)
175
+ end
176
+
177
+ it 'preserves * when selecting specific fields' do
178
+ subject.select :*
179
+ subject.select :id
180
+ subject.select.should be(true)
181
+ end
182
+
183
+ it 'overrides any selected fields when * is passed' do
184
+ subject.select :id
185
+ subject.select :*
186
+ subject.select.should be(true)
187
+ end
188
+
189
+ end
190
+
191
+
166
192
  end
167
193
 
168
194
  context "with no query body" do
@@ -310,9 +336,11 @@ describe Praxis::Mapper::Query::Base do
310
336
  end
311
337
  end
312
338
 
313
- context "#raw" do
314
- let(:model) { PersonModel }
339
+ context 'with a selectors from the identity map' do
340
+ let(:selectors) { {model => {select: [:name, :state], track: [:address]}} }
315
341
 
342
+ its(:select) { should eq(id: nil, name: nil, state: nil) }
343
+ its(:track) { should eq Set[:address] }
316
344
  end
317
345
 
318
346
  end
@@ -41,6 +41,17 @@ describe Praxis::Mapper::Query::Sequel do
41
41
  query.execute
42
42
  connection.sqls.should eq(["SELECT id, name FROM items WHERE (name = 'something') LIMIT 10"])
43
43
  end
44
+
45
+ context 'with select :* in query' do
46
+ it 'runs the correct sql with "SELECT *"' do
47
+ query.select :*
48
+ connection.sqls.should be_empty
49
+ query.execute
50
+ connection.sqls.should eq(["SELECT * FROM items WHERE (name = 'something') LIMIT 10"])
51
+ end
52
+
53
+ end
54
+
44
55
  end
45
56
 
46
57
 
@@ -91,6 +91,21 @@ describe Praxis::Mapper::Query::Sql do
91
91
 
92
92
  end
93
93
 
94
+ context 'selecting all fields' do
95
+ subject(:query) do
96
+ Praxis::Mapper::Query::Sql.new(identity_map, SimpleModel) do
97
+ select :id, :name
98
+ where "deployment_id=2"
99
+ track :parent
100
+ end
101
+ end
102
+
103
+ it 'generates proper select clause if select is true' do
104
+ query.select :*
105
+ query.select_clause.should eq 'SELECT *'
106
+ end
107
+
108
+ end
94
109
  context '#_multi_get' do
95
110
 
96
111
  let(:ids) { [1, 2, 3] }
@@ -1,7 +1,6 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
2
 
3
3
  describe Praxis::Mapper::Resource do
4
-
5
4
  let(:parent_record) { ParentModel.new(id: 100, name: 'george sr') }
6
5
  let(:parent_records) { [ParentModel.new(id: 101, name: "georgia"),ParentModel.new(id: 102, name: 'georgina')] }
7
6
  let(:record) { SimpleModel.new(id: 103, name: 'george xvi') }
@@ -15,8 +14,24 @@ describe Praxis::Mapper::Resource do
15
14
  end
16
15
 
17
16
  context 'configuration' do
18
- subject { SimpleResource }
17
+ subject(:resource) { SimpleResource }
19
18
  its(:model) { should == model }
19
+
20
+ context 'properties' do
21
+ subject(:properties) { resource.properties }
22
+
23
+ it 'includes directly-set properties' do
24
+ properties[:other_resource].should eq(dependencies: [:other_model])
25
+ end
26
+
27
+ it 'inherits from a superclass' do
28
+ properties[:href].should eq(dependencies: [:id])
29
+ end
30
+
31
+ it 'properly overrides a property from the parent' do
32
+ properties[:name].should eq(dependencies: [:simple_name])
33
+ end
34
+ end
20
35
  end
21
36
 
22
37
  context 'retrieving resources' do
@@ -0,0 +1,221 @@
1
+ require 'spec_helper'
2
+
3
+ describe Praxis::Mapper::SelectorGenerator do
4
+ let(:properties) { {} }
5
+ let(:resource) { BlogResource }
6
+ subject(:generator) {Praxis::Mapper::SelectorGenerator.new }
7
+
8
+ before do
9
+ generator.add(resource,properties)
10
+ end
11
+
12
+ let(:expected_selectors) { {} }
13
+
14
+ context 'for a simple field' do
15
+ let(:properties) { {id: true} }
16
+ let(:expected_selectors) do
17
+ {
18
+ BlogModel => {
19
+ select: Set.new([:id]),
20
+ track: Set.new()
21
+ }
22
+ }
23
+ end
24
+
25
+ it 'generates the correct set of selectors' do
26
+ generator.selectors.should eq expected_selectors
27
+ end
28
+ end
29
+
30
+ context 'for a simple property' do
31
+ let(:properties) { {display_name: true} }
32
+ let(:expected_selectors) do
33
+ {
34
+ BlogModel => {
35
+ select: Set.new([:name]),
36
+ track: Set.new()
37
+ }
38
+ }
39
+ end
40
+ it 'generates the correct set of selectors' do
41
+ generator.selectors.should eq expected_selectors
42
+ end
43
+ end
44
+
45
+ context 'for an association' do
46
+ let(:properties) { {owner: true} }
47
+ let(:expected_selectors) do
48
+ {
49
+ BlogModel => {
50
+ select: Set.new([:owner_id]),
51
+ track: Set.new([:owner])
52
+ }
53
+ }
54
+ end
55
+ it 'generates the correct set of selectors' do
56
+ generator.selectors.should eq expected_selectors
57
+ end
58
+
59
+ context 'that is many_to_many' do
60
+ let(:properties) { {commented_posts: true} }
61
+ let(:resource) { UserResource }
62
+ let(:expected_selectors) do
63
+ {
64
+ CommentModel => {
65
+ select: Set.new([:author_id, :post_id]),
66
+ track: Set.new([:post])
67
+ },
68
+ UserModel => {
69
+ select: Set.new([]),
70
+ track: Set.new([:comments])
71
+ }
72
+ }
73
+ end
74
+ it 'generates the correct set of selectors' do
75
+ generator.selectors.should eq expected_selectors
76
+ end
77
+ end
78
+ end
79
+
80
+ context 'for a property that specifies a field from an association' do
81
+ let(:properties) { {owner_email: true} }
82
+ let(:expected_selectors) do
83
+ {
84
+ BlogModel => {
85
+ select: Set.new([:owner_id]),
86
+ track: Set.new([:owner])
87
+ },
88
+ UserModel => {
89
+ select: Set.new([:email]),
90
+ track: Set.new()
91
+ }
92
+ }
93
+ end
94
+
95
+ it 'generates the correct set of selectors' do
96
+ generator.selectors.should eq expected_selectors
97
+ end
98
+ end
99
+
100
+ context 'for a simple property that requires all fields' do
101
+ let(:properties) { {everything: true} }
102
+ let(:expected_selectors) do
103
+ {
104
+ BlogModel => {
105
+ select: true,
106
+ track: Set.new()
107
+ }
108
+ }
109
+ end
110
+ it 'generates the correct set of selectors' do
111
+ generator.selectors.should eq expected_selectors
112
+ end
113
+ end
114
+
115
+ context 'for property that uses an associated property' do
116
+ let(:properties) { {owner_full_name: true} }
117
+ let(:expected_selectors) do
118
+ {
119
+ BlogModel => {
120
+ select: Set.new([:owner_id]),
121
+ track: Set.new([:owner])
122
+ },
123
+ UserModel => {
124
+ select: Set.new([:first_name, :last_name]),
125
+ track: Set.new()
126
+ }
127
+ }
128
+ end
129
+ it 'generates the correct set of selectors' do
130
+ generator.selectors.should eq expected_selectors
131
+ end
132
+ end
133
+
134
+
135
+ context 'for a property that requires all fields from an association' do
136
+ let(:properties) { {everything_from_owner: true} }
137
+ let(:expected_selectors) do
138
+ {
139
+ BlogModel => {
140
+ select: Set.new([:owner_id]),
141
+ track: Set.new([:owner])
142
+ },
143
+ UserModel => {
144
+ select: true,
145
+ track: Set.new()
146
+ }
147
+ }
148
+ end
149
+ it 'generates the correct set of selectors' do
150
+ generator.selectors.should eq expected_selectors
151
+ end
152
+ end
153
+
154
+ context 'with a property that groups multiple fields' do
155
+ let(:properties) { {owner_full_name: {first: true}} }
156
+ let(:expected_selectors) do
157
+ {
158
+ BlogModel => {
159
+ select: Set.new([:owner_id]),
160
+ track: Set.new([:owner])
161
+ },
162
+ UserModel => {
163
+ select: Set.new([:first_name, :last_name]),
164
+ track: Set.new()
165
+ }
166
+ }
167
+ end
168
+ it 'generates selectors that ignore any unapplicable subrefinements' do
169
+ generator.selectors.should eq expected_selectors
170
+ end
171
+ end
172
+
173
+ context 'for a property with no dependencies' do
174
+ let(:properties) { {id: true, kind: true} }
175
+ let(:expected_selectors) do
176
+ {
177
+ BlogModel => {
178
+ select: Set.new([:id]),
179
+ track: Set.new()
180
+ }
181
+ }
182
+ end
183
+ it 'generates the correct set of selectors' do
184
+ generator.selectors.should eq expected_selectors
185
+ end
186
+ end
187
+
188
+ context 'with large set of properties' do
189
+
190
+ let(:properties) do
191
+ {
192
+ display_name: true,
193
+ owner: {
194
+ id: true,
195
+ full_name: true,
196
+ blogs_summary: {href: true, size: true},
197
+ main_blog: {id: true},
198
+ },
199
+ administrator: {id: true, full_name: true}
200
+ }
201
+ end
202
+
203
+ let(:expected_selectors) do
204
+ {
205
+ BlogModel=> {
206
+ select: Set.new([:id, :name, :owner_id, :administrator_id]),
207
+ track: Set.new([:owner, :administrator])
208
+ },
209
+ UserModel=> {
210
+ select: Set.new([:id, :first_name, :last_name, :main_blog_id]),
211
+ track: Set.new([:blogs, :main_blog])
212
+ }
213
+ }
214
+ end
215
+
216
+ it 'generates the correct set of selectors' do
217
+ generator.selectors.should eq(expected_selectors)
218
+ end
219
+ end
220
+
221
+ end
@@ -1,3 +1,6 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
1
4
  Encoding.default_external = Encoding::UTF_8
2
5
 
3
6
  require 'rubygems'
@@ -19,9 +22,11 @@ require 'praxis-mapper'
19
22
  require 'active_support/core_ext/kernel'
20
23
 
21
24
 
22
- require_relative 'support/spec_sequel_models'
23
25
  require_relative 'support/spec_models'
26
+ require_relative 'support/spec_sequel_models'
24
27
  require_relative 'support/spec_resources'
28
+ require_relative 'support/spec_sequel_resources'
29
+
25
30
  require_relative 'spec_fixtures'
26
31
 
27
32
  require 'praxis-mapper/support'
@@ -43,7 +48,7 @@ RSpec.configure do |config|
43
48
  ]
44
49
 
45
50
  config.include FactoryGirl::Syntax::Methods
46
-
51
+
47
52
  config.before(:suite) do
48
53
  FactoryGirl.find_definitions
49
54
  Sequel::Model.db.transaction do
@@ -5,6 +5,7 @@ class BaseResource < Praxis::Mapper::Resource
5
5
  base_href + "/#{self.class.collection_name}/#{self.id}"
6
6
  end
7
7
 
8
+ property :href, dependencies: [:id]
8
9
  end
9
10
 
10
11
  class CompositeIdResource < BaseResource
@@ -28,6 +29,9 @@ class SimpleResource < BaseResource
28
29
  self.other_model
29
30
  end
30
31
 
32
+ property :other_resource, dependencies: [:other_model]
33
+
34
+ property :name, dependencies: [:simple_name]
31
35
  end
32
36
 
33
37
  class SimplerResource < BaseResource
@@ -63,8 +67,20 @@ end
63
67
  class AddressResource < BaseResource
64
68
  model AddressModel
65
69
 
66
- def href
70
+
71
+ def href
67
72
  "/addresses/#{self.id}"
68
73
  end
69
- end
74
+ property :href, dependencies: [:id]
70
75
 
76
+ def owner_name
77
+ self.owner.name
78
+ end
79
+ property :owner_name, dependencies: ['owner.name']
80
+
81
+ def resident_count
82
+ self.residents.size
83
+ end
84
+ property :resident_count, dependencies: [:residents]
85
+
86
+ end
@@ -4,10 +4,20 @@
4
4
 
5
5
  DB = Sequel.sqlite
6
6
 
7
- DB.create_table! :users do
7
+ DB.create_table! :blogs do
8
8
  primary_key :id
9
+ Integer :owner_id
10
+ Integer :administrator_id
9
11
 
10
12
  String :name
13
+ end
14
+
15
+ DB.create_table! :users do
16
+ primary_key :id
17
+ Integer :main_blog_id
18
+
19
+ String :first_name
20
+ String :last_name
11
21
  String :email
12
22
  end
13
23
 
@@ -54,16 +64,29 @@ Praxis::Mapper::ConnectionManager.setup do
54
64
  end
55
65
  end
56
66
 
67
+ class BlogModel < Sequel::Model(:blogs)
68
+ include Praxis::Mapper::SequelCompat
69
+ many_to_one :owner, class: 'UserModel', key: :owner_id
70
+ many_to_one :administrator, class: 'UserModel', key: :administrator_id
71
+
72
+ end
73
+
57
74
  class UserModel < Sequel::Model(:users)
58
75
  include Praxis::Mapper::SequelCompat
59
76
 
60
77
  repository_name :sequel
61
78
 
62
- one_to_many :posts, class: 'PostModel'
63
- one_to_many :comments, class: 'CommentModel'
79
+ one_to_many :posts, class: 'PostModel', key: :post_id
80
+ one_to_many :comments, class: 'CommentModel', key: :author_id
81
+ one_to_many :blogs, class: 'BlogModel', key: :owner_id
82
+
83
+ one_to_many :administered_blogs, class: 'BlogModel', key: :administrator_id
64
84
 
65
85
  many_to_many :commented_posts, class: 'PostModel',
66
- join_table: 'comments', join_model: 'CommentModel'
86
+ join_table: 'comments', join_model: 'CommentModel',
87
+ through: [:comments, :post]
88
+
89
+ many_to_one :main_blog, class: 'BlogModel', key: :main_blog_id
67
90
  end
68
91
 
69
92
  class PostModel < Sequel::Model(:posts)
@@ -89,7 +112,7 @@ end
89
112
 
90
113
  class CompositeIdSequelModel < Sequel::Model(:composite_ids)
91
114
  include Praxis::Mapper::SequelCompat
92
-
115
+
93
116
  repository_name :sequel
94
117
 
95
118
  set_primary_key [:id, :type]
@@ -101,9 +124,9 @@ end
101
124
 
102
125
  class OtherSequelModel < Sequel::Model(:others)
103
126
  include Praxis::Mapper::SequelCompat
104
-
127
+
105
128
  repository_name :sequel
106
-
129
+
107
130
  many_to_one :composite,
108
131
  class: 'CompositeIdSequelModel',
109
132
  key: [:composite_id, :composite_type]
@@ -0,0 +1,57 @@
1
+ class BlogResource < BaseResource
2
+ property :display_name, dependencies: [:name]
3
+ property :owner_email, dependencies: ['owner.email']
4
+ property :owner_full_name, dependencies: ['owner.full_name']
5
+ property :everything, dependencies: [:*]
6
+ property :everything_from_owner, dependencies: ['owner.*']
7
+ property :kind, dependencies: nil
8
+
9
+ model BlogModel
10
+
11
+ def kind
12
+ self.class.name.demodulize
13
+ end
14
+
15
+ def display_name
16
+ self.name
17
+ end
18
+
19
+ def owner_email
20
+ self.owner.email
21
+ end
22
+
23
+ def owner_full_name
24
+ self.owner.full_name
25
+ end
26
+
27
+ def everything
28
+ 'everything'
29
+ end
30
+
31
+ def everything_from_owner
32
+ 'everything_from_owner'
33
+ end
34
+
35
+ end
36
+
37
+ class UserResource < BaseResource
38
+ model UserModel
39
+
40
+ def full_name
41
+ "#{first_name} #{last_name}"
42
+ end
43
+ property :full_name, dependencies: [:first_name, :last_name]
44
+
45
+ def blogs_summary
46
+ {
47
+ href: "www.foo.com/#{self.id}",
48
+ size: blogs.size
49
+ }
50
+ end
51
+
52
+ property :blogs_summary, dependencies: [:id, :blogs]
53
+ end
54
+
55
+ class CommentResource < BaseResource
56
+ model CommentModel
57
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: praxis-mapper
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.2
4
+ version: '4.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep M. Blanquer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-09-26 00:00:00.000000000 Z
12
+ date: 2015-10-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: randexp
@@ -235,6 +235,20 @@ dependencies:
235
235
  - - ">="
236
236
  - !ruby/object:Gem::Version
237
237
  version: '0'
238
+ - !ruby/object:Gem::Dependency
239
+ name: coveralls
240
+ requirement: !ruby/object:Gem::Requirement
241
+ requirements:
242
+ - - ">="
243
+ - !ruby/object:Gem::Version
244
+ version: '0'
245
+ type: :development
246
+ prerelease: false
247
+ version_requirements: !ruby/object:Gem::Requirement
248
+ requirements:
249
+ - - ">="
250
+ - !ruby/object:Gem::Version
251
+ version: '0'
238
252
  description:
239
253
  email:
240
254
  - blanquer@gmail.com
@@ -267,6 +281,7 @@ files:
267
281
  - lib/praxis-mapper/query/sql.rb
268
282
  - lib/praxis-mapper/query_statistics.rb
269
283
  - lib/praxis-mapper/resource.rb
284
+ - lib/praxis-mapper/selector_generator.rb
270
285
  - lib/praxis-mapper/sequel_compat.rb
271
286
  - lib/praxis-mapper/support.rb
272
287
  - lib/praxis-mapper/support/factory_girl.rb
@@ -289,6 +304,7 @@ files:
289
304
  - spec/praxis-mapper/query/sequel_spec.rb
290
305
  - spec/praxis-mapper/query/sql_spec.rb
291
306
  - spec/praxis-mapper/resource_spec.rb
307
+ - spec/praxis-mapper/selector_generator_spec.rb
292
308
  - spec/praxis-mapper/sequel_compat_spec.rb
293
309
  - spec/praxis_mapper_spec.rb
294
310
  - spec/spec_fixtures.rb
@@ -296,6 +312,7 @@ files:
296
312
  - spec/support/spec_models.rb
297
313
  - spec/support/spec_resources.rb
298
314
  - spec/support/spec_sequel_models.rb
315
+ - spec/support/spec_sequel_resources.rb
299
316
  homepage: https://github.com/rightscale/praxis-mapper
300
317
  licenses:
301
318
  - MIT
@@ -334,6 +351,7 @@ test_files:
334
351
  - spec/praxis-mapper/query/sequel_spec.rb
335
352
  - spec/praxis-mapper/query/sql_spec.rb
336
353
  - spec/praxis-mapper/resource_spec.rb
354
+ - spec/praxis-mapper/selector_generator_spec.rb
337
355
  - spec/praxis-mapper/sequel_compat_spec.rb
338
356
  - spec/praxis_mapper_spec.rb
339
357
  - spec/spec_fixtures.rb
@@ -341,4 +359,5 @@ test_files:
341
359
  - spec/support/spec_models.rb
342
360
  - spec/support/spec_resources.rb
343
361
  - spec/support/spec_sequel_models.rb
362
+ - spec/support/spec_sequel_resources.rb
344
363
  has_rdoc: