inquery 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.releaser_config +4 -0
  4. data/.rubocop.yml +42 -0
  5. data/.travis.yml +9 -0
  6. data/.yardopts +1 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE +21 -0
  9. data/README.md +288 -0
  10. data/RUBY_VERSION +1 -0
  11. data/Rakefile +36 -0
  12. data/VERSION +1 -0
  13. data/doc/Inquery.html +119 -0
  14. data/doc/Inquery/Exceptions.html +115 -0
  15. data/doc/Inquery/Exceptions/Base.html +127 -0
  16. data/doc/Inquery/Exceptions/InvalidRelation.html +131 -0
  17. data/doc/Inquery/Exceptions/UnknownCallSignature.html +131 -0
  18. data/doc/Inquery/Mixins.html +117 -0
  19. data/doc/Inquery/Mixins/RelationValidation.html +334 -0
  20. data/doc/Inquery/Mixins/RelationValidation/ClassMethods.html +190 -0
  21. data/doc/Inquery/Mixins/SchemaValidation.html +124 -0
  22. data/doc/Inquery/Mixins/SchemaValidation/ClassMethods.html +192 -0
  23. data/doc/Inquery/Query.html +736 -0
  24. data/doc/Inquery/Query/Chainable.html +476 -0
  25. data/doc/_index.html +254 -0
  26. data/doc/class_list.html +58 -0
  27. data/doc/css/common.css +1 -0
  28. data/doc/css/full_list.css +57 -0
  29. data/doc/css/style.css +339 -0
  30. data/doc/file.README.html +365 -0
  31. data/doc/file_list.html +60 -0
  32. data/doc/frames.html +26 -0
  33. data/doc/index.html +365 -0
  34. data/doc/js/app.js +219 -0
  35. data/doc/js/full_list.js +181 -0
  36. data/doc/js/jquery.js +4 -0
  37. data/doc/method_list.html +147 -0
  38. data/doc/top-level-namespace.html +112 -0
  39. data/inquery.gemspec +58 -0
  40. data/lib/inquery.rb +10 -0
  41. data/lib/inquery/exceptions.rb +7 -0
  42. data/lib/inquery/mixins/relation_validation.rb +100 -0
  43. data/lib/inquery/mixins/schema_validation.rb +27 -0
  44. data/lib/inquery/query.rb +50 -0
  45. data/lib/inquery/query/chainable.rb +53 -0
  46. data/test/db/models.rb +20 -0
  47. data/test/db/schema.rb +20 -0
  48. data/test/inquery/query/chainable_test.rb +67 -0
  49. data/test/inquery/query_test.rb +47 -0
  50. data/test/queries/group/fetch_as_json.rb +13 -0
  51. data/test/queries/group/fetch_green.rb +11 -0
  52. data/test/queries/group/fetch_red.rb +11 -0
  53. data/test/queries/group/filter_with_color.rb +12 -0
  54. data/test/queries/user/fetch_all.rb +9 -0
  55. data/test/queries/user/fetch_in_group.rb +13 -0
  56. data/test/queries/user/fetch_in_group_rel.rb +17 -0
  57. data/test/test_helper.rb +26 -0
  58. metadata +265 -0
@@ -0,0 +1,10 @@
1
+ module Inquery
2
+ end
3
+
4
+ require 'schemacop'
5
+
6
+ require 'inquery/exceptions'
7
+ require 'inquery/mixins/schema_validation'
8
+ require 'inquery/mixins/relation_validation'
9
+ require 'inquery/query'
10
+ require 'inquery/query/chainable'
@@ -0,0 +1,7 @@
1
+ module Inquery
2
+ module Exceptions
3
+ class Base < StandardError; end
4
+ class UnknownCallSignature < Base; end
5
+ class InvalidRelation < Base; end
6
+ end
7
+ end
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,13 @@
1
+ module Queries
2
+ module Group
3
+ class FetchAsJson < Inquery::Query
4
+ def call
5
+ ::Group.all
6
+ end
7
+
8
+ def process(result)
9
+ result.to_json
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Queries
2
+ module Group
3
+ class FetchGreen < Inquery::Query::Chainable
4
+ relation class: 'Group'
5
+
6
+ def call
7
+ relation.where(color: 'green')
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Queries
2
+ module Group
3
+ class FetchRed < Inquery::Query::Chainable
4
+ relation class: 'Group'
5
+
6
+ def call
7
+ relation.where(color: 'red')
8
+ end
9
+ end
10
+ end
11
+ end