inquery 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.releaser_config +4 -0
- data/.rubocop.yml +42 -0
- data/.travis.yml +9 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +288 -0
- data/RUBY_VERSION +1 -0
- data/Rakefile +36 -0
- data/VERSION +1 -0
- data/doc/Inquery.html +119 -0
- data/doc/Inquery/Exceptions.html +115 -0
- data/doc/Inquery/Exceptions/Base.html +127 -0
- data/doc/Inquery/Exceptions/InvalidRelation.html +131 -0
- data/doc/Inquery/Exceptions/UnknownCallSignature.html +131 -0
- data/doc/Inquery/Mixins.html +117 -0
- data/doc/Inquery/Mixins/RelationValidation.html +334 -0
- data/doc/Inquery/Mixins/RelationValidation/ClassMethods.html +190 -0
- data/doc/Inquery/Mixins/SchemaValidation.html +124 -0
- data/doc/Inquery/Mixins/SchemaValidation/ClassMethods.html +192 -0
- data/doc/Inquery/Query.html +736 -0
- data/doc/Inquery/Query/Chainable.html +476 -0
- data/doc/_index.html +254 -0
- data/doc/class_list.html +58 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +57 -0
- data/doc/css/style.css +339 -0
- data/doc/file.README.html +365 -0
- data/doc/file_list.html +60 -0
- data/doc/frames.html +26 -0
- data/doc/index.html +365 -0
- data/doc/js/app.js +219 -0
- data/doc/js/full_list.js +181 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +147 -0
- data/doc/top-level-namespace.html +112 -0
- data/inquery.gemspec +58 -0
- data/lib/inquery.rb +10 -0
- data/lib/inquery/exceptions.rb +7 -0
- data/lib/inquery/mixins/relation_validation.rb +100 -0
- data/lib/inquery/mixins/schema_validation.rb +27 -0
- data/lib/inquery/query.rb +50 -0
- data/lib/inquery/query/chainable.rb +53 -0
- data/test/db/models.rb +20 -0
- data/test/db/schema.rb +20 -0
- data/test/inquery/query/chainable_test.rb +67 -0
- data/test/inquery/query_test.rb +47 -0
- data/test/queries/group/fetch_as_json.rb +13 -0
- data/test/queries/group/fetch_green.rb +11 -0
- data/test/queries/group/fetch_red.rb +11 -0
- data/test/queries/group/filter_with_color.rb +12 -0
- data/test/queries/user/fetch_all.rb +9 -0
- data/test/queries/user/fetch_in_group.rb +13 -0
- data/test/queries/user/fetch_in_group_rel.rb +17 -0
- data/test/test_helper.rb +26 -0
- metadata +265 -0
data/lib/inquery.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
module Inquery
|
2
|
+
module Mixins
|
3
|
+
module RelationValidation
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
OPTIONS_SCHEMA = {
|
7
|
+
hash: {
|
8
|
+
class: { type: String, null: true, required: false },
|
9
|
+
fields: { type: :integer, null: true, required: false },
|
10
|
+
default_select: { type: :symbol, null: true, required: false },
|
11
|
+
default: { type: [Proc, FalseClass], null: true, required: false }
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
DEFAULT_OPTIONS = {
|
16
|
+
# Allows to restrict the class (attribute `klass`) of the relation. Use
|
17
|
+
# `nil` to not perform any checks. The `class` attribute will also be
|
18
|
+
# taken to infer a default if no relation is given and you didn't
|
19
|
+
# specify any `default`.
|
20
|
+
class: nil,
|
21
|
+
|
22
|
+
# This allows to specify a default relation that will be taken if no
|
23
|
+
# relation is given. This must be specified as a Proc returning the
|
24
|
+
# relation. Set this to `false` for no default. If this is set to `nil`,
|
25
|
+
# it will try to infer the default from the option `class` (if given).
|
26
|
+
default: nil,
|
27
|
+
|
28
|
+
# Allows to restrict the number of fields / values the relation must
|
29
|
+
# select. This is particularly useful if you're using the query as a
|
30
|
+
# subquery and need it to return exactly one field. Use `nil` to not
|
31
|
+
# perform any checks.
|
32
|
+
fields: nil,
|
33
|
+
|
34
|
+
# If this is set to a symbol, the relation does not have any select
|
35
|
+
# fields specified (`select_values` is empty) and `fields` is > 0, it
|
36
|
+
# will automatically select the given field. Use `nil` to disable this
|
37
|
+
# behavior.
|
38
|
+
default_select: :id
|
39
|
+
}
|
40
|
+
|
41
|
+
included do
|
42
|
+
class_attribute :_relation_options
|
43
|
+
end
|
44
|
+
|
45
|
+
module ClassMethods
|
46
|
+
# Allows to configure the parameters of the relation this query operates
|
47
|
+
# on. See {OPTIONS_SCHEMA} for documentation of the options hash.
|
48
|
+
def relation(options = {})
|
49
|
+
Schemacop.validate!(OPTIONS_SCHEMA, options)
|
50
|
+
self._relation_options = options
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Validates (and possibly alters) the given relation according to the
|
55
|
+
# options specified at class level using the `relation` method.
|
56
|
+
def validate_relation!(relation)
|
57
|
+
options = DEFAULT_OPTIONS.dup
|
58
|
+
options.merge!(self.class._relation_options.dup) if self.class._relation_options
|
59
|
+
|
60
|
+
relation_class = options[:class].try(:constantize)
|
61
|
+
|
62
|
+
# ---------------------------------------------------------------
|
63
|
+
# Validate presence
|
64
|
+
# ---------------------------------------------------------------
|
65
|
+
if relation.nil?
|
66
|
+
if options[:default]
|
67
|
+
relation = options[:default].call
|
68
|
+
elsif options[:default].nil? && relation_class
|
69
|
+
relation = relation_class.all
|
70
|
+
else
|
71
|
+
fail Inquery::Exceptions::InvalidRelation, 'A relation must be given for this query.'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# ---------------------------------------------------------------
|
76
|
+
# Validate class
|
77
|
+
# ---------------------------------------------------------------
|
78
|
+
if relation_class && relation_class != relation.klass
|
79
|
+
fail Inquery::Exceptions::InvalidRelation, "Unexpected relation class '#{relation.klass}' for this query, expected a '#{relation_class}'."
|
80
|
+
end
|
81
|
+
|
82
|
+
# ---------------------------------------------------------------
|
83
|
+
# Validate selected fields
|
84
|
+
# ---------------------------------------------------------------
|
85
|
+
fields_count = relation.select_values.size
|
86
|
+
|
87
|
+
if fields_count == 0 && options[:default_select] && options[:fields] && options[:fields] > 0
|
88
|
+
relation = relation.select(options[:default_select])
|
89
|
+
fields_count = 1
|
90
|
+
end
|
91
|
+
|
92
|
+
if !options[:fields].nil? && fields_count != options[:fields]
|
93
|
+
fail Inquery::Exceptions::InvalidRelation, "Expected given relation to select #{options[:fields]} field(s) but got #{fields_count}."
|
94
|
+
end
|
95
|
+
|
96
|
+
return relation
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Inquery
|
2
|
+
module Mixins
|
3
|
+
module SchemaValidation
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class_attribute :_schema
|
8
|
+
self._schema = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def schema(schema)
|
13
|
+
fail 'Schema must be a hash.' unless schema.is_a?(Hash)
|
14
|
+
|
15
|
+
unless schema[:type]
|
16
|
+
schema = {
|
17
|
+
type: :hash,
|
18
|
+
hash: schema
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
self._schema = schema
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Inquery
|
2
|
+
class Query
|
3
|
+
include Mixins::SchemaValidation
|
4
|
+
|
5
|
+
attr_reader :params
|
6
|
+
|
7
|
+
# Instantiates the query class using the given arguments
|
8
|
+
# and runs `call` and `process` on it.
|
9
|
+
def self.run(*args)
|
10
|
+
new(*args).run
|
11
|
+
end
|
12
|
+
|
13
|
+
# Instantiates the query class using the given arguments
|
14
|
+
# and runs `call` on it.
|
15
|
+
def self.call(*args)
|
16
|
+
new(*args).call
|
17
|
+
end
|
18
|
+
|
19
|
+
# Instantiates the query class and validates the given params hash (if there
|
20
|
+
# was a validation schema specified).
|
21
|
+
def initialize(params = {})
|
22
|
+
@params = params
|
23
|
+
|
24
|
+
if self.class._schema
|
25
|
+
Schemacop.validate!(self.class._schema, @params)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Runs both `call` and `process`.
|
30
|
+
def run
|
31
|
+
process(call)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Override this method to perform the actual query.
|
35
|
+
def call
|
36
|
+
fail NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
# Override this method to perform an optional result postprocessing.
|
40
|
+
def process(results)
|
41
|
+
results
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns a copy of the query's params, wrapped in an OpenStruct object for
|
45
|
+
# easyer access.
|
46
|
+
def osparams
|
47
|
+
@osparams ||= OpenStruct.new(params)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Inquery
|
2
|
+
class Query::Chainable < Query
|
3
|
+
include Inquery::Mixins::RelationValidation
|
4
|
+
|
5
|
+
# Allows using this class as an AR scope.
|
6
|
+
def self.call(*args)
|
7
|
+
return new(*args).call
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(*args)
|
11
|
+
fail args.inspect
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :relation
|
15
|
+
|
16
|
+
def initialize(*args)
|
17
|
+
relation, params = parse_init_args(*args)
|
18
|
+
@relation = validate_relation!(relation)
|
19
|
+
super(params)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def parse_init_args(*args)
|
25
|
+
# new(relation)
|
26
|
+
if (args[0].is_a?(ActiveRecord::Relation) || args[0].class < ActiveRecord::Base) && args[1].nil?
|
27
|
+
relation = args[0]
|
28
|
+
params = {}
|
29
|
+
|
30
|
+
# new(params)
|
31
|
+
elsif args[0].is_a?(Hash) && args[1].nil?
|
32
|
+
relation = nil
|
33
|
+
params = args[0]
|
34
|
+
|
35
|
+
# new(relation, params)
|
36
|
+
elsif (args[0].is_a?(ActiveRecord::Relation) || args[0].class < ActiveRecord::Base) && args[1].is_a?(Hash)
|
37
|
+
relation = args[0]
|
38
|
+
params = args[1]
|
39
|
+
|
40
|
+
# new()
|
41
|
+
elsif args.empty?
|
42
|
+
relation = nil
|
43
|
+
params = {}
|
44
|
+
|
45
|
+
# Unknown
|
46
|
+
else
|
47
|
+
fail Inquery::Exceptions::UnknownCallSignature, "Unknown call signature for the query constructor: #{args.collect(&:class)}."
|
48
|
+
end
|
49
|
+
|
50
|
+
return relation, params
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/test/db/models.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'queries/group/fetch_green'
|
2
|
+
require 'queries/group/fetch_red'
|
3
|
+
|
4
|
+
class GroupsUser < ActiveRecord::Base
|
5
|
+
belongs_to :group
|
6
|
+
belongs_to :user
|
7
|
+
end
|
8
|
+
|
9
|
+
class User < ActiveRecord::Base
|
10
|
+
has_many :groups_users
|
11
|
+
has_many :groups, through: :groups_users
|
12
|
+
end
|
13
|
+
|
14
|
+
class Group < ActiveRecord::Base
|
15
|
+
has_many :groups_users
|
16
|
+
has_many :users, through: :groups_users
|
17
|
+
|
18
|
+
scope :red, Queries::Group::FetchRed
|
19
|
+
scope :green, Queries::Group::FetchGreen
|
20
|
+
end
|
data/test/db/schema.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
self.verbose = false
|
3
|
+
|
4
|
+
create_table :groups, force: true do |t|
|
5
|
+
t.string :name
|
6
|
+
t.string :color
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
create_table :users, force: true do |t|
|
11
|
+
t.string :name
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
|
15
|
+
create_table :groups_users, force: true do |t|
|
16
|
+
t.integer :group_id
|
17
|
+
t.integer :user_id
|
18
|
+
t.timestamps
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'queries/user/fetch_in_group_rel'
|
4
|
+
require 'queries/group/filter_with_color'
|
5
|
+
|
6
|
+
module Inquery
|
7
|
+
class Query
|
8
|
+
class ChainableTest < Minitest::Unit::TestCase
|
9
|
+
include TestHelper
|
10
|
+
|
11
|
+
def setup
|
12
|
+
self.class.setup_db
|
13
|
+
self.class.setup_base_data
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_fetch_users_in_group_rels
|
17
|
+
result = Queries::User::FetchInGroupRel.run(Group.where('ID IN (1, 2)'))
|
18
|
+
assert_equal User.find([1, 2, 3]), result.to_a
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_fetch_users_in_group_rels_with_select
|
22
|
+
# Fetch all groups user 1 is in
|
23
|
+
group_ids_rel = GroupsUser.where(user_id: 1).select(:group_id)
|
24
|
+
|
25
|
+
# Fetch all users that are in these groups
|
26
|
+
result = Queries::User::FetchInGroupRel.run(group_ids_rel)
|
27
|
+
assert_equal User.find([1, 2, 3]), result.to_a
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_fetch_red_groups
|
31
|
+
result = Queries::Group::FetchRed.run
|
32
|
+
assert_equal Group.find([1]), result.to_a
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_fetch_red_groups_via_scope
|
36
|
+
result = Group.red
|
37
|
+
assert_equal Group.find([1]), result.to_a
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_fetch_green_groups_via_scope
|
41
|
+
result = Group.green
|
42
|
+
assert_equal Group.find([2, 3]), result.to_a
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_fetch_green_groups_via_scope_with_where
|
46
|
+
# Where before
|
47
|
+
result = Group.where('id > 2').green
|
48
|
+
assert_equal Group.find([3]), result.to_a
|
49
|
+
|
50
|
+
# Where after
|
51
|
+
result = Group.green.where('id > 2')
|
52
|
+
assert_equal Group.find([3]), result.to_a
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_fetch_green_groups_with_where
|
56
|
+
# With default scope
|
57
|
+
result = Queries::Group::FetchGreen.run(Group.where('id > 2'))
|
58
|
+
assert_equal Group.find([3]), result.to_a
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_filter_with_color
|
62
|
+
result = Queries::Group::FilterWithColor.run(Group.where('id > 2'), color: 'green')
|
63
|
+
assert_equal Group.find([3]), result.to_a
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'queries/user/fetch_all'
|
3
|
+
require 'queries/user/fetch_in_group'
|
4
|
+
require 'queries/group/fetch_as_json'
|
5
|
+
|
6
|
+
module Inquery
|
7
|
+
class QueryTest < Minitest::Unit::TestCase
|
8
|
+
include TestHelper
|
9
|
+
|
10
|
+
def setup
|
11
|
+
self.class.setup_db
|
12
|
+
self.class.setup_base_data
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_fetch_all_users
|
16
|
+
result = Queries::User::FetchAll.run
|
17
|
+
assert_equal User.find([1, 2, 3]), result
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_fetch_users_in_group
|
21
|
+
result = Queries::User::FetchInGroup.run(group_id: 1)
|
22
|
+
assert_equal User.find([1, 2]), result.to_a
|
23
|
+
|
24
|
+
result = Queries::User::FetchInGroup.run(group_id: 2)
|
25
|
+
assert_equal User.find([1, 3]), result.to_a
|
26
|
+
|
27
|
+
result = Queries::User::FetchInGroup.run(group_id: 3)
|
28
|
+
assert_equal User.find([2]), result.to_a
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_fetch_users_in_group_with_invalid_schema
|
32
|
+
assert_raises Schemacop::Exceptions::Validation do
|
33
|
+
Queries::User::FetchInGroup.run
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_fetch_groups_as_json
|
38
|
+
result = Queries::Group::FetchAsJson.call
|
39
|
+
assert_equal Group.all, result
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_fetch_groups_as_json_with_process
|
43
|
+
result = Queries::Group::FetchAsJson.run
|
44
|
+
assert_equal Group.all.to_json, result
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|