arel_rest 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '03696f3d7df1d671e416ddc6286525fc21aaeb40241208bdce54a3862324c44a'
4
- data.tar.gz: 2bbd4386162c8eef4e4d9cf43206e442f66cb4c34c181a92f1f9439c67819ed8
3
+ metadata.gz: b16fe311f54f93f520d73d38d672b532d1ede2908aec026d130ff51eecc208d7
4
+ data.tar.gz: 3b7f1dd29124f3241a33d12d3081879a06196a8ec5843b9533cd31555014c0ae
5
5
  SHA512:
6
- metadata.gz: c748ceda6d574ccd7ec7b12360f97f5aa54e50dc6eb71902d70471cd74ddd601bd61510b3583cf06ba561dbd8f76d645a79499589e9d9781aaccc7fc4a61b35c
7
- data.tar.gz: e3bf8486e07b1877bbebf3df080d55935b4a152e76cd6697abd8acb23678b8ee6fb425c6a4a52ba4daeecaf208fcae1925e402421fe8546ce457df5d6080ca47
6
+ metadata.gz: 891a2f435f9bcc3c34a00ffe03d89e63f38092dd1caca5bb07f0ed65e87d51c421ea61ab511fcdf49846956f8fd97fc98461eb6980c22a270dee341436ba8833
7
+ data.tar.gz: 7c9a0a7858e2a44b735923e76a0d7149788dcf553826410240123ebb4494bdf0a8ecacd8fd96fad5f3f7d3d01ec0918de26852afdabc04040d21a4d328125bdb
data/lib/parser.rb ADDED
@@ -0,0 +1,83 @@
1
+ require 'predications/eq_operator'
2
+ require 'predications/not_eq_operator'
3
+ require 'predications/gt_operator'
4
+ require 'predications/gteq_operator'
5
+ require 'predications/lt_operator'
6
+ require 'predications/lteq_operator'
7
+ require 'predications/in_operator'
8
+ require 'predications/not_in_operator'
9
+ require 'predications/between_operator'
10
+ require 'predications/matches_operator'
11
+ require 'predications/does_not_match_operator'
12
+ require 'predications/order_operator'
13
+
14
+ module ArelRest
15
+ class Parser
16
+ TEMPLATE_OPERATORS = {
17
+ "eq" => Predications::EqOperator,
18
+ "not_eq" => Predications::NotEqOperator,
19
+ "gt" => Predications::GtOperator,
20
+ "gteq" => Predications::GteqOperator,
21
+ "lt" => Predications::LtOperator,
22
+ "lteq" => Predications::LteqOperator,
23
+ "in" => Predications::InOperator,
24
+ "not_in" => Predications::NotInOperator,
25
+ "between" => Predications::BetweenOperator,
26
+ "matches" => Predications::MatchesOperator,
27
+ "does_not_match" => Predications::DoesNotMatchOperator,
28
+ }
29
+
30
+ # Recevi object query and return array with
31
+ # string query and value.
32
+ # Ex: query object: {attribute: "a", operator:"=", values: [1]}
33
+ # >> ["a = ?", [1]]
34
+
35
+ class OperatorNotFound < StandardError
36
+ attr_accessor :column
37
+
38
+ def initialize(column)
39
+ @column = column
40
+ super
41
+ end
42
+
43
+ def message
44
+ "Operator #{@column} not found"
45
+ end
46
+ end
47
+
48
+ def self.build_pair_query_string_and_values(q)
49
+ operator = TEMPLATE_OPERATORS[q[:operator].to_s]
50
+
51
+ raise(Parser::OperatorNotFound, q[:operator].to_s) unless operator
52
+
53
+ operator.process(q)
54
+ end
55
+
56
+ def self.query_builder(query)
57
+ conector = query.keys.detect{|connector| [:or, :and].include?(connector.to_sym)}
58
+ pair_query_string_and_values = query[conector].map do |query_obj|
59
+ if query_obj.keys.map(&:to_sym).any?{|key| [:or,:and].include?(key)}
60
+ nested_query = query_builder(query_obj) # Recursive
61
+ builded = nested_query
62
+ else
63
+ builded = build_pair_query_string_and_values(query_obj)
64
+ end
65
+
66
+ builded
67
+ end.inject do |query_composited, query_node|
68
+ if query_composited.nil?
69
+ query_composited = query_node
70
+ else
71
+ query_composited = query_composited.send(conector, query_node)
72
+ end
73
+ query_composited
74
+ end
75
+
76
+ pair_query_string_and_values
77
+ end
78
+
79
+ def self.parse_filter_to_arel(query)
80
+ query_builder(query)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,27 @@
1
+ module ArelRest::Predications
2
+ class BetweenOperator
3
+ class << self
4
+ def process(query)
5
+ table = Arel::Table.new(query[:attribute].split(".")[0])
6
+ column = query[:attribute].split(".")[1]
7
+
8
+ range = Range.new(
9
+ try_transform_date(query[:values][0]),
10
+ try_transform_date(query[:values][1])
11
+ )
12
+ table[column].between(range)
13
+ end
14
+
15
+ private
16
+
17
+ def try_transform_date(date)
18
+ begin
19
+ Time.use_zone(ArelRest.time_zone) { Time.parse(date)}
20
+ rescue StandardError => e
21
+ date
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ module ArelRest::Predications
2
+ class DoesNotMatchOperator
3
+ class << self
4
+ def process(query)
5
+ table = Arel::Table.new(query[:attribute].split(".")[0])
6
+ column = query[:attribute].split(".")[1]
7
+ table[column].does_not_match("%#{query[:values]}%")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module ArelRest::Predications
2
+ class EqOperator
3
+ class << self
4
+ def process(query)
5
+ values = query[:values] == '=null=' ? nil : query[:values]
6
+
7
+ table = Arel::Table.new(query[:attribute].split(".")[0])
8
+ table[query[:attribute].split(".")[1]].eq(values)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module ArelRest::Predications
2
+ class GtOperator
3
+ class << self
4
+ def process(query)
5
+ table = Arel::Table.new(query[:attribute].split(".")[0])
6
+ column = query[:attribute].split(".")[1]
7
+
8
+ table[column].gt(query[:values])
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module ArelRest::Predications
2
+ class GteqOperator
3
+ class << self
4
+ def process(query)
5
+ table = Arel::Table.new(query[:attribute].split(".")[0])
6
+ column = query[:attribute].split(".")[1]
7
+
8
+ table[column].gteq(query[:values])
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module ArelRest::Predications
2
+ class InOperator
3
+ class << self
4
+ def process(query)
5
+ values = query[:values].dup.map{|value| value==('=null=') ? nil : value}
6
+
7
+ table = Arel::Table.new(query[:attribute].split(".")[0])
8
+ table[query[:attribute].split(".")[1]].in(values)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module ArelRest::Predications
2
+ class LtOperator
3
+ class << self
4
+ def process(query)
5
+ table = Arel::Table.new(query[:attribute].split(".")[0])
6
+ column = query[:attribute].split(".")[1]
7
+
8
+ table[column].lt(query[:values])
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module ArelRest::Predications
2
+ class LteqOperator
3
+ class << self
4
+ def process(query)
5
+ table = Arel::Table.new(query[:attribute].split(".")[0])
6
+ column = query[:attribute].split(".")[1]
7
+
8
+ table[column].lteq(query[:values])
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ module ArelRest::Predications
2
+ class MatchesOperator
3
+ class << self
4
+ def process(query)
5
+ table = Arel::Table.new(query[:attribute].split(".")[0])
6
+ table[query[:attribute].split(".")[1]].matches("%#{query[:values]}%")
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ module ArelRest::Predications
2
+ class NotEqOperator
3
+ class << self
4
+ def process(query)
5
+ values = query[:values] == '=null=' ? nil : query[:values]
6
+
7
+ table = Arel::Table.new(query[:attribute].split(".")[0])
8
+ column = query[:attribute].split(".")[1]
9
+ table[column].not_eq(values)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module ArelRest::Predications
2
+ class NotInOperator
3
+ class << self
4
+ def process(query)
5
+ values = query[:values].dup.map{|value| value==('=null=') ? nil : value}
6
+
7
+ table = Arel::Table.new(query[:attribute].split(".")[0])
8
+ table[query[:attribute].split(".")[1]].not_in(values)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module ArelRest::Predications
2
+ class OrderOperator
3
+ class << self
4
+ def process(query)
5
+ table = Arel::Table.new(query[:attribute].split(".")[0])
6
+ column = query[:attribute].split(".")[1]
7
+
8
+ return table[column].asc if query[:values] == 'asc'
9
+ return table[column].desc if query[:values] == 'desc'
10
+ end
11
+ end
12
+ end
13
+ end
data/lib/query.rb ADDED
@@ -0,0 +1,145 @@
1
+ require "active_support/concern"
2
+ #TODO: Criar uma forma de checar a syntax dos consultas. Ex: Não aceitar dimenssões sem <table_name>.<column>
3
+ module ArelRest
4
+ module Query
5
+ extend ActiveSupport::Concern
6
+ included do
7
+ WHITE_LIST_MENSURE_OP = {
8
+ "count" => :count,
9
+ "average" => :average,
10
+ "minimum" => :minimum,
11
+ "maximum" => :maximum,
12
+ "sum" => :sum
13
+ }
14
+
15
+ @relationship_tree = {}
16
+
17
+ def self.schema
18
+ @relationship_tree = yield
19
+ end
20
+
21
+ def self.query(_rest_query)
22
+ # Conditional methods chain
23
+ self
24
+ .then do |method_chain|
25
+ _rest_query[:filters] ? method_chain.filter(_rest_query[:filters]) : method_chain
26
+ end
27
+ .then do |method_chain|
28
+ _rest_query[:sort] ? method_chain.order_by_dimensions(_rest_query[:sort]) : method_chain
29
+ end
30
+ .then do |method_chain|
31
+ _rest_query[:dimensions] ? method_chain.group_by_dimensions(_rest_query[:dimensions]) : method_chain
32
+ end
33
+ .then do |method_chain|
34
+ _rest_query[:page] ? method_chain.offset(_rest_query[:page]) : method_chain.offset(0)
35
+ end
36
+ .then do |method_chain|
37
+ _rest_query[:size] ? method_chain.limit(_rest_query[:size]) : method_chain.limit(100)
38
+ end
39
+ .then do |method_chain|
40
+ if _rest_query[:measures]
41
+ mensure_op = _rest_query[:measures].split('.')[0]
42
+ column = _rest_query[:measures].split('.')[1]
43
+ method_chain.send(WHITE_LIST_MENSURE_OP[mensure_op], column)
44
+ else
45
+ method_chain
46
+ end
47
+ end
48
+ end
49
+
50
+ def self.filter(query)
51
+ return where({}) unless query.present?
52
+ query_nodes = ArelRest::Parser.parse_filter_to_arel(query)
53
+ tables_from_arel_node = collect_tables_from_arel(query_nodes).reject{|table| table == self.table_name}
54
+
55
+ paths = tables_from_arel_node.map do |table|
56
+ build_join_hash find_path_to_relation(relationship_tree, table)
57
+ end
58
+ where(query_nodes).joins(paths)
59
+ end
60
+
61
+ def self.group_by_dimensions(query)
62
+ return group({}) unless query.present?
63
+ paths = query
64
+ # TODO: Estudar syntaxe p/ entender se essa extração de nome de tabela é correta
65
+ .map{|dimension| dimension.split('.')[0]}
66
+ .map{|table| find_path_to_relation(relationship_tree, table)}.compact
67
+ .map{|path| build_join_hash path}
68
+
69
+ group(query).joins(paths)
70
+ end
71
+
72
+ def self.order_by_dimensions(query)
73
+ return order({}) unless query.present?
74
+ paths = query
75
+ # TODO: Estudar syntaxe p/ entender se essa extração de nome de tabela é correta
76
+ .keys
77
+ .map(&:to_s)
78
+ .map{|dimension| dimension.split('.')[0]}
79
+ .map{|table| find_path_to_relation(relationship_tree, table)}.compact
80
+ .map{|path| build_join_hash path}
81
+
82
+ order(
83
+ ArelRest::Predications::OrderOperator
84
+ .process({
85
+ attribute: query.keys.first.to_s,
86
+ values: query.values.first
87
+ })
88
+ ).joins(paths)
89
+ end
90
+
91
+ def self.relationship_tree
92
+ @relationship_tree
93
+ end
94
+
95
+ # Busca o caminho até uma associação específica (ex: :comments)
96
+ def self.find_path_to_relation(tree, target_table, current_path = [])
97
+ tree.each do |model_name, associations|
98
+ associations.each do |assoc_name, subtree|
99
+ table_name_assoc = model_name.to_s.constantize.reflect_on_association(assoc_name).table_name
100
+ path_with_assoc = current_path + [model_name, assoc_name]
101
+ return path_with_assoc + [subtree.keys.first] if (table_name_assoc == target_table)
102
+
103
+ result = find_path_to_relation(subtree, target_table, path_with_assoc)
104
+ return result if result
105
+ end
106
+ end
107
+ nil
108
+ end
109
+
110
+ # Converte caminho [:posts, :comments] em hash encadeado { posts: :comments }
111
+ def self.build_join_hash(path)
112
+ path ||= []
113
+ join_path = path.select.with_index { |_, i| i.odd? }
114
+ join_path.reverse.reduce { |acc, key| { key => acc } }
115
+ end
116
+
117
+ # Recebe um nó(Arel::Node) e busca o nome de todas as tabelas a partir desse nó
118
+ # TODO: Talvez seja mais fácil obter toda a expressão e obter todas as tabelas a partir
119
+ # da string
120
+ def self.collect_tables_from_arel(node, tables = Set.new)
121
+ return tables unless node.is_a?(Arel::Nodes::Node) || node.is_a?(Arel::Attributes::Attribute)
122
+
123
+ # Se for atributo, pega o nome da tabela
124
+ if node.is_a?(Arel::Attributes::Attribute)
125
+ tables << node.relation.name.to_s
126
+ end
127
+
128
+ # Percorre children, expressions, left, right, grouping, etc
129
+ children = []
130
+ children += node.children if node.respond_to?(:children)
131
+ children << node.left if node.respond_to?(:left) && node.left
132
+ children << node.right if node.respond_to?(:right) && node.right
133
+ children << node.expr if node.respond_to?(:expr) && node.expr
134
+ children += node.expressions if node.respond_to?(:expressions)
135
+
136
+ children.compact.each do |child|
137
+ collect_tables_from_arel(child, tables)
138
+ end
139
+
140
+ tables
141
+ end
142
+
143
+ end
144
+ end
145
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arel_rest
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luiz Filipe, Lucas Hunter, Leonardo Baptista, Rafael C. Abreu
@@ -109,6 +109,20 @@ extensions: []
109
109
  extra_rdoc_files: []
110
110
  files:
111
111
  - lib/arel_rest.rb
112
+ - lib/parser.rb
113
+ - lib/predications/between_operator.rb
114
+ - lib/predications/does_not_match_operator.rb
115
+ - lib/predications/eq_operator.rb
116
+ - lib/predications/gt_operator.rb
117
+ - lib/predications/gteq_operator.rb
118
+ - lib/predications/in_operator.rb
119
+ - lib/predications/lt_operator.rb
120
+ - lib/predications/lteq_operator.rb
121
+ - lib/predications/matches_operator.rb
122
+ - lib/predications/not_eq_operator.rb
123
+ - lib/predications/not_in_operator.rb
124
+ - lib/predications/order_operator.rb
125
+ - lib/query.rb
112
126
  homepage: https://github.com/luizfilipecosta/arel_rest
113
127
  licenses:
114
128
  - MIT