inquery 0.0.1

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.
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