filter_param 0.1.0

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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE +21 -0
  4. data/README.md +138 -0
  5. data/lib/filter_param/ast/attribute.rb +15 -0
  6. data/lib/filter_param/ast/expressions.rb +37 -0
  7. data/lib/filter_param/ast/group.rb +13 -0
  8. data/lib/filter_param/ast/literal.rb +26 -0
  9. data/lib/filter_param/ast/literals/boolean.rb +38 -0
  10. data/lib/filter_param/ast/literals/date.rb +33 -0
  11. data/lib/filter_param/ast/literals/date_time.rb +36 -0
  12. data/lib/filter_param/ast/literals/decimal.rb +29 -0
  13. data/lib/filter_param/ast/literals/integer.rb +39 -0
  14. data/lib/filter_param/ast/literals/null.rb +19 -0
  15. data/lib/filter_param/ast/literals/string.rb +43 -0
  16. data/lib/filter_param/ast/node.rb +15 -0
  17. data/lib/filter_param/ast/scope.rb +12 -0
  18. data/lib/filter_param/definition.rb +157 -0
  19. data/lib/filter_param/field.rb +45 -0
  20. data/lib/filter_param/operator.rb +46 -0
  21. data/lib/filter_param/operators/and.rb +13 -0
  22. data/lib/filter_param/operators/case_insensitive_equal.rb +16 -0
  23. data/lib/filter_param/operators/contains.rb +18 -0
  24. data/lib/filter_param/operators/ends_with.rb +18 -0
  25. data/lib/filter_param/operators/equal.rb +21 -0
  26. data/lib/filter_param/operators/field_filter_operator.rb +40 -0
  27. data/lib/filter_param/operators/greater_than.rb +16 -0
  28. data/lib/filter_param/operators/greater_than_equal.rb +16 -0
  29. data/lib/filter_param/operators/group.rb +17 -0
  30. data/lib/filter_param/operators/less_than.rb +16 -0
  31. data/lib/filter_param/operators/less_than_equal.rb +16 -0
  32. data/lib/filter_param/operators/not.rb +16 -0
  33. data/lib/filter_param/operators/not_equal.rb +19 -0
  34. data/lib/filter_param/operators/or.rb +13 -0
  35. data/lib/filter_param/operators/present.rb +21 -0
  36. data/lib/filter_param/operators/starts_with.rb +18 -0
  37. data/lib/filter_param/parser.rb +144 -0
  38. data/lib/filter_param/scope.rb +24 -0
  39. data/lib/filter_param/transformer.rb +37 -0
  40. data/lib/filter_param/transpiler.rb +114 -0
  41. data/lib/filter_param/version.rb +5 -0
  42. data/lib/filter_param.rb +70 -0
  43. metadata +143 -0
