praxis 2.0.pre.8 → 2.0.pre.13
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/.gitignore +2 -0
- data/.ruby-version +1 -1
- data/.travis.yml +1 -3
- data/CHANGELOG.md +33 -0
- data/TODO.md +1 -4
- data/bin/praxis +67 -12
- data/lib/praxis.rb +10 -3
- data/lib/praxis/action_definition.rb +15 -13
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +0 -7
- data/lib/praxis/api_general_info.rb +1 -1
- data/lib/praxis/application.rb +6 -2
- data/lib/praxis/blueprint.rb +357 -0
- data/lib/praxis/bootloader.rb +9 -3
- data/lib/praxis/bootloader_stages/environment.rb +16 -13
- data/lib/praxis/collection.rb +1 -11
- data/lib/praxis/config_hash.rb +44 -0
- data/lib/praxis/docs/{openapi → open_api}/info_object.rb +18 -10
- data/lib/praxis/docs/{openapi → open_api}/media_type_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/operation_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/parameter_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/paths_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/request_body_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/response_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/responses_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/schema_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/server_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/tag_object.rb +0 -0
- data/lib/praxis/docs/open_api_generator.rb +91 -6
- data/lib/praxis/endpoint_definition.rb +273 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +182 -58
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +47 -56
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +153 -0
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
- data/lib/praxis/extensions/field_expansion.rb +3 -36
- data/lib/praxis/extensions/pagination.rb +5 -32
- data/lib/praxis/extensions/pagination/ordering_params.rb +1 -1
- data/lib/praxis/extensions/pagination/pagination_params.rb +6 -4
- data/lib/praxis/field_expander.rb +90 -0
- data/lib/praxis/finalizable.rb +34 -0
- data/lib/praxis/mapper/active_model_compat.rb +4 -0
- data/lib/praxis/mapper/resource.rb +18 -2
- data/lib/praxis/mapper/selector_generator.rb +2 -1
- data/lib/praxis/mapper/sequel_compat.rb +7 -0
- data/lib/praxis/media_type.rb +3 -68
- data/lib/praxis/plugin_concern.rb +1 -1
- data/lib/praxis/plugins/mapper_plugin.rb +24 -15
- data/lib/praxis/plugins/pagination_plugin.rb +34 -4
- data/lib/praxis/renderer.rb +88 -0
- data/lib/praxis/request.rb +1 -1
- data/lib/praxis/resource_definition.rb +2 -311
- data/lib/praxis/response_definition.rb +2 -10
- data/lib/praxis/response_template.rb +3 -3
- data/lib/praxis/router.rb +2 -2
- data/lib/praxis/routing_config.rb +1 -1
- data/lib/praxis/tasks/api_docs.rb +17 -64
- data/lib/praxis/tasks/routes.rb +1 -1
- data/lib/praxis/types/media_type_common.rb +1 -11
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +0 -1
- data/spec/functional_spec.rb +5 -9
- data/spec/praxis/action_definition_spec.rb +12 -20
- data/spec/praxis/blueprint_spec.rb +373 -0
- data/spec/praxis/bootloader_spec.rb +10 -2
- data/spec/praxis/collection_spec.rb +0 -13
- data/spec/praxis/config_hash_spec.rb +64 -0
- data/spec/praxis/{resource_definition_spec.rb → endpoint_definition_spec.rb} +37 -64
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +249 -168
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +190 -8
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +140 -0
- data/spec/praxis/extensions/field_expansion_spec.rb +5 -24
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
- data/spec/praxis/field_expander_spec.rb +149 -0
- data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
- data/spec/praxis/media_type_identifier_spec.rb +5 -4
- data/spec/praxis/media_type_spec.rb +4 -93
- data/spec/praxis/renderer_spec.rb +188 -0
- data/spec/praxis/response_definition_spec.rb +0 -31
- data/spec/praxis/response_spec.rb +1 -1
- data/spec/praxis/router_spec.rb +8 -8
- data/spec/praxis/routing_config_spec.rb +3 -3
- data/spec/spec_app/app/controllers/instances.rb +13 -7
- data/spec/spec_app/design/media_types/instance.rb +1 -19
- data/spec/spec_app/design/media_types/volume.rb +1 -1
- data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -14
- data/spec/spec_app/design/resources/instances.rb +5 -8
- data/spec/spec_app/design/resources/volume_snapshots.rb +1 -1
- data/spec/spec_app/design/resources/volumes.rb +1 -1
- data/spec/support/spec_authorization_plugin.rb +1 -1
- data/spec/support/spec_blueprints.rb +72 -0
- data/spec/support/{spec_resource_definitions.rb → spec_endpoint_definitions.rb} +2 -2
- data/spec/support/spec_media_types.rb +6 -26
- data/tasks/thor/app.rb +8 -34
- data/tasks/thor/example.rb +51 -285
- data/tasks/thor/model.rb +40 -0
- data/tasks/thor/scaffold.rb +117 -0
- data/tasks/thor/templates/generator/empty_app/.gitignore +0 -1
- data/tasks/thor/templates/generator/empty_app/Gemfile +7 -23
- data/tasks/thor/templates/generator/empty_app/README.md +1 -1
- data/tasks/thor/templates/generator/empty_app/Rakefile +4 -13
- data/tasks/thor/templates/generator/empty_app/{design/response_templates → app/v1/resources}/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/{design/response_templates → app/v1/resources}/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/config/environment.rb +26 -17
- data/tasks/thor/templates/generator/empty_app/{design/v1/resources → config/initializers}/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/{design/v1/resources → config/initializers}/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/design/v1/endpoints/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/design/v1/endpoints/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/docs/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/docs/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/spec/spec_helper.rb +14 -9
- data/tasks/thor/templates/generator/example_app/.gitignore +1 -0
- data/tasks/thor/templates/generator/example_app/Gemfile +19 -0
- data/tasks/thor/templates/generator/example_app/Rakefile +61 -0
- data/tasks/thor/templates/generator/example_app/app/models/user.rb +6 -0
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
- data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +17 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +11 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +25 -0
- data/tasks/thor/templates/generator/example_app/config.ru +30 -0
- data/tasks/thor/templates/generator/example_app/config/environment.rb +41 -0
- data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +12 -0
- data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
- data/tasks/thor/templates/generator/example_app/design/api.rb +18 -0
- data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +37 -0
- data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +21 -0
- data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +20 -0
- data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +42 -0
- data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +37 -0
- data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
- data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
- data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
- data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
- data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
- metadata +64 -136
- data/lib/api_browser/.bowerrc +0 -3
- data/lib/api_browser/.editorconfig +0 -21
- data/lib/api_browser/Gruntfile.js +0 -581
- data/lib/api_browser/app/index.html +0 -59
- data/lib/api_browser/app/js/app.js +0 -48
- data/lib/api_browser/app/js/controllers/action.js +0 -47
- data/lib/api_browser/app/js/controllers/controller.js +0 -10
- data/lib/api_browser/app/js/controllers/menu.js +0 -93
- data/lib/api_browser/app/js/controllers/trait.js +0 -10
- data/lib/api_browser/app/js/controllers/type.js +0 -24
- data/lib/api_browser/app/js/directives/attribute_description.js +0 -56
- data/lib/api_browser/app/js/directives/attribute_table.js +0 -28
- data/lib/api_browser/app/js/directives/conditional_requirements.js +0 -13
- data/lib/api_browser/app/js/directives/fixed_if_fits.js +0 -38
- data/lib/api_browser/app/js/directives/highlight.js +0 -14
- data/lib/api_browser/app/js/directives/menu_item.js +0 -59
- data/lib/api_browser/app/js/directives/no_container.js +0 -8
- data/lib/api_browser/app/js/directives/readable_list.js +0 -87
- data/lib/api_browser/app/js/directives/request_examples.js +0 -31
- data/lib/api_browser/app/js/directives/type_placeholder.js +0 -30
- data/lib/api_browser/app/js/directives/url.js +0 -15
- data/lib/api_browser/app/js/factories/Configuration.js +0 -12
- data/lib/api_browser/app/js/factories/Documentation.js +0 -61
- data/lib/api_browser/app/js/factories/Example.js +0 -51
- data/lib/api_browser/app/js/factories/PageInfo.js +0 -9
- data/lib/api_browser/app/js/factories/normalize_attributes.js +0 -20
- data/lib/api_browser/app/js/factories/prepare_template.js +0 -15
- data/lib/api_browser/app/js/factories/template_for.js +0 -128
- data/lib/api_browser/app/js/filters/attribute_name.js +0 -10
- data/lib/api_browser/app/js/filters/friendly_json.js +0 -5
- data/lib/api_browser/app/js/filters/has_requirement.js +0 -14
- data/lib/api_browser/app/js/filters/header_info.js +0 -9
- data/lib/api_browser/app/js/filters/is_empty.js +0 -8
- data/lib/api_browser/app/js/filters/markdown.js +0 -6
- data/lib/api_browser/app/js/filters/resource_name.js +0 -5
- data/lib/api_browser/app/js/filters/tag_requirement.js +0 -13
- data/lib/api_browser/app/sass/modules/_body.scss +0 -40
- data/lib/api_browser/app/sass/modules/_cloke.scss +0 -8
- data/lib/api_browser/app/sass/modules/_header.scss +0 -10
- data/lib/api_browser/app/sass/modules/_nav.scss +0 -7
- data/lib/api_browser/app/sass/modules/_sidebar.scss +0 -134
- data/lib/api_browser/app/sass/modules/_switch.scss +0 -55
- data/lib/api_browser/app/sass/modules/_table.scss +0 -13
- data/lib/api_browser/app/sass/praxis.scss +0 -70
- data/lib/api_browser/app/sass/variables/_bootstrap-variables.scss +0 -774
- data/lib/api_browser/app/views/action.html +0 -97
- data/lib/api_browser/app/views/builtin/field-selector.html +0 -24
- data/lib/api_browser/app/views/controller.html +0 -55
- data/lib/api_browser/app/views/directives/attribute_description.html +0 -2
- data/lib/api_browser/app/views/directives/attribute_description/default.html +0 -2
- data/lib/api_browser/app/views/directives/attribute_description/example.html +0 -13
- data/lib/api_browser/app/views/directives/attribute_description/headers.html +0 -8
- data/lib/api_browser/app/views/directives/attribute_description/member_options.html +0 -4
- data/lib/api_browser/app/views/directives/attribute_description/values.html +0 -14
- data/lib/api_browser/app/views/directives/attribute_table.html +0 -17
- data/lib/api_browser/app/views/directives/menu_item.html +0 -8
- data/lib/api_browser/app/views/directives/url.html +0 -3
- data/lib/api_browser/app/views/examples/general.html +0 -26
- data/lib/api_browser/app/views/home.html +0 -5
- data/lib/api_browser/app/views/layout.html +0 -8
- data/lib/api_browser/app/views/menu.html +0 -42
- data/lib/api_browser/app/views/navbar.html +0 -9
- data/lib/api_browser/app/views/trait.html +0 -13
- data/lib/api_browser/app/views/type.html +0 -6
- data/lib/api_browser/app/views/type/details.html +0 -33
- data/lib/api_browser/app/views/types/embedded/array.html +0 -2
- data/lib/api_browser/app/views/types/embedded/default.html +0 -12
- data/lib/api_browser/app/views/types/embedded/field-selector.html +0 -13
- data/lib/api_browser/app/views/types/embedded/links.html +0 -11
- data/lib/api_browser/app/views/types/embedded/requirements.html +0 -6
- data/lib/api_browser/app/views/types/embedded/single_req.html +0 -9
- data/lib/api_browser/app/views/types/embedded/struct.html +0 -14
- data/lib/api_browser/app/views/types/label/link.html +0 -1
- data/lib/api_browser/app/views/types/label/primitive.html +0 -1
- data/lib/api_browser/app/views/types/label/primitive_collection.html +0 -1
- data/lib/api_browser/app/views/types/label/type.html +0 -1
- data/lib/api_browser/app/views/types/label/type_collection.html +0 -1
- data/lib/api_browser/app/views/types/main/array.html +0 -22
- data/lib/api_browser/app/views/types/main/default.html +0 -23
- data/lib/api_browser/app/views/types/main/hash.html +0 -23
- data/lib/api_browser/app/views/types/standalone/array.html +0 -3
- data/lib/api_browser/app/views/types/standalone/default.html +0 -18
- data/lib/api_browser/app/views/types/standalone/struct.html +0 -2
- data/lib/api_browser/bower_template.json +0 -41
- data/lib/api_browser/package-lock.json +0 -7110
- data/lib/api_browser/package.json +0 -43
- data/lib/praxis/docs/generator.rb +0 -243
- data/lib/praxis/docs/link_builder.rb +0 -30
- data/lib/praxis/links.rb +0 -135
- data/lib/praxis/types/multipart.rb +0 -109
- data/spec/api_browser/directives/type_placeholder_spec.js +0 -134
- data/spec/api_browser/factories/configuration_spec.js +0 -32
- data/spec/api_browser/factories/documentation_spec.js +0 -100
- data/spec/api_browser/factories/normalize_attributes_spec.js +0 -92
- data/spec/api_browser/factories/template_for_spec.js +0 -67
- data/spec/api_browser/filters/attribute_name_spec.js +0 -23
- data/spec/praxis/types/multipart_spec.rb +0 -112
- data/tasks/thor/templates/generator/empty_app/.rspec +0 -1
- data/tasks/thor/templates/generator/empty_app/Guardfile +0 -3
- data/tasks/thor/templates/generator/empty_app/config/rainbows.rb +0 -57
- data/tasks/thor/templates/generator/empty_app/docs/app.js +0 -1
- data/tasks/thor/templates/generator/empty_app/docs/styles.scss +0 -3
|
@@ -44,7 +44,11 @@ describe Praxis::Bootloader do
|
|
|
44
44
|
expect(bootloader.before(stage.name)).to eq('before!')
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
it "raises when given an invalid stage name"
|
|
47
|
+
it "raises when given an invalid stage name" do
|
|
48
|
+
expect{
|
|
49
|
+
bootloader.before('nope!')
|
|
50
|
+
}.to raise_error(Praxis::Exceptions::StageNotFound,/Error running a before block for stage nope!/)
|
|
51
|
+
end
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
context ".after" do
|
|
@@ -54,7 +58,11 @@ describe Praxis::Bootloader do
|
|
|
54
58
|
expect(bootloader.after(stage.name)).to eq('after!')
|
|
55
59
|
end
|
|
56
60
|
|
|
57
|
-
it "raises when given an invalid stage name"
|
|
61
|
+
it "raises when given an invalid stage name" do
|
|
62
|
+
expect{
|
|
63
|
+
bootloader.after('nope!')
|
|
64
|
+
}.to raise_error(Praxis::Exceptions::StageNotFound,/Error running an after block for stage nope!/)
|
|
65
|
+
end
|
|
58
66
|
end
|
|
59
67
|
|
|
60
68
|
context ".use" do
|
|
@@ -51,19 +51,6 @@ describe Praxis::Collection do
|
|
|
51
51
|
its(:identifier) { should eq Person.identifier + "; type=collection" }
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
context '.views' do
|
|
55
|
-
subject(:views) { collection.views }
|
|
56
|
-
its(:keys) { should match_array(member_type.views.keys)}
|
|
57
|
-
|
|
58
|
-
it 'generates CollectionViews from the member views' do
|
|
59
|
-
collection.views.each do |name, view|
|
|
60
|
-
expect(view.name).to be name
|
|
61
|
-
expect(view.schema).to be member_type
|
|
62
|
-
expect(view.contents).to eq member_type.views[name].contents
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
54
|
context '.load' do
|
|
68
55
|
let(:volume_data) do
|
|
69
56
|
{
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
|
3
|
+
|
|
4
|
+
describe Praxis::ConfigHash do
|
|
5
|
+
subject(:instance) { Praxis::ConfigHash.new(hash, &block) }
|
|
6
|
+
let(:hash) { { one: ['existing'], two: 'dos' } }
|
|
7
|
+
let(:block) do
|
|
8
|
+
proc { 'abc' }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
context 'initialization' do
|
|
12
|
+
it 'saves the passed hash' do
|
|
13
|
+
expect(subject.hash).to be(hash)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
context '.from' do
|
|
18
|
+
subject(:instance) { Praxis::ConfigHash.from(hash, &block) }
|
|
19
|
+
it 'returns an instance' do
|
|
20
|
+
expect(subject).to be_kind_of(Praxis::ConfigHash)
|
|
21
|
+
expect(subject.hash).to be(hash)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context '#to_hash' do
|
|
26
|
+
let(:block) do
|
|
27
|
+
proc { hash['i_was'] = 'here' }
|
|
28
|
+
end
|
|
29
|
+
it 'evaluates the block and returns the resulting hash' do
|
|
30
|
+
expect(subject.to_hash).to eq(subject.hash)
|
|
31
|
+
expect(subject.hash['i_was']).to eq('here')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context '#method_missing' do
|
|
36
|
+
context 'when keys do not exist in the hash key' do
|
|
37
|
+
it 'sets a single value to the hash' do
|
|
38
|
+
subject.some_name 'someval'
|
|
39
|
+
expect(subject.hash[:some_name]).to eq('someval')
|
|
40
|
+
end
|
|
41
|
+
it 'sets a multiple values to the hash key' do
|
|
42
|
+
subject.some_name 'someval', 'other1', 'other2'
|
|
43
|
+
expect(subject.hash[:some_name]).to include('someval', 'other1', 'other2')
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
context 'when keys already exist in the hash key' do
|
|
47
|
+
it 'adds one value to the hash' do
|
|
48
|
+
subject.one'newval'
|
|
49
|
+
expect(subject.hash[:one]).to match_array(%w(existing newval))
|
|
50
|
+
end
|
|
51
|
+
it 'adds multiple values to the hash key' do
|
|
52
|
+
subject.one 'newval', 'other1', 'other2'
|
|
53
|
+
expect(subject.hash[:one]).to match_array(%w(existing newval other1 other2))
|
|
54
|
+
end
|
|
55
|
+
context 'when passing a value and a block' do
|
|
56
|
+
let(:my_block) { proc {} }
|
|
57
|
+
it 'adds the tuple to the hash key' do
|
|
58
|
+
subject.one 'val', &my_block
|
|
59
|
+
expect(subject.hash[:one]).to include(['val', my_block])
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
|
|
3
|
-
describe Praxis::
|
|
4
|
-
subject(:
|
|
3
|
+
describe Praxis::EndpointDefinition do
|
|
4
|
+
subject(:endpoint_definition) { PeopleResource }
|
|
5
5
|
|
|
6
6
|
its(:description) { should eq('People resource') }
|
|
7
7
|
its(:media_type) { should eq(Person) }
|
|
@@ -14,10 +14,10 @@ describe Praxis::ResourceDefinition do
|
|
|
14
14
|
its(:metadata) { should_not have_key(:doc_visibility) }
|
|
15
15
|
|
|
16
16
|
context '.describe' do
|
|
17
|
-
subject(:describe) {
|
|
17
|
+
subject(:describe) { endpoint_definition.describe }
|
|
18
18
|
|
|
19
|
-
its([:description]) { should eq(
|
|
20
|
-
its([:media_type]) { should eq(
|
|
19
|
+
its([:description]) { should eq(endpoint_definition.description) }
|
|
20
|
+
its([:media_type]) { should eq(endpoint_definition.media_type.describe(true)) }
|
|
21
21
|
|
|
22
22
|
its([:actions]) { should have(2).items }
|
|
23
23
|
its([:metadata]) { should be_kind_of(Hash) }
|
|
@@ -25,7 +25,7 @@ describe Praxis::ResourceDefinition do
|
|
|
25
25
|
it { should_not have_key(:parent)}
|
|
26
26
|
|
|
27
27
|
context 'for a resource with a parent' do
|
|
28
|
-
let(:
|
|
28
|
+
let(:endpoint_definition) { ApiResources::VolumeSnapshots}
|
|
29
29
|
|
|
30
30
|
its([:parent]) { should eq ApiResources::Volumes.id }
|
|
31
31
|
end
|
|
@@ -34,29 +34,29 @@ describe Praxis::ResourceDefinition do
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
context '.routing_prefix' do
|
|
37
|
-
subject(:
|
|
37
|
+
subject(:endpoint_definition) { ApiResources::VolumeSnapshots }
|
|
38
38
|
it do
|
|
39
|
-
expect(
|
|
39
|
+
expect(endpoint_definition.routing_prefix).to eq('/clouds/:cloud_id/volumes/:volume_id/snapshots')
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
context '.parent_prefix' do
|
|
44
|
-
subject(:
|
|
44
|
+
subject(:endpoint_definition) { ApiResources::VolumeSnapshots }
|
|
45
45
|
let(:base_path){ Praxis::ApiDefinition.instance.info.base_path }
|
|
46
46
|
its(:parent_prefix){ should eq('/clouds/:cloud_id/volumes/:volume_id') }
|
|
47
47
|
it do
|
|
48
|
-
expect(
|
|
48
|
+
expect(endpoint_definition.parent_prefix).to_not match(/^#{base_path}/)
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
context '.action' do
|
|
54
54
|
it 'requires a block' do
|
|
55
|
-
expect {
|
|
55
|
+
expect { endpoint_definition.action(:something)
|
|
56
56
|
}.to raise_error(ArgumentError)
|
|
57
57
|
end
|
|
58
58
|
it 'creates an ActionDefinition for actions' do
|
|
59
|
-
index =
|
|
59
|
+
index = endpoint_definition.actions[:index]
|
|
60
60
|
expect(index).to be_kind_of(Praxis::ActionDefinition)
|
|
61
61
|
expect(index.description).to eq("index description")
|
|
62
62
|
end
|
|
@@ -64,7 +64,7 @@ describe Praxis::ResourceDefinition do
|
|
|
64
64
|
it 'complains if action names are not symbols' do
|
|
65
65
|
expect do
|
|
66
66
|
Class.new do
|
|
67
|
-
include Praxis::
|
|
67
|
+
include Praxis::EndpointDefinition
|
|
68
68
|
action "foo" do
|
|
69
69
|
end
|
|
70
70
|
end
|
|
@@ -73,9 +73,9 @@ describe Praxis::ResourceDefinition do
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
context 'action_defaults' do
|
|
76
|
-
let(:
|
|
76
|
+
let(:endpoint_definition) do
|
|
77
77
|
Class.new do
|
|
78
|
-
include Praxis::
|
|
78
|
+
include Praxis::EndpointDefinition
|
|
79
79
|
media_type Person
|
|
80
80
|
|
|
81
81
|
version '1.0'
|
|
@@ -135,13 +135,13 @@ describe Praxis::ResourceDefinition do
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
it 'are applied to actions' do
|
|
138
|
-
action =
|
|
138
|
+
action = endpoint_definition.actions[:show]
|
|
139
139
|
expect(action.params.attributes).to have_key(:id)
|
|
140
140
|
expect(action.route.path.to_s).to eq '/api/:base_param/people/:id'
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
context 'includes base_params from the APIDefinition' do
|
|
144
|
-
let(:show_action_params){
|
|
144
|
+
let(:show_action_params){ endpoint_definition.actions[:show].params }
|
|
145
145
|
|
|
146
146
|
it 'including globally defined' do
|
|
147
147
|
expect(show_action_params.attributes).to have_key(:base_param)
|
|
@@ -159,15 +159,15 @@ describe Praxis::ResourceDefinition do
|
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
context 'setting other values' do
|
|
162
|
-
subject(:
|
|
162
|
+
subject(:endpoint_definition) { Class.new {include Praxis::EndpointDefinition } }
|
|
163
163
|
|
|
164
164
|
let(:some_proc) { Proc.new {} }
|
|
165
165
|
let(:some_hash) { Hash.new }
|
|
166
166
|
|
|
167
167
|
it 'accepts a string as media_type' do
|
|
168
|
-
|
|
169
|
-
expect(
|
|
170
|
-
expect(
|
|
168
|
+
endpoint_definition.media_type('Something')
|
|
169
|
+
expect(endpoint_definition.media_type).to be_kind_of(Praxis::SimpleMediaType)
|
|
170
|
+
expect(endpoint_definition.media_type.identifier).to eq('Something')
|
|
171
171
|
end
|
|
172
172
|
|
|
173
173
|
its(:version_options){ should be_kind_of(Hash) }
|
|
@@ -176,57 +176,30 @@ describe Praxis::ResourceDefinition do
|
|
|
176
176
|
|
|
177
177
|
|
|
178
178
|
context '.trait' do
|
|
179
|
-
subject(:
|
|
179
|
+
subject(:endpoint_definition) { Class.new {include Praxis::EndpointDefinition } }
|
|
180
180
|
it 'raises an error for missing traits' do
|
|
181
|
-
expect {
|
|
181
|
+
expect { endpoint_definition.trait(:stuff) }.to raise_error(Praxis::Exceptions::InvalidTrait)
|
|
182
182
|
end
|
|
183
|
-
it '
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
context 'deprecated action methods' do
|
|
188
|
-
subject(:resource_definition) do
|
|
189
|
-
Class.new do
|
|
190
|
-
include Praxis::ResourceDefinition
|
|
191
|
-
|
|
192
|
-
def self.name
|
|
193
|
-
'FooBar'
|
|
194
|
-
end
|
|
183
|
+
it 'adds it to its list when it is available in the APIDefinition instance' do
|
|
184
|
+
trait_name = :test
|
|
185
|
+
expect(endpoint_definition.traits).to_not include(trait_name)
|
|
195
186
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
headers { key "Inherited-Header", String }
|
|
199
|
-
params { attribute :inherited_params, String }
|
|
200
|
-
response :not_found
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
action :index do
|
|
204
|
-
end
|
|
205
|
-
end
|
|
187
|
+
endpoint_definition.trait(trait_name)
|
|
188
|
+
expect(endpoint_definition.traits).to include(trait_name)
|
|
206
189
|
end
|
|
207
|
-
|
|
208
|
-
let(:action) { resource_definition.actions[:index] }
|
|
209
|
-
|
|
210
|
-
it 'are applied to the action' do
|
|
211
|
-
expect(action.payload.attributes).to have_key(:inherited_payload)
|
|
212
|
-
expect(action.headers.attributes).to have_key("Inherited-Header")
|
|
213
|
-
expect(action.params.attributes).to have_key(:inherited_params)
|
|
214
|
-
expect(action.responses).to have_key(:not_found)
|
|
215
|
-
end
|
|
216
|
-
|
|
217
190
|
end
|
|
218
191
|
|
|
219
192
|
context 'with nodoc! called' do
|
|
220
193
|
before do
|
|
221
|
-
|
|
194
|
+
endpoint_definition.nodoc!
|
|
222
195
|
end
|
|
223
196
|
|
|
224
197
|
it 'has the :doc_visibility option set' do
|
|
225
|
-
expect(
|
|
198
|
+
expect(endpoint_definition.metadata[:doc_visibility]).to be(:none)
|
|
226
199
|
end
|
|
227
200
|
|
|
228
201
|
it 'is exposed in the describe' do
|
|
229
|
-
expect(
|
|
202
|
+
expect(endpoint_definition.describe[:metadata][:doc_visibility]).to be(:none)
|
|
230
203
|
end
|
|
231
204
|
|
|
232
205
|
end
|
|
@@ -238,14 +211,14 @@ describe Praxis::ResourceDefinition do
|
|
|
238
211
|
end
|
|
239
212
|
it 'cannot be done if already been defined' do
|
|
240
213
|
expect{
|
|
241
|
-
|
|
214
|
+
endpoint_definition.canonical_path :reset
|
|
242
215
|
}.to raise_error(/'canonical_path' can only be defined once./)
|
|
243
216
|
end
|
|
244
217
|
end
|
|
245
218
|
context 'if none specified' do
|
|
246
|
-
subject(:
|
|
219
|
+
subject(:endpoint_definition) do
|
|
247
220
|
Class.new do
|
|
248
|
-
include Praxis::
|
|
221
|
+
include Praxis::EndpointDefinition
|
|
249
222
|
action :show do
|
|
250
223
|
end
|
|
251
224
|
end
|
|
@@ -255,9 +228,9 @@ describe Praxis::ResourceDefinition do
|
|
|
255
228
|
end
|
|
256
229
|
end
|
|
257
230
|
context 'with an undefined action' do
|
|
258
|
-
subject(:
|
|
231
|
+
subject(:endpoint_definition) do
|
|
259
232
|
Class.new do
|
|
260
|
-
include Praxis::
|
|
233
|
+
include Praxis::EndpointDefinition
|
|
261
234
|
canonical_path :non_existent
|
|
262
235
|
end
|
|
263
236
|
end
|
|
@@ -275,7 +248,7 @@ describe Praxis::ResourceDefinition do
|
|
|
275
248
|
end
|
|
276
249
|
end
|
|
277
250
|
context '#parse_href' do
|
|
278
|
-
let(:parsed){
|
|
251
|
+
let(:parsed){ endpoint_definition.parse_href("/people/1") }
|
|
279
252
|
it 'accesses the path expansion functions of the primary route' do
|
|
280
253
|
expect(parsed).to have_key(:id)
|
|
281
254
|
end
|
|
@@ -14,6 +14,7 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
|
14
14
|
shared_examples 'subject_equivalent_to' do |expected_result|
|
|
15
15
|
it do
|
|
16
16
|
loaded_ids = subject.all.map(&:id).sort
|
|
17
|
+
expected_result = expected_result.call if expected_result.is_a?(Proc)
|
|
17
18
|
expected_ids = expected_result.all.map(&:id).sort
|
|
18
19
|
expect(loaded_ids).to_not be_empty
|
|
19
20
|
expect(loaded_ids).to eq(expected_ids)
|
|
@@ -23,12 +24,11 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
|
23
24
|
# Poorman's way to compare SQL queries...
|
|
24
25
|
shared_examples 'subject_matches_sql' do |expected_sql|
|
|
25
26
|
it do
|
|
26
|
-
|
|
27
|
-
gen_sql = subject.all.to_sql.gsub(/[()]/,'')
|
|
27
|
+
gen_sql = subject.all.to_sql
|
|
28
28
|
# Strip blank at the beggining (and end) of every line
|
|
29
29
|
# ...and recompose it by adding an extra space at the beginning of each one instead
|
|
30
30
|
exp = expected_sql.split(/\n/).map do |line|
|
|
31
|
-
" " + line.strip
|
|
31
|
+
" " + line.strip
|
|
32
32
|
end.join.strip
|
|
33
33
|
expect(gen_sql).to eq(exp)
|
|
34
34
|
end
|
|
@@ -37,9 +37,9 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
|
37
37
|
context 'initialize' do
|
|
38
38
|
it 'sets the right things to the instance' do
|
|
39
39
|
instance
|
|
40
|
-
expect(instance.
|
|
40
|
+
expect(instance.instance_variable_get(:@initial_query)).to eq(base_query)
|
|
41
41
|
expect(instance.model).to eq(base_model)
|
|
42
|
-
expect(instance.
|
|
42
|
+
expect(instance.filters_map).to eq(filters_map)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
context 'generate' do
|
|
@@ -52,197 +52,278 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
|
52
52
|
expect(subject).to be(base_query)
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
|
-
context '
|
|
56
|
-
context '
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
|
|
67
|
-
end
|
|
68
|
-
end
|
|
55
|
+
context 'with flat AND conditions' do
|
|
56
|
+
context 'by a simple field' do
|
|
57
|
+
context 'that maps to the same name' do
|
|
58
|
+
let(:filters_string) { 'category_uuid=deadbeef1' }
|
|
59
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
|
|
60
|
+
end
|
|
61
|
+
context 'same-name filter mapping works' do
|
|
62
|
+
context 'even if ther was not a filter explicitly defined for it' do
|
|
63
|
+
let(:filters_string) { 'category_uuid=deadbeef1' }
|
|
64
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
|
|
65
|
+
end
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
67
|
+
context 'but if it is a field that does not exist in the model' do
|
|
68
|
+
let(:filters_string) { 'nonexisting=valuehere' }
|
|
69
|
+
it 'it blows up with the right error' do
|
|
70
|
+
expect{subject}.to raise_error(/Filtering by nonexisting is not allowed/)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
context 'that maps to a different name' do
|
|
75
|
+
let(:filters_string) { 'name=Book1'}
|
|
76
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
|
|
77
|
+
end
|
|
78
|
+
context 'that is mapped as a nested struct' do
|
|
79
|
+
let(:filters_string) { 'fake_nested.name=Book1'}
|
|
80
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
|
|
81
|
+
end
|
|
82
82
|
end
|
|
83
|
-
end
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
end
|
|
99
|
-
context '= (with array)' do
|
|
100
|
-
let(:filters_string) { 'author.id=11,22'}
|
|
101
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IN (11,22)')
|
|
102
|
-
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
103
|
-
WHERE "#{PREF}/author"."id" IN (11,22)
|
|
104
|
-
SQL
|
|
105
|
-
end
|
|
106
|
-
context '!=' do
|
|
107
|
-
let(:filters_string) { 'author.id!=11'}
|
|
108
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <> 11')
|
|
109
|
-
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
110
|
-
WHERE "#{PREF}/author"."id" <> 11
|
|
111
|
-
SQL
|
|
112
|
-
end
|
|
113
|
-
context '!= (with array)' do
|
|
114
|
-
let(:filters_string) { 'author.id!=11,888'}
|
|
115
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id NOT IN (11,888)')
|
|
116
|
-
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
117
|
-
WHERE "#{PREF}/author"."id" NOT IN (11,888)
|
|
118
|
-
SQL
|
|
119
|
-
end
|
|
120
|
-
context '>' do
|
|
121
|
-
let(:filters_string) { 'author.id>1'}
|
|
122
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id > 1')
|
|
123
|
-
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
124
|
-
WHERE "#{PREF}/author"."id" > 1
|
|
125
|
-
SQL
|
|
126
|
-
end
|
|
127
|
-
context '<' do
|
|
128
|
-
let(:filters_string) { 'author.id<22'}
|
|
129
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id < 22')
|
|
130
|
-
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
131
|
-
WHERE "#{PREF}/author"."id" < 22
|
|
132
|
-
SQL
|
|
133
|
-
end
|
|
134
|
-
context '>=' do
|
|
135
|
-
let(:filters_string) { 'author.id>=22'}
|
|
136
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id >= 22')
|
|
137
|
-
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
138
|
-
WHERE "#{PREF}/author"."id" >= 22
|
|
139
|
-
SQL
|
|
140
|
-
end
|
|
141
|
-
context '<=' do
|
|
142
|
-
let(:filters_string) { 'author.id<=22'}
|
|
143
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <= 22')
|
|
144
|
-
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
145
|
-
WHERE "#{PREF}/author"."id" <= 22
|
|
146
|
-
SQL
|
|
147
|
-
end
|
|
148
|
-
context '!' do
|
|
149
|
-
let(:filters_string) { 'author.id!'}
|
|
150
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IS NOT NULL')
|
|
151
|
-
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
152
|
-
WHERE "#{PREF}/author"."id" IS NOT NULL
|
|
153
|
-
SQL
|
|
84
|
+
context 'by a field or a related model' do
|
|
85
|
+
context 'for a belongs_to association' do
|
|
86
|
+
let(:filters_string) { 'author.name=author2'}
|
|
87
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name' => 'author2')
|
|
88
|
+
end
|
|
89
|
+
context 'for a has_many association' do
|
|
90
|
+
let(:filters_string) { 'taggings.label=primary' }
|
|
91
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
|
|
92
|
+
end
|
|
93
|
+
context 'for a has_many through association' do
|
|
94
|
+
let(:filters_string) { 'tags.name=blue' }
|
|
95
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:tags).where('active_tags.name' => 'blue')
|
|
96
|
+
end
|
|
154
97
|
end
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
98
|
+
|
|
99
|
+
# NOTE: apparently AR when conditions are build with strings in the where clauses (instead of names, etc)
|
|
100
|
+
# it decides to parenthesize them, even when there's only 1 condition. Hence the silly parentization of
|
|
101
|
+
# these SQL fragments here (and others)
|
|
102
|
+
context 'by using all supported operators' do
|
|
103
|
+
PREF = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
|
|
104
|
+
COMMON_SQL_PREFIX = <<~SQL
|
|
105
|
+
SELECT "active_books".* FROM "active_books"
|
|
106
|
+
INNER JOIN
|
|
107
|
+
"active_authors" "#{PREF}/author" ON "#{PREF}/author"."id" = "active_books"."author_id"
|
|
108
|
+
SQL
|
|
109
|
+
context '=' do
|
|
110
|
+
let(:filters_string) { 'author.id=11'}
|
|
111
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id = 11')
|
|
112
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
113
|
+
WHERE ("#{PREF}/author"."id" = 11)
|
|
114
|
+
SQL
|
|
115
|
+
end
|
|
116
|
+
context '= (with array)' do
|
|
117
|
+
let(:filters_string) { 'author.id=11,22'}
|
|
118
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IN (11,22)')
|
|
119
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
120
|
+
WHERE ("#{PREF}/author"."id" IN (11,22))
|
|
121
|
+
SQL
|
|
122
|
+
end
|
|
123
|
+
context '!=' do
|
|
124
|
+
let(:filters_string) { 'author.id!=11'}
|
|
125
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <> 11')
|
|
126
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
127
|
+
WHERE ("#{PREF}/author"."id" <> 11)
|
|
128
|
+
SQL
|
|
129
|
+
end
|
|
130
|
+
context '!= (with array)' do
|
|
131
|
+
let(:filters_string) { 'author.id!=11,888'}
|
|
132
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id NOT IN (11,888)')
|
|
133
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
134
|
+
WHERE ("#{PREF}/author"."id" NOT IN (11,888))
|
|
135
|
+
SQL
|
|
136
|
+
end
|
|
137
|
+
context '>' do
|
|
138
|
+
let(:filters_string) { 'author.id>1'}
|
|
139
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id > 1')
|
|
140
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
141
|
+
WHERE ("#{PREF}/author"."id" > 1)
|
|
142
|
+
SQL
|
|
143
|
+
end
|
|
144
|
+
context '<' do
|
|
145
|
+
let(:filters_string) { 'author.id<22'}
|
|
146
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id < 22')
|
|
147
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
148
|
+
WHERE ("#{PREF}/author"."id" < 22)
|
|
149
|
+
SQL
|
|
150
|
+
end
|
|
151
|
+
context '>=' do
|
|
152
|
+
let(:filters_string) { 'author.id>=22'}
|
|
153
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id >= 22')
|
|
154
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
155
|
+
WHERE ("#{PREF}/author"."id" >= 22)
|
|
156
|
+
SQL
|
|
157
|
+
end
|
|
158
|
+
context '<=' do
|
|
159
|
+
let(:filters_string) { 'author.id<=22'}
|
|
160
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <= 22')
|
|
161
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
162
|
+
WHERE ("#{PREF}/author"."id" <= 22)
|
|
163
|
+
SQL
|
|
164
|
+
end
|
|
165
|
+
context '!' do
|
|
166
|
+
let(:filters_string) { 'author.id!'}
|
|
167
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IS NOT NULL')
|
|
166
168
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL)
|
|
170
|
+
SQL
|
|
169
171
|
end
|
|
170
|
-
context '
|
|
171
|
-
let(:filters_string) { 'author.name
|
|
172
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name
|
|
172
|
+
context '!!' do
|
|
173
|
+
let(:filters_string) { 'author.name!!'}
|
|
174
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name IS NULL')
|
|
173
175
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
WHERE ("#{PREF}/author"."name" IS NULL)
|
|
177
|
+
SQL
|
|
178
|
+
end
|
|
179
|
+
context 'including LIKE fuzzy queries' do
|
|
180
|
+
context 'LIKE' do
|
|
181
|
+
let(:filters_string) { 'author.name=author*'}
|
|
182
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name LIKE "author%"')
|
|
183
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
184
|
+
WHERE ("#{PREF}/author"."name" LIKE 'author%')
|
|
185
|
+
SQL
|
|
186
|
+
end
|
|
187
|
+
context 'NOT LIKE' do
|
|
188
|
+
let(:filters_string) { 'author.name!=foobar*'}
|
|
189
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name NOT LIKE "foobar%"')
|
|
190
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
191
|
+
WHERE ("#{PREF}/author"."name" NOT LIKE 'foobar%')
|
|
192
|
+
SQL
|
|
193
|
+
end
|
|
176
194
|
end
|
|
177
195
|
end
|
|
178
|
-
end
|
|
179
196
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
context 'with a field mapping using a proc' do
|
|
198
|
+
let(:filters_string) { 'name_is_not=Book1' }
|
|
199
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where.not(simple_name: 'Book1')
|
|
200
|
+
end
|
|
184
201
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
202
|
+
context 'with a deeply nested chains' do
|
|
203
|
+
context 'of depth 2' do
|
|
204
|
+
let(:filters_string) { 'category.books.name=Book2' }
|
|
205
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books).where('books_active_categories.simple_name': 'Book2')
|
|
206
|
+
end
|
|
207
|
+
context 'multiple conditions on a nested relationship' do
|
|
208
|
+
let(:filters_string) { 'category.books.taggings.tag_id=1&category.books.taggings.label=primary' }
|
|
209
|
+
it_behaves_like 'subject_equivalent_to',
|
|
210
|
+
ActiveBook.joins(category: { books: :taggings }).where('active_taggings.tag_id': 1).where('active_taggings.label': 'primary')
|
|
211
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
|
212
|
+
SELECT "active_books".* FROM "active_books"
|
|
213
|
+
INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
|
|
214
|
+
INNER JOIN "active_books" "books_active_categories" ON "books_active_categories"."category_uuid" = "active_categories"."uuid"
|
|
215
|
+
INNER JOIN "active_taggings" "#{PREF}/category/books/taggings" ON "/category/books/taggings"."book_id" = "books_active_categories"."id"
|
|
216
|
+
WHERE ("#{PREF}/category/books/taggings"."tag_id" = 1)
|
|
217
|
+
AND ("#{PREF}/category/books/taggings"."label" = 'primary')
|
|
218
|
+
SQL
|
|
219
|
+
end
|
|
220
|
+
context 'that contain multiple joins to the same table' do
|
|
221
|
+
let(:filters_string) { 'taggings.tag.taggings.tag_id=1' }
|
|
222
|
+
it_behaves_like 'subject_equivalent_to',
|
|
223
|
+
ActiveBook.joins(taggings: {tag: :taggings}).where('taggings_active_tags.tag_id=1')
|
|
224
|
+
end
|
|
189
225
|
end
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
AND ("#{PREF}/category/books/taggings"."label" = 'primary')
|
|
201
|
-
SQL
|
|
226
|
+
|
|
227
|
+
context 'by multiple fields' do
|
|
228
|
+
context 'adds the where clauses for the top model if fields belong to it' do
|
|
229
|
+
let(:filters_string) { 'category_uuid=deadbeef1&name=Book1' }
|
|
230
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1', simple_name: 'Book1')
|
|
231
|
+
end
|
|
232
|
+
context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
|
|
233
|
+
let(:filters_string) { 'taggings.label=primary&taggings.tag_id=2' }
|
|
234
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary', 'active_taggings.tag_id' => 2)
|
|
235
|
+
end
|
|
202
236
|
end
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
237
|
+
|
|
238
|
+
context 'uses fully qualified names for conditions (disambiguate fields)' do
|
|
239
|
+
context 'when we have a join table condition that has the same field' do
|
|
240
|
+
let(:filters_string) { 'name=Book1&category.books.name=Book3' }
|
|
241
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books)
|
|
242
|
+
.where('simple_name': 'Book1')
|
|
243
|
+
.where('books_active_categories.simple_name': 'Book3')
|
|
244
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
|
245
|
+
SELECT "active_books".* FROM "active_books"
|
|
246
|
+
INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
|
|
247
|
+
INNER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
|
|
248
|
+
WHERE ("active_books"."simple_name" = 'Book1')
|
|
249
|
+
AND ("#{PREF}/category/books"."simple_name" = 'Book3')
|
|
250
|
+
SQL
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
context 'it qualifis them even if there are no joined tables/conditions at all' do
|
|
254
|
+
let(:filters_string) { 'id=11'}
|
|
255
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
|
256
|
+
SELECT "active_books".* FROM "active_books"
|
|
257
|
+
WHERE ("active_books"."id" = 11)
|
|
258
|
+
SQL
|
|
259
|
+
end
|
|
260
|
+
|
|
207
261
|
end
|
|
208
262
|
end
|
|
209
263
|
|
|
210
|
-
context '
|
|
264
|
+
context 'with simple OR conditions' do
|
|
211
265
|
context 'adds the where clauses for the top model if fields belong to it' do
|
|
212
|
-
let(:filters_string) { 'category_uuid=deadbeef1
|
|
213
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1'
|
|
266
|
+
let(:filters_string) { 'category_uuid=deadbeef1|name=Book1' }
|
|
267
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(simple_name: 'Book1'))
|
|
268
|
+
end
|
|
269
|
+
context 'supports top level parenthesis' do
|
|
270
|
+
let(:filters_string) { '(category_uuid=deadbeef1|name=Book1)' }
|
|
271
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(simple_name: 'Book1'))
|
|
214
272
|
end
|
|
215
273
|
context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
|
|
216
|
-
let(:filters_string) { 'taggings.label=primary
|
|
217
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary'
|
|
274
|
+
let(:filters_string) { 'taggings.label=primary|taggings.tag_id=2' }
|
|
275
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
|
|
276
|
+
.or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
|
|
218
277
|
end
|
|
219
278
|
end
|
|
220
279
|
|
|
221
|
-
context '
|
|
222
|
-
|
|
223
|
-
|
|
280
|
+
context 'with combined AND and OR conditions' do
|
|
281
|
+
let(:filters_string) { '(category_uuid=deadbeef1|category_uuid=deadbeef2)&(name=Book1|name=Book2)' }
|
|
282
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(category_uuid: 'deadbeef2'))
|
|
283
|
+
.and(ActiveBook.where(simple_name: 'Book1').or(ActiveBook.where(simple_name: 'Book2')))
|
|
284
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
|
224
285
|
SELECT "active_books".* FROM "active_books"
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
let(:filters_string) { 'name=Book1&category.books.name=Book3' }
|
|
229
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books)
|
|
230
|
-
.where('simple_name': 'Book1')
|
|
231
|
-
.where('books_active_categories.simple_name': 'Book3')
|
|
232
|
-
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
|
233
|
-
WHERE ("#{PREF}/category/books"."simple_name" = 'Book3')
|
|
234
|
-
AND ("active_books"."simple_name" = 'Book1')
|
|
235
|
-
SQL
|
|
236
|
-
end
|
|
286
|
+
WHERE ("active_books"."category_uuid" = 'deadbeef1' OR "active_books"."category_uuid" = 'deadbeef2')
|
|
287
|
+
AND ("active_books"."simple_name" = 'Book1' OR "active_books"."simple_name" = 'Book2')
|
|
288
|
+
SQL
|
|
237
289
|
|
|
238
|
-
context '
|
|
239
|
-
let(:filters_string) { '
|
|
290
|
+
context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
|
|
291
|
+
let(:filters_string) { 'taggings.label=primary|taggings.tag_id=2' }
|
|
292
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
|
|
293
|
+
.or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
|
|
240
294
|
it_behaves_like 'subject_matches_sql', <<~SQL
|
|
241
295
|
SELECT "active_books".* FROM "active_books"
|
|
242
|
-
|
|
243
|
-
|
|
296
|
+
INNER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
|
|
297
|
+
WHERE ("/taggings"."label" = 'primary' OR "/taggings"."tag_id" = 2)
|
|
298
|
+
SQL
|
|
244
299
|
end
|
|
245
300
|
|
|
301
|
+
context '3-deep AND and OR conditions' do
|
|
302
|
+
let(:filters_string) { '(category.name=cat2|(taggings.label=primary&tags.name=red))&category_uuid=deadbeef1' }
|
|
303
|
+
it_behaves_like('subject_equivalent_to', Proc.new do
|
|
304
|
+
base=ActiveBook.joins(:category,:taggings,:tags)
|
|
305
|
+
|
|
306
|
+
and1_or1 = base.where('category.name': 'cat2')
|
|
307
|
+
|
|
308
|
+
and1_or2_and1 = base.where('taggings.label': 'primary')
|
|
309
|
+
and1_or2_and2 = base.where('tags.name': 'red')
|
|
310
|
+
and1_or2 = and1_or2_and1.and(and1_or2_and2)
|
|
311
|
+
|
|
312
|
+
and1 = and1_or1.or(and1_or2)
|
|
313
|
+
and2=base.where(category_uuid: 'deadbeef1')
|
|
314
|
+
|
|
315
|
+
query = and1.and(and2)
|
|
316
|
+
end)
|
|
317
|
+
|
|
318
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
|
319
|
+
SELECT "active_books".* FROM "active_books"
|
|
320
|
+
INNER JOIN "active_categories" "/category" ON "/category"."uuid" = "active_books"."category_uuid"
|
|
321
|
+
INNER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
|
|
322
|
+
INNER JOIN "active_taggings" "taggings_active_books_join" ON "taggings_active_books_join"."book_id" = "active_books"."id"
|
|
323
|
+
INNER JOIN "active_tags" "/tags" ON "/tags"."id" = "taggings_active_books_join"."tag_id"
|
|
324
|
+
WHERE ("/category"."name" = 'cat2' OR ("/taggings"."label" = 'primary') AND ("/tags"."name" = 'red')) AND ("active_books"."category_uuid" = 'deadbeef1')
|
|
325
|
+
SQL
|
|
326
|
+
end
|
|
246
327
|
end
|
|
247
328
|
|
|
248
329
|
context 'ActiveRecord continues to work as expected (with our patches)' do
|