praxis 0.21 → 2.0.pre.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +8 -15
- data/CHANGELOG.md +328 -299
- data/CONTRIBUTING.md +4 -4
- data/README.md +11 -9
- data/lib/api_browser/app/js/directives/attribute_table.js +2 -1
- data/lib/api_browser/app/js/directives/conditional_requirements.js +13 -0
- data/lib/api_browser/app/js/directives/type_placeholder.js +10 -1
- data/lib/api_browser/app/js/factories/normalize_attributes.js +4 -2
- data/lib/api_browser/app/js/factories/template_for.js +5 -2
- data/lib/api_browser/app/js/filters/has_requirement.js +14 -0
- data/lib/api_browser/app/js/filters/tag_requirement.js +13 -0
- data/lib/api_browser/app/sass/praxis.scss +11 -0
- data/lib/api_browser/app/views/action.html +2 -2
- data/lib/api_browser/app/views/directives/attribute_description/member_options.html +2 -2
- data/lib/api_browser/app/views/directives/attribute_table.html +1 -1
- data/lib/api_browser/app/views/type.html +1 -1
- data/lib/api_browser/app/views/type/details.html +2 -2
- data/lib/api_browser/app/views/types/embedded/array.html +2 -0
- data/lib/api_browser/app/views/types/embedded/default.html +3 -1
- data/lib/api_browser/app/views/types/embedded/requirements.html +6 -0
- data/lib/api_browser/app/views/types/embedded/single_req.html +9 -0
- data/lib/api_browser/app/views/types/embedded/struct.html +14 -2
- data/lib/api_browser/app/views/types/standalone/array.html +1 -1
- data/lib/api_browser/app/views/types/standalone/struct.html +2 -1
- data/lib/api_browser/package.json +1 -1
- data/lib/praxis.rb +9 -3
- data/lib/praxis/action_definition.rb +1 -1
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
- data/lib/praxis/application.rb +1 -9
- data/lib/praxis/bootloader.rb +1 -4
- data/lib/praxis/config.rb +1 -1
- data/lib/praxis/dispatcher.rb +10 -6
- data/lib/praxis/docs/generator.rb +2 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +180 -0
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +273 -0
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
- data/lib/praxis/extensions/field_selection.rb +1 -9
- data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +51 -0
- data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +61 -0
- data/lib/praxis/extensions/rails_compat.rb +2 -0
- data/lib/praxis/extensions/rails_compat/request_methods.rb +19 -0
- data/lib/praxis/handlers/xml.rb +1 -1
- data/lib/praxis/mapper/active_model_compat.rb +98 -0
- data/lib/praxis/mapper/resource.rb +242 -0
- data/lib/praxis/mapper/selector_generator.rb +149 -0
- data/lib/praxis/mapper/sequel_compat.rb +76 -0
- data/lib/praxis/media_type_identifier.rb +2 -1
- data/lib/praxis/middleware_app.rb +20 -2
- data/lib/praxis/multipart/parser.rb +14 -2
- data/lib/praxis/notifications.rb +1 -1
- data/lib/praxis/plugins/mapper_plugin.rb +64 -0
- data/lib/praxis/plugins/rails_plugin.rb +104 -0
- data/lib/praxis/request.rb +7 -1
- data/lib/praxis/request_superclassing.rb +11 -0
- data/lib/praxis/resource_definition.rb +5 -5
- data/lib/praxis/response.rb +1 -1
- data/lib/praxis/route.rb +1 -1
- data/lib/praxis/routing_config.rb +1 -1
- data/lib/praxis/trait.rb +1 -1
- data/lib/praxis/types/media_type_common.rb +2 -2
- data/lib/praxis/types/multipart.rb +1 -1
- data/lib/praxis/types/multipart_array.rb +2 -2
- data/lib/praxis/types/multipart_array/part_definition.rb +1 -1
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +14 -13
- data/spec/functional_spec.rb +4 -7
- data/spec/praxis/action_definition_spec.rb +1 -1
- data/spec/praxis/application_spec.rb +1 -1
- data/spec/praxis/collection_spec.rb +3 -2
- data/spec/praxis/config_spec.rb +2 -2
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +106 -0
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +147 -0
- data/spec/praxis/extensions/field_selection/support/spec_resources_active_model.rb +130 -0
- data/spec/praxis/extensions/field_selection/support/spec_resources_sequel.rb +106 -0
- data/spec/praxis/handlers/xml_spec.rb +2 -2
- data/spec/praxis/mapper/resource_spec.rb +169 -0
- data/spec/praxis/mapper/selector_generator_spec.rb +293 -0
- data/spec/praxis/media_type_spec.rb +0 -10
- data/spec/praxis/middleware_app_spec.rb +29 -9
- data/spec/praxis/request_stages/action_spec.rb +8 -1
- data/spec/praxis/response_definition_spec.rb +7 -4
- data/spec/praxis/response_spec.rb +1 -1
- data/spec/praxis/responses/internal_server_error_spec.rb +2 -2
- data/spec/praxis/responses/validation_error_spec.rb +2 -2
- data/spec/praxis/router_spec.rb +1 -1
- data/spec/spec_app/app/controllers/instances.rb +1 -1
- data/spec/spec_app/config/environment.rb +3 -21
- data/spec/spec_helper.rb +11 -15
- data/spec/support/be_deep_equal_matcher.rb +39 -0
- data/spec/support/spec_resources.rb +124 -0
- data/tasks/thor/templates/generator/empty_app/Gemfile +3 -3
- metadata +102 -77
- data/.ruby-version +0 -1
- data/lib/praxis/extensions/mapper_selectors.rb +0 -16
- data/lib/praxis/media_type_collection.rb +0 -127
- data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
- data/lib/praxis/stats.rb +0 -113
- data/spec/praxis/media_type_collection_spec.rb +0 -157
- data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
- data/spec/praxis/stats_spec.rb +0 -9
- data/spec/spec_app/app/models/person.rb +0 -3
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
require 'praxis/mapper/active_model_compat'
|
4
|
+
|
5
|
+
# Creates a new in-memory DB, and the necessary tables (and mini-seeds) for the models in this file
|
6
|
+
def create_tables
|
7
|
+
|
8
|
+
ActiveRecord::Base.establish_connection(
|
9
|
+
adapter: 'sqlite3',
|
10
|
+
dbfile: ':memory:',
|
11
|
+
database: ':memory:'
|
12
|
+
)
|
13
|
+
|
14
|
+
ActiveRecord::Schema.define do
|
15
|
+
ActiveRecord::Migration.suppress_messages do
|
16
|
+
create_table :active_books do |table|
|
17
|
+
table.column :simple_name, :string
|
18
|
+
table.column :added_column, :string
|
19
|
+
table.column :category_uuid, :string
|
20
|
+
table.column :author_id, :integer
|
21
|
+
end
|
22
|
+
|
23
|
+
create_table :active_authors do |table|
|
24
|
+
table.column :name, :string
|
25
|
+
end
|
26
|
+
|
27
|
+
create_table :active_categories do |table|
|
28
|
+
table.column :uuid, :string
|
29
|
+
table.column :name, :string
|
30
|
+
end
|
31
|
+
|
32
|
+
create_table :active_tags do |table|
|
33
|
+
table.column :name, :string
|
34
|
+
end
|
35
|
+
|
36
|
+
create_table :active_taggings do |table|
|
37
|
+
table.column :book_id, :integer
|
38
|
+
table.column :tag_id, :integer
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
create_tables
|
45
|
+
|
46
|
+
class ActiveBook < ActiveRecord::Base
|
47
|
+
include Praxis::Mapper::ActiveModelCompat
|
48
|
+
|
49
|
+
belongs_to :category, class_name: 'ActiveCategory', foreign_key: :category_uuid, primary_key: :uuid
|
50
|
+
belongs_to :author, class_name: 'ActiveAuthor'
|
51
|
+
has_many :taggings, class_name: 'ActiveTagging', foreign_key: :book_id
|
52
|
+
has_many :tags, class_name: 'ActiveTag', through: :taggings
|
53
|
+
end
|
54
|
+
|
55
|
+
class ActiveAuthor < ActiveRecord::Base
|
56
|
+
include Praxis::Mapper::ActiveModelCompat
|
57
|
+
has_many :books, class_name: 'ActiveBook', foreign_key: :author_id
|
58
|
+
end
|
59
|
+
|
60
|
+
class ActiveCategory < ActiveRecord::Base
|
61
|
+
include Praxis::Mapper::ActiveModelCompat
|
62
|
+
has_many :books, class_name: 'ActiveBook', primary_key: :uuid, foreign_key: :category_uuid
|
63
|
+
end
|
64
|
+
|
65
|
+
class ActiveTag < ActiveRecord::Base
|
66
|
+
include Praxis::Mapper::ActiveModelCompat
|
67
|
+
end
|
68
|
+
|
69
|
+
class ActiveTagging < ActiveRecord::Base
|
70
|
+
include Praxis::Mapper::ActiveModelCompat
|
71
|
+
belongs_to :book, class_name: 'ActiveBook', foreign_key: :book_id
|
72
|
+
belongs_to :tag, class_name: 'ActiveTag', foreign_key: :tag_id
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
# A set of resource classes for use in specs
|
77
|
+
class ActiveBaseResource < Praxis::Mapper::Resource
|
78
|
+
end
|
79
|
+
|
80
|
+
class ActiveAuthorResource < ActiveBaseResource
|
81
|
+
model ActiveAuthor
|
82
|
+
|
83
|
+
property :display_name, dependencies: [:name]
|
84
|
+
end
|
85
|
+
|
86
|
+
class ActiveCategoryResource < ActiveBaseResource
|
87
|
+
model ActiveCategory
|
88
|
+
end
|
89
|
+
|
90
|
+
class ActiveTagResource < ActiveBaseResource
|
91
|
+
model ActiveTag
|
92
|
+
end
|
93
|
+
|
94
|
+
class ActiveBookResource < ActiveBaseResource
|
95
|
+
model ActiveBook
|
96
|
+
|
97
|
+
# Forces to add an extra column (added_column)...and yet another (author_id) that will serve
|
98
|
+
# to check that if that's already automatically added due to an association, it won't interfere or duplicate
|
99
|
+
property :author, dependencies: [:author, :added_column, :author_id]
|
100
|
+
|
101
|
+
property :name, dependencies: [:simple_name]
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def seed_data
|
106
|
+
cat1 = ActiveCategory.create( id: 1 , uuid: 'deadbeef1', name: 'cat1' )
|
107
|
+
cat2 = ActiveCategory.create( id: 2 , uuid: 'deadbeef2', name: 'cat2' )
|
108
|
+
|
109
|
+
author1 = ActiveAuthor.create( id: 11, name: 'author1' )
|
110
|
+
author2 = ActiveAuthor.create( id: 22, name: 'author2' )
|
111
|
+
|
112
|
+
tag_blue = ActiveTag.create(id: 1 , name: 'blue' )
|
113
|
+
tag_red = ActiveTag.create(id: 2 , name: 'red' )
|
114
|
+
|
115
|
+
book1 = ActiveBook.create( id: 1 , simple_name: 'Book1', category_uuid: 'deadbeef1')
|
116
|
+
book1.author = author1
|
117
|
+
book1.category = cat1
|
118
|
+
book1.save
|
119
|
+
ActiveTagging.create(book: book1, tag: tag_blue)
|
120
|
+
ActiveTagging.create(book: book1, tag: tag_red)
|
121
|
+
|
122
|
+
|
123
|
+
book2 = ActiveBook.create( id: 2 , simple_name: 'Book2', category_uuid: 'deadbeef1')
|
124
|
+
book2.author = author2
|
125
|
+
book2.category = cat2
|
126
|
+
book2.save
|
127
|
+
ActiveTagging.create(book: book2, tag: tag_red)
|
128
|
+
end
|
129
|
+
|
130
|
+
seed_data
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
|
3
|
+
require 'praxis/mapper/sequel_compat'
|
4
|
+
|
5
|
+
|
6
|
+
# Creates a new in-memory DB, and the necessary tables (and mini-seeds) for the sequel models in this file
|
7
|
+
def create_and_seed_tables
|
8
|
+
sequeldb = Sequel.sqlite
|
9
|
+
# sequeldb.loggers = [Logger.new($stdout)] # Uncomment to see sequel logs
|
10
|
+
|
11
|
+
sequeldb.create_table! :sequel_simple_models do
|
12
|
+
primary_key :id
|
13
|
+
String :simple_name
|
14
|
+
Integer :parent_id
|
15
|
+
String :parent_uuid
|
16
|
+
Integer :other_model_id
|
17
|
+
String :added_column
|
18
|
+
end
|
19
|
+
sequeldb.create_table! :sequel_other_models do
|
20
|
+
primary_key :id
|
21
|
+
end
|
22
|
+
sequeldb.create_table! :sequel_parent_models do
|
23
|
+
primary_key :id
|
24
|
+
String :uuid
|
25
|
+
end
|
26
|
+
sequeldb.create_table! :sequel_tag_models do
|
27
|
+
primary_key :id
|
28
|
+
String :tag_name
|
29
|
+
end
|
30
|
+
sequeldb.create_table! :sequel_simple_models_sequel_tag_models do
|
31
|
+
Integer :sequel_simple_model_id
|
32
|
+
Integer :tag_id
|
33
|
+
end
|
34
|
+
|
35
|
+
sequeldb[:sequel_parent_models] << { id: 1 , uuid: 'deadbeef1'}
|
36
|
+
sequeldb[:sequel_parent_models] << { id: 2 , uuid: 'deadbeef2'}
|
37
|
+
|
38
|
+
sequeldb[:sequel_other_models] << { id: 11 }
|
39
|
+
sequeldb[:sequel_other_models] << { id: 22 }
|
40
|
+
|
41
|
+
sequeldb[:sequel_tag_models] << { id: 1 , tag_name: 'blue' }
|
42
|
+
sequeldb[:sequel_tag_models] << { id: 2 , tag_name: 'red' }
|
43
|
+
|
44
|
+
# Simple model 1 is tagged as blue and red
|
45
|
+
sequeldb[:sequel_simple_models_sequel_tag_models] << { sequel_simple_model_id: 1, tag_id: 1 }
|
46
|
+
sequeldb[:sequel_simple_models_sequel_tag_models] << { sequel_simple_model_id: 1, tag_id: 2 }
|
47
|
+
# Simple model 2 is tagged as red
|
48
|
+
sequeldb[:sequel_simple_models_sequel_tag_models] << { sequel_simple_model_id: 2, tag_id: 2 }
|
49
|
+
|
50
|
+
# It's weird to have a parent id and parent uuid (which points to different actual parents)
|
51
|
+
# But it allows us to check pointing to both PKs and not PK columns
|
52
|
+
sequeldb[:sequel_simple_models] << { id: 1 , simple_name: 'Simple1', parent_id: 1, other_model_id: 11, parent_uuid: 'deadbeef1'}
|
53
|
+
sequeldb[:sequel_simple_models] << { id: 2 , simple_name: 'Simple2', parent_id: 2, other_model_id: 22, parent_uuid: 'deadbeef1'}
|
54
|
+
end
|
55
|
+
|
56
|
+
create_and_seed_tables
|
57
|
+
|
58
|
+
class SequelSimpleModel < Sequel::Model
|
59
|
+
include Praxis::Mapper::SequelCompat
|
60
|
+
|
61
|
+
many_to_one :parent, class: 'SequelParentModel'
|
62
|
+
many_to_one :other_model, class: 'SequelOtherModel'
|
63
|
+
many_to_many :tags, class: 'SequelTagModel'
|
64
|
+
end
|
65
|
+
|
66
|
+
class SequelOtherModel < Sequel::Model
|
67
|
+
include Praxis::Mapper::SequelCompat
|
68
|
+
end
|
69
|
+
|
70
|
+
class SequelParentModel < Sequel::Model
|
71
|
+
include Praxis::Mapper::SequelCompat
|
72
|
+
one_to_many :children, class: 'SequelSimpleModel', primary_key: :uuid, key: :parent_uuid
|
73
|
+
end
|
74
|
+
|
75
|
+
class SequelTagModel < Sequel::Model
|
76
|
+
include Praxis::Mapper::SequelCompat
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# A set of resource classes for use in specs
|
81
|
+
class SequelBaseResource < Praxis::Mapper::Resource
|
82
|
+
end
|
83
|
+
|
84
|
+
class SequelOtherResource < SequelBaseResource
|
85
|
+
model SequelOtherModel
|
86
|
+
|
87
|
+
property :display_name, dependencies: [:name]
|
88
|
+
end
|
89
|
+
|
90
|
+
class SequelParentResource < SequelBaseResource
|
91
|
+
model SequelParentModel
|
92
|
+
end
|
93
|
+
|
94
|
+
class SequelTagResource < SequelBaseResource
|
95
|
+
model SequelTagModel
|
96
|
+
end
|
97
|
+
|
98
|
+
class SequelSimpleResource < SequelBaseResource
|
99
|
+
model SequelSimpleModel
|
100
|
+
|
101
|
+
# Forces to add an extra column (added_column)...and yet another (parent_id) that will serve
|
102
|
+
# to check that if that's already automatically added due to an association, it won't interfere or duplicate
|
103
|
+
property :parent, dependencies: [:parent, :added_column, :parent_id]
|
104
|
+
|
105
|
+
property :name, dependencies: [:simple_name]
|
106
|
+
end
|
@@ -128,7 +128,7 @@ describe Praxis::Handlers::XML do
|
|
128
128
|
it_behaves_like 'xml something'
|
129
129
|
end
|
130
130
|
context 'a array with elements of all types' do
|
131
|
-
let(:parsed){ ["just text",:a,1,BigDecimal
|
131
|
+
let(:parsed){ ["just text",:a,1,BigDecimal(100),0.1,true,Date.new] }
|
132
132
|
it_behaves_like 'xml something'
|
133
133
|
end
|
134
134
|
context 'a hash with a complex substructure' do
|
@@ -137,7 +137,7 @@ describe Praxis::Handlers::XML do
|
|
137
137
|
"text" => "just text",
|
138
138
|
"symbol" => :a,
|
139
139
|
"num" => 1,
|
140
|
-
"bd" => BigDecimal
|
140
|
+
"bd" => BigDecimal(100),
|
141
141
|
"float" => 0.1,
|
142
142
|
"truthyness" => true,
|
143
143
|
"day" => Date.new,
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Praxis::Mapper::Resource do
|
4
|
+
let(:parent_record) { ParentModel.new(id: 100, name: 'george sr') }
|
5
|
+
let(:parent_records) { [ParentModel.new(id: 101, name: "georgia"),ParentModel.new(id: 102, name: 'georgina')] }
|
6
|
+
let(:record) { SimpleModel.new(id: 103, name: 'george xvi') }
|
7
|
+
let(:model) { SimpleModel}
|
8
|
+
|
9
|
+
context 'configuration' do
|
10
|
+
subject(:resource) { SimpleResource }
|
11
|
+
its(:model) { should == model }
|
12
|
+
|
13
|
+
context 'properties' do
|
14
|
+
subject(:properties) { resource.properties }
|
15
|
+
|
16
|
+
it 'includes directly-set properties' do
|
17
|
+
expect(properties[:other_resource]).to eq(dependencies: [:other_model])
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'inherits from a superclass' do
|
21
|
+
expect(properties[:href]).to eq(dependencies: [:id])
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'properly overrides a property from the parent' do
|
25
|
+
expect(properties[:name]).to eq(dependencies: [:simple_name])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'retrieving resources' do
|
31
|
+
context 'getting a single resource' do
|
32
|
+
before do
|
33
|
+
expect(SimpleModel).to receive(:get).with(name: 'george xvi').and_return(record)
|
34
|
+
end
|
35
|
+
|
36
|
+
subject(:resource) { SimpleResource.get(:name => 'george xvi') }
|
37
|
+
|
38
|
+
it { is_expected.to be_kind_of(SimpleResource) }
|
39
|
+
|
40
|
+
its(:record) { should be record }
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'getting multiple resources' do
|
44
|
+
before do
|
45
|
+
expect(SimpleModel).to receive(:all).with(name: ['george xvi']).and_return([record])
|
46
|
+
end
|
47
|
+
|
48
|
+
subject(:resource_collection) { SimpleResource.all(:name => ["george xvi"]) }
|
49
|
+
|
50
|
+
it { is_expected.to be_kind_of(Array) }
|
51
|
+
|
52
|
+
it 'fetches the models and wraps them' do
|
53
|
+
resource = resource_collection.first
|
54
|
+
expect(resource).to be_kind_of(SimpleResource)
|
55
|
+
expect(resource.record).to eq record
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'delegating to the underlying model' do
|
61
|
+
|
62
|
+
subject { SimpleResource.new(record) }
|
63
|
+
|
64
|
+
it 'does respond_to attributes in the model' do
|
65
|
+
expect(subject).to respond_to(:name)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'does not respond_to :id if the model does not have it' do
|
69
|
+
resource = OtherResource.new(OtherModel.new(:name => "foo"))
|
70
|
+
expect(resource).not_to respond_to(:id)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'returns raw results for simple attributes' do
|
74
|
+
expect(record).to receive(:name).and_call_original
|
75
|
+
expect(subject.name).to eq("george xvi")
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'wraps model objects in Resource instances' do
|
79
|
+
expect(record).to receive(:parent).and_return(parent_record)
|
80
|
+
|
81
|
+
parent = subject.parent
|
82
|
+
|
83
|
+
expect(parent).to be_kind_of(ParentResource)
|
84
|
+
expect(parent.name).to eq("george sr")
|
85
|
+
expect(parent.record).to eq(parent_record)
|
86
|
+
end
|
87
|
+
|
88
|
+
context "for serialized array associations" do
|
89
|
+
let(:record) { YamlArrayModel.new(:id => 1)}
|
90
|
+
|
91
|
+
subject { YamlArrayResource.new(record)}
|
92
|
+
|
93
|
+
it 'wraps arrays of model objects in an array of resource instances' do
|
94
|
+
expect(record).to receive(:parents).and_return(parent_records)
|
95
|
+
|
96
|
+
parents = subject.parents
|
97
|
+
expect(parents).to have(parent_records.size).items
|
98
|
+
expect(parents).to be_kind_of(Array)
|
99
|
+
|
100
|
+
parents.each { |parent| expect(parent).to be_kind_of(ParentResource) }
|
101
|
+
expect(parents.collect { |parent| parent.record }).to match_array(parent_records)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'resource_delegate' do
|
107
|
+
let(:other_name) { "foo" }
|
108
|
+
let(:other_attribute) { "other value" }
|
109
|
+
let(:other_record) { OtherModel.new(:name => other_name, :other_attribute => other_attribute)}
|
110
|
+
let(:other_resource) { OtherResource.new(other_record) }
|
111
|
+
|
112
|
+
let(:record) { SimpleModel.new(id: 105, name: "george xvi", other_name: other_name) }
|
113
|
+
|
114
|
+
subject(:resource) { SimpleResource.new(record) }
|
115
|
+
|
116
|
+
it 'delegates to the target' do
|
117
|
+
expect(record).to receive(:other_model).and_return(other_record)
|
118
|
+
expect(resource.other_attribute).to eq(other_attribute)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
context "memoized resource creation" do
|
124
|
+
let(:other_name) { "foo" }
|
125
|
+
let(:other_attribute) { "other value" }
|
126
|
+
let(:other_record) { OtherModel.new(:name => other_name, :other_attribute => other_attribute)}
|
127
|
+
let(:other_resource) { OtherResource.new(other_record) }
|
128
|
+
let(:record) { SimpleModel.new(id: 105, name: "george xvi", other_name: other_name) }
|
129
|
+
|
130
|
+
subject(:resource) { SimpleResource.new(record) }
|
131
|
+
|
132
|
+
it 'memoizes related resource creation' do
|
133
|
+
allow(record).to receive(:other_model).and_return(other_record)
|
134
|
+
expect(resource.other_resource).to be(SimpleResource.new(record).other_resource)
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
context ".wrap" do
|
141
|
+
it 'memoizes resource creation' do
|
142
|
+
expect(SimpleResource.wrap(record)).to be(SimpleResource.wrap(record))
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'works with nil resources, returning an empty set' do
|
146
|
+
wrapped_obj = SimpleResource.wrap(nil)
|
147
|
+
expect(wrapped_obj).to be_kind_of(Array)
|
148
|
+
expect(wrapped_obj.length).to be(0)
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'works array with nil member, returning only existing records' do
|
152
|
+
wrapped_set = SimpleResource.wrap([nil, record])
|
153
|
+
expect(wrapped_set).to be_kind_of(Array)
|
154
|
+
expect(wrapped_set.length).to be(1)
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'works with non-enumerable objects, that respond to collect' do
|
158
|
+
collectable = double("ArrayProxy", to_a: [record, record] )
|
159
|
+
|
160
|
+
wrapped_set = SimpleResource.wrap(collectable)
|
161
|
+
expect(wrapped_set.length).to be(2)
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'works regardless of the resource class used' do
|
165
|
+
expect(SimpleResource.wrap(record)).to be(OtherResource.wrap(record))
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
describe Praxis::Mapper::SelectorGenerator do
|
5
|
+
let(:resource) { SimpleResource }
|
6
|
+
subject(:generator) {described_class.new }
|
7
|
+
|
8
|
+
context '#add' do
|
9
|
+
let(:resource) { SimpleResource }
|
10
|
+
shared_examples 'a proper selector' do
|
11
|
+
it { expect(generator.add(resource, fields).dump).to be_deep_equal selectors }
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'basic combos' do
|
15
|
+
context 'direct column fields' do
|
16
|
+
let(:fields) { {id: true, foobar: true} }
|
17
|
+
let(:selectors) do
|
18
|
+
{
|
19
|
+
model: SimpleModel,
|
20
|
+
columns: [:id, :foobar]
|
21
|
+
}
|
22
|
+
end
|
23
|
+
it_behaves_like 'a proper selector'
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'aliased column fields' do
|
27
|
+
let(:fields) { {id: true, name: true} }
|
28
|
+
let(:selectors) do
|
29
|
+
{
|
30
|
+
model: SimpleModel,
|
31
|
+
columns: [:id, :simple_name]
|
32
|
+
}
|
33
|
+
end
|
34
|
+
it_behaves_like 'a proper selector'
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'pure associations without recursion' do
|
38
|
+
let(:fields) { {other_model: true} }
|
39
|
+
let(:selectors) do
|
40
|
+
{
|
41
|
+
model: SimpleModel,
|
42
|
+
columns: [:other_model_id], # FK of the other_model association
|
43
|
+
tracks: {
|
44
|
+
other_model: {
|
45
|
+
columns: [:id], # joining key for the association
|
46
|
+
model: OtherModel
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
end
|
51
|
+
it_behaves_like 'a proper selector'
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'aliased associations without recursion' do
|
55
|
+
let(:fields) { {other_resource: true} }
|
56
|
+
let(:selectors) do
|
57
|
+
{
|
58
|
+
model: SimpleModel,
|
59
|
+
columns: [:other_model_id], # FK of the other_model association
|
60
|
+
tracks: {
|
61
|
+
other_model: {
|
62
|
+
columns: [:id], # joining key for the association
|
63
|
+
model: OtherModel
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
end
|
68
|
+
it_behaves_like 'a proper selector'
|
69
|
+
end
|
70
|
+
context 'aliased associations without recursion (that map to columns and other associations)' do
|
71
|
+
let(:fields) { {aliased_method: true} }
|
72
|
+
let(:selectors) do
|
73
|
+
{
|
74
|
+
model: SimpleModel,
|
75
|
+
columns: [:column1, :other_model_id], # other_model_id => because of the association
|
76
|
+
tracks: {
|
77
|
+
other_model: {
|
78
|
+
columns: [:id], # joining key for the association
|
79
|
+
model: OtherModel
|
80
|
+
}
|
81
|
+
}
|
82
|
+
}
|
83
|
+
end
|
84
|
+
it_behaves_like 'a proper selector'
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'redefined associations that add some extra columns (would need both the underlying association AND the columns in place)' do
|
88
|
+
let(:fields) { {parent: true} }
|
89
|
+
let(:selectors) do
|
90
|
+
{
|
91
|
+
model: SimpleModel,
|
92
|
+
columns: [:parent_id, :added_column],
|
93
|
+
tracks: {
|
94
|
+
parent: {
|
95
|
+
columns: [:id],
|
96
|
+
model: ParentModel
|
97
|
+
}
|
98
|
+
}
|
99
|
+
}
|
100
|
+
end
|
101
|
+
it_behaves_like 'a proper selector'
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'a simple property that requires all fields' do
|
105
|
+
let(:fields) { {everything: true} }
|
106
|
+
let(:selectors) do
|
107
|
+
{
|
108
|
+
model: SimpleModel,
|
109
|
+
columns: [:*],
|
110
|
+
}
|
111
|
+
end
|
112
|
+
it_behaves_like 'a proper selector'
|
113
|
+
end
|
114
|
+
|
115
|
+
context 'a simple property that requires itself' do
|
116
|
+
let(:fields) { {circular_dep: true} }
|
117
|
+
let(:selectors) do
|
118
|
+
{
|
119
|
+
model: SimpleModel,
|
120
|
+
columns: [:circular_dep, :column1], #allows to "expand" the dependency into itself + others
|
121
|
+
}
|
122
|
+
end
|
123
|
+
it_behaves_like 'a proper selector'
|
124
|
+
end
|
125
|
+
|
126
|
+
context 'a simple property without dependencies' do
|
127
|
+
let(:fields) { {no_deps: true} }
|
128
|
+
let(:selectors) do
|
129
|
+
{
|
130
|
+
model: SimpleModel
|
131
|
+
}
|
132
|
+
end
|
133
|
+
it_behaves_like 'a proper selector'
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'nested tracking' do
|
139
|
+
context 'pure associations follow the nested fields' do
|
140
|
+
let(:fields) do
|
141
|
+
{
|
142
|
+
other_model: {
|
143
|
+
id: true
|
144
|
+
}
|
145
|
+
}
|
146
|
+
end
|
147
|
+
let(:selectors) do
|
148
|
+
{
|
149
|
+
model: SimpleModel,
|
150
|
+
columns: [:other_model_id],
|
151
|
+
tracks: {
|
152
|
+
other_model: {
|
153
|
+
model: OtherModel,
|
154
|
+
columns: [:id]
|
155
|
+
}
|
156
|
+
}
|
157
|
+
}
|
158
|
+
end
|
159
|
+
it_behaves_like 'a proper selector'
|
160
|
+
end
|
161
|
+
|
162
|
+
context 'Aliased resources disregard any nested fields...' do
|
163
|
+
let(:fields) do
|
164
|
+
{
|
165
|
+
other_resource: {
|
166
|
+
id: true
|
167
|
+
}
|
168
|
+
}
|
169
|
+
end
|
170
|
+
let(:selectors) do
|
171
|
+
{
|
172
|
+
model: SimpleModel,
|
173
|
+
columns: [:other_model_id],
|
174
|
+
tracks: {
|
175
|
+
other_model: {
|
176
|
+
model: OtherModel,
|
177
|
+
columns: [:id]
|
178
|
+
}
|
179
|
+
}
|
180
|
+
}
|
181
|
+
end
|
182
|
+
it_behaves_like 'a proper selector'
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context 'string associations' do
|
187
|
+
context 'that specify a direct existing colum in the target dependency' do
|
188
|
+
let(:fields) { { direct_other_name: true } }
|
189
|
+
let(:selectors) do
|
190
|
+
{
|
191
|
+
model: SimpleModel,
|
192
|
+
columns: [:other_model_id],
|
193
|
+
tracks: {
|
194
|
+
other_model: {
|
195
|
+
model: OtherModel,
|
196
|
+
columns: [:id, :name]
|
197
|
+
}
|
198
|
+
}
|
199
|
+
}
|
200
|
+
end
|
201
|
+
it_behaves_like 'a proper selector'
|
202
|
+
end
|
203
|
+
|
204
|
+
context 'that specify an aliased property in the target dependency' do
|
205
|
+
let(:fields) { { aliased_other_name: true } }
|
206
|
+
let(:selectors) do
|
207
|
+
{
|
208
|
+
model: SimpleModel,
|
209
|
+
columns: [:other_model_id],
|
210
|
+
tracks: {
|
211
|
+
other_model: {
|
212
|
+
model: OtherModel,
|
213
|
+
columns: [:id, :name]
|
214
|
+
}
|
215
|
+
}
|
216
|
+
}
|
217
|
+
end
|
218
|
+
it_behaves_like 'a proper selector'
|
219
|
+
end
|
220
|
+
|
221
|
+
context 'for a property that requires all fields from an association' do
|
222
|
+
let(:fields) { {everything_from_parent: true} }
|
223
|
+
let(:selectors) do
|
224
|
+
{
|
225
|
+
model: SimpleModel,
|
226
|
+
columns: [:parent_id],
|
227
|
+
tracks: {
|
228
|
+
parent: {
|
229
|
+
model: ParentModel,
|
230
|
+
columns: [:*]
|
231
|
+
}
|
232
|
+
}
|
233
|
+
}
|
234
|
+
end
|
235
|
+
it_behaves_like 'a proper selector'
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
context 'required extra select fields due to associations' do
|
240
|
+
context 'many_to_one' do
|
241
|
+
let(:fields) { {other_model: true} }
|
242
|
+
let(:selectors) do
|
243
|
+
{
|
244
|
+
model: SimpleModel,
|
245
|
+
columns: [:other_model_id], # FK of the other_model association
|
246
|
+
tracks: {
|
247
|
+
other_model: {
|
248
|
+
columns: [:id],
|
249
|
+
model: OtherModel
|
250
|
+
}
|
251
|
+
}
|
252
|
+
}
|
253
|
+
end
|
254
|
+
it_behaves_like 'a proper selector'
|
255
|
+
end
|
256
|
+
context 'one_to_many' do
|
257
|
+
let(:resource) { ParentResource }
|
258
|
+
let(:fields) { {simple_children: true} }
|
259
|
+
let(:selectors) do
|
260
|
+
{
|
261
|
+
model: ParentModel,
|
262
|
+
columns: [:id], # No FKs in the source model for one_to_many
|
263
|
+
tracks: {
|
264
|
+
simple_children: {
|
265
|
+
columns: [:parent_id],
|
266
|
+
model: SimpleModel
|
267
|
+
}
|
268
|
+
}
|
269
|
+
}
|
270
|
+
end
|
271
|
+
it_behaves_like 'a proper selector'
|
272
|
+
end
|
273
|
+
context 'many_to_many' do
|
274
|
+
let(:resource) { OtherResource }
|
275
|
+
let(:fields) { {simple_models: true} }
|
276
|
+
let(:selectors) do
|
277
|
+
{
|
278
|
+
model: OtherModel,
|
279
|
+
columns: [:id], #join key in the source model for many_to_many (where the middle table points to)
|
280
|
+
tracks: {
|
281
|
+
simple_models: {
|
282
|
+
columns: [:id], #join key in the target model for many_to_many (where the middle table points to)
|
283
|
+
model: SimpleModel
|
284
|
+
}
|
285
|
+
}
|
286
|
+
}
|
287
|
+
end
|
288
|
+
it_behaves_like 'a proper selector'
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|