@@ -0,0 +1,114 @@
1
+ module FilterParam
2
+ class Transpiler
3
+ def initialize(ar_relation, definition)
4
+ @ar_relation = ar_relation
5
+ @definition = definition
6
+ end
7
+
8
+ def transpile!(string_expression)
9
+ return nil if string_expression.blank?
10
+
11
+ ast_root = parse(string_expression)
12
+
13
+ visit(ast_root)
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :ar_relation, :definition
19
+
20
+ def ar_model
21
+ @ar_relation.model
22
+ end
23
+
24
+ def parse(string_expression)
25
+ parse_tree = Parser.new.parse(string_expression, reporter: Parslet::ErrorReporter::Deepest.new)
26
+
27
+ Transformer.new.apply(parse_tree)
28
+ end
29
+
30
+ def field_for_name(name)
31
+ definition.find_field!(name)
32
+ end
33
+
34
+ def visit(node)
35
+ return node unless node.respond_to?(:accept)
36
+
37
+ node.accept(self)
38
+ end
39
+
40
+ def visit_group(group)
41
+ expression = visit(group.expression)
42
+
43
+ Operator.for(:group).sql(expression)
44
+ end
45
+
46
+ def visit_scope(scope)
47
+ actual_scope_name = definition.find_scope!(scope.name)
48
+ .actual_name
49
+ scope_args = scope.args.map { |arg| visit(arg) }
50
+
51
+ scope_sql = ar_model.send(actual_scope_name, *scope_args)
52
+ .select(ar_model.primary_key)
53
+ .to_sql
54
+
55
+ "(#{ar_model.primary_key} IN (#{scope_sql}))"
56
+ end
57
+
58
+ def visit_attribute(attribute)
59
+ field_for_name(attribute.name)
60
+ end
61
+
62
+ def visit_literal(literal)
63
+ literal.value
64
+ end
65
+
66
+ def visit_unary_expression(expression)
67
+ operator_symbol = expression.operator
68
+ operand = expression.operand
69
+ return transpile_negated_expression(operand) if operator_symbol == :not
70
+
71
+ operand = visit(operand)
72
+ Operator.for(operator_symbol).sql(operand)
73
+ end
74
+
75
+ def visit_binary_expression(expression)
76
+ operator_symbol = expression.operator
77
+ operator = Operator.for(operator_symbol)
78
+
79
+ if operator < Operators::FieldFilterOperator
80
+ field = visit(expression.left_operand)
81
+ literal = expression.right_operand
82
+ literal.value = field.transform_value(literal.value)
83
+
84
+ return operator.sql(field, literal.type_cast(field.type))
85
+ end
86
+
87
+ operator.sql(visit(expression.left_operand),
88
+ visit(expression.right_operand))
89
+ end
90
+
91
+ def transpile_negated_expression(expression)
92
+ operator_tag = expression.operator
93
+ operator = Operator.for(operator_tag)
94
+ not_operator = Operator.for(:not)
95
+
96
+ if operator < Operators::FieldFilterOperator
97
+ # TODO: refactor
98
+ if operator.type == :binary
99
+ field = visit(expression.left_operand)
100
+ literal = expression.right_operand
101
+ literal.value = field.transform_value(literal.value)
102
+
103
+ return not_operator.sql(operator_tag, field, literal.type_cast(field.type))
104
+ else
105
+ return not_operator.sql(operator_tag, visit(expression.operand))
106
+ end
107
+ end
108
+
109
+ operands = expression.operands.map { |operand| visit(operand) }
110
+
111
+ not_operator.sql(operator_tag, *operands)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FilterParam
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parslet"
4
+ require "active_record"
5
+ require "active_support"
6
+ require "active_support/core_ext/object/inclusion"
7
+ require_relative "filter_param/ast/node"
8
+ require_relative "filter_param/ast/attribute"
9
+ require_relative "filter_param/ast/expressions"
10
+ require_relative "filter_param/ast/group"
11
+ require_relative "filter_param/ast/scope"
12
+ require_relative "filter_param/ast/literal"
13
+ require_relative "filter_param/ast/literals/null"
14
+ require_relative "filter_param/ast/literals/string"
15
+ require_relative "filter_param/ast/literals/boolean"
16
+ require_relative "filter_param/ast/literals/integer"
17
+ require_relative "filter_param/ast/literals/decimal"
18
+ require_relative "filter_param/ast/literals/date"
19
+ require_relative "filter_param/ast/literals/date_time"
20
+ require_relative "filter_param/operator"
21
+ require_relative "filter_param/operators/group"
22
+ require_relative "filter_param/operators/field_filter_operator"
23
+ require_relative "filter_param/operators/and"
24
+ require_relative "filter_param/operators/or"
25
+ require_relative "filter_param/operators/not"
26
+ require_relative "filter_param/operators/equal"
27
+ require_relative "filter_param/operators/not_equal"
28
+ require_relative "filter_param/operators/case_insensitive_equal"
29
+ require_relative "filter_param/operators/less_than"
30
+ require_relative "filter_param/operators/less_than_equal"
31
+ require_relative "filter_param/operators/greater_than"
32
+ require_relative "filter_param/operators/greater_than_equal"
33
+ require_relative "filter_param/operators/starts_with"
34
+ require_relative "filter_param/operators/ends_with"
35
+ require_relative "filter_param/operators/contains"
36
+ require_relative "filter_param/operators/present"
37
+ require_relative "filter_param/transformer"
38
+ require_relative "filter_param/parser"
39
+ require_relative "filter_param/transpiler"
40
+ require_relative "filter_param/field"
41
+ require_relative "filter_param/scope"
42
+ require_relative "filter_param/definition"
43
+ require_relative "filter_param/version"
44
+
45
+ module FilterParam
46
+ class BaseError < StandardError; end
47
+ class UnknownType < BaseError; end
48
+ class ExpressionError < BaseError; end
49
+ class UnknownField < BaseError; end
50
+ class UnknownScope < BaseError; end
51
+ class InvalidLiteral < ExpressionError; end
52
+ class ParseError < ExpressionError; end
53
+
54
+ # Creates a new FilterParam definition that whitelists the columns that are allowed to
55
+ # be filtered (i.e. used in SQL WHERE).
56
+ #
57
+ # @param [Proc] block Field definition block
58
+ #
59
+ # @example
60
+ # FilterParam.define do
61
+ # fields :first_name, :last_name
62
+ # field :birth_date, rename: bdate
63
+ # end
64
+ #
65
+ # @return [Definition] Filter param definition
66
+ #
67
+ def self.define(&block)
68
+ Definition.new.define(&block)
69
+ end
70
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: filter_param
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Uy Jayson B
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-05-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parslet
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4.2'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '7.1'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '4.2'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '7.1'
47
+ - !ruby/object:Gem::Dependency
48
+ name: activesupport
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '4.2'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '7.1'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '4.2'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '7.1'
67
+ description: Filter records using a SCIM inspired filter expression
68
+ email:
69
+ - uy.json.dev@gmail.com
70
+ executables: []
71
+ extensions: []
72
+ extra_rdoc_files: []
73
+ files:
74
+ - CHANGELOG.md
75
+ - LICENSE
76
+ - README.md
77
+ - lib/filter_param.rb
78
+ - lib/filter_param/ast/attribute.rb
79
+ - lib/filter_param/ast/expressions.rb
80
+ - lib/filter_param/ast/group.rb
81
+ - lib/filter_param/ast/literal.rb
82
+ - lib/filter_param/ast/literals/boolean.rb
83
+ - lib/filter_param/ast/literals/date.rb
84
+ - lib/filter_param/ast/literals/date_time.rb
85
+ - lib/filter_param/ast/literals/decimal.rb
86
+ - lib/filter_param/ast/literals/integer.rb
87
+ - lib/filter_param/ast/literals/null.rb
88
+ - lib/filter_param/ast/literals/string.rb
89
+ - lib/filter_param/ast/node.rb
90
+ - lib/filter_param/ast/scope.rb
91
+ - lib/filter_param/definition.rb
92
+ - lib/filter_param/field.rb
93
+ - lib/filter_param/operator.rb
94
+ - lib/filter_param/operators/and.rb
95
+ - lib/filter_param/operators/case_insensitive_equal.rb
96
+ - lib/filter_param/operators/contains.rb
97
+ - lib/filter_param/operators/ends_with.rb
98
+ - lib/filter_param/operators/equal.rb
99
+ - lib/filter_param/operators/field_filter_operator.rb
100
+ - lib/filter_param/operators/greater_than.rb
101
+ - lib/filter_param/operators/greater_than_equal.rb
102
+ - lib/filter_param/operators/group.rb
103
+ - lib/filter_param/operators/less_than.rb
104
+ - lib/filter_param/operators/less_than_equal.rb
105
+ - lib/filter_param/operators/not.rb
106
+ - lib/filter_param/operators/not_equal.rb
107
+ - lib/filter_param/operators/or.rb
108
+ - lib/filter_param/operators/present.rb
109
+ - lib/filter_param/operators/starts_with.rb
110
+ - lib/filter_param/parser.rb
111
+ - lib/filter_param/scope.rb
112
+ - lib/filter_param/transformer.rb
113
+ - lib/filter_param/transpiler.rb
114
+ - lib/filter_param/version.rb
115
+ homepage: https://github.com/jsonb-uy/filter_param
116
+ licenses:
117
+ - MIT
118
+ metadata:
119
+ homepage_uri: https://github.com/jsonb-uy/filter_param
120
+ source_code_uri: https://github.com/jsonb-uy/filter_param
121
+ changelog_uri: https://github.com/jsonb-uy/filter_param/blob/main/CHANGELOG.md
122
+ documentation_uri: https://rubydoc.info/github/jsonb-uy/filter_param/main
123
+ bug_tracker_uri: https://github.com/jsonb-uy/filter_param/issues
124
+ post_install_message:
125
+ rdoc_options: []
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: 2.3.0
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ requirements: []
139
+ rubygems_version: 3.4.6
140
+ signing_key:
141
+ specification_version: 4
142
+ summary: SCIM-style API filter for ActiveRecord-based apps
143
+ test_files: []