praxis-mapper 4.1.2 → 4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: