praxis 0.21 → 2.0.pre.3
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 +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
|