jql_ruby 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5a08c2507174f677a6b35095285197a2a58f1114946e86b68a7a440a95bbc1bc
4
+ data.tar.gz: cf908b48b100e8336c598fa5b8dd42ba4c506395dd6b45cbe3b46af07f61953c
5
+ SHA512:
6
+ metadata.gz: 53de7669ff8f13e8191341a2754da46d31af20510a81158e68840b5162a71c25e0c3ae1ef2cb112fbf98532fc3f3be7f21780f2a7ee82574ba15839cc120ab12
7
+ data.tar.gz: 50f81f5ef39112880e2e269930bee53f911e0149b68624c6ea319cf794a44a857b118a741fc47d6a12d4f8a9a68f555350114ca5be332304eebb82fa7c791d4d
data/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.1.0] - 2026-04-05
8
+
9
+ ### Added
10
+
11
+ - Recursive descent parser for the full JQL grammar (AND, OR, NOT, parentheses, ORDER BY)
12
+ - Terminal clause operators: `=`, `!=`, `<`, `>`, `<=`, `>=`, `~`, `!~`, `IN`, `NOT IN`, `IS`, `IS NOT`, `WAS`, `WAS NOT`, `WAS IN`, `WAS NOT IN`, `CHANGED`
13
+ - WAS/CHANGED predicate support: `AFTER`, `BEFORE`, `DURING`, `ON`, `BY`, `FROM`, `TO`
14
+ - Function operands (e.g. `currentUser()`, `startOfMonth("-1")`)
15
+ - Custom field syntax (`cf[10001]`)
16
+ - Quoted and unquoted string values, numbers
17
+ - AST node classes with visitor-pattern `accept` methods
18
+ - Adapter pattern for ORM integration (`Adapters::Base`)
19
+ - ActiveRecord adapter with Arel-based query building
20
+ - Configurable field mapping (simple column, or custom resolver block)
21
+ - Configurable function resolvers with runtime context
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ignition App Pty Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # JqlRuby
2
+
3
+ A pure-Ruby parser for [Jira Query Language (JQL)](https://support.atlassian.com/jira-software-cloud/docs/use-advanced-search-with-jira-query-language-jql/). Parses JQL strings into an abstract syntax tree and optionally converts them into ORM queries via an adapter pattern.
4
+
5
+ The grammar is modeled after Atlassian's [@atlaskit/jql-parser](https://www.npmjs.com/package/@atlaskit/jql-parser).
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem "jql_ruby"
13
+ ```
14
+
15
+ Or install directly:
16
+
17
+ ```
18
+ gem install jql_ruby
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```ruby
24
+ result = JqlRuby.parse('project = MYPROJ AND status = Open ORDER BY created DESC')
25
+
26
+ result.success? # => true
27
+ result.query # => JqlRuby::Ast::Query
28
+
29
+ result.query.where_clause # => JqlRuby::Ast::AndClause
30
+ result.query.order_by # => JqlRuby::Ast::OrderBy
31
+ ```
32
+
33
+ ## Supported Grammar
34
+
35
+ JqlRuby supports the full JQL specification:
36
+
37
+ | Category | Syntax |
38
+ |---|---|
39
+ | **Equality** | `=`, `!=` |
40
+ | **Comparison** | `<`, `>`, `<=`, `>=` |
41
+ | **Contains** | `~` (LIKE), `!~` (NOT LIKE) |
42
+ | **Set** | `IN (...)`, `NOT IN (...)` |
43
+ | **Null checks** | `IS EMPTY`, `IS NOT EMPTY`, `IS NULL`, `IS NOT NULL` |
44
+ | **History** | `WAS`, `WAS NOT`, `WAS IN`, `WAS NOT IN` |
45
+ | **Change** | `CHANGED` |
46
+ | **Predicates** | `AFTER`, `BEFORE`, `DURING`, `ON`, `BY`, `FROM`, `TO` |
47
+ | **Logical** | `AND`, `OR`, `NOT`, `!`, parentheses |
48
+ | **Ordering** | `ORDER BY field ASC/DESC` |
49
+ | **Functions** | `currentUser()`, `now()`, any `name(args...)` |
50
+ | **Custom fields** | `cf[10001]` |
51
+
52
+ ## AST Nodes
53
+
54
+ Parsing produces a tree of AST nodes:
55
+
56
+ ```
57
+ Query
58
+ ├── where_clause (one of:)
59
+ │ ├── TerminalClause — field, operator, operand, predicates
60
+ │ ├── AndClause — clauses[]
61
+ │ ├── OrClause — clauses[]
62
+ │ └── NotClause — clause, operator (:not or :bang)
63
+ └── order_by
64
+ └── OrderBy — fields[] of SearchSort (field, direction)
65
+ ```
66
+
67
+ ### Operand types
68
+
69
+ - `ValueOperand` — string or number literal
70
+ - `FunctionOperand` — function name + arguments
71
+ - `ListOperand` — parenthesized list of operands
72
+ - `KeywordOperand` — `EMPTY` or `NULL`
73
+
74
+ ### Working with the AST
75
+
76
+ ```ruby
77
+ result = JqlRuby.parse('priority = High AND duedate < now()')
78
+ clause = result.query.where_clause # => AndClause
79
+
80
+ clause.clauses[0].field.name # => "priority"
81
+ clause.clauses[0].operator.value # => :eq
82
+ clause.clauses[0].operand.value # => "High"
83
+
84
+ clause.clauses[1].operand # => FunctionOperand(name: "now")
85
+ ```
86
+
87
+ Every node has an `accept(visitor)` method for implementing the visitor pattern.
88
+
89
+ ## ActiveRecord Adapter
90
+
91
+ The gem includes an adapter that converts parsed JQL into ActiveRecord scopes.
92
+
93
+ ### Setup
94
+
95
+ ```ruby
96
+ adapter = JqlRuby::Adapters::ActiveRecord.new(Issue) do |config|
97
+ # simple column mapping (field name defaults to column name)
98
+ config.field "status"
99
+ config.field "project", column: :project_key
100
+ config.field "votes"
101
+ config.field "created", column: :created_at
102
+ config.field "duedate", column: :due_date
103
+
104
+ # custom resolver for fields that need joins or complex logic
105
+ config.field "assignee" do |scope, operator, value|
106
+ case operator
107
+ when :eq
108
+ scope.joins(:assignee).where(users: { username: value })
109
+ when :is
110
+ scope.where(assignee_id: nil)
111
+ when :is_not
112
+ scope.where.not(assignee_id: nil)
113
+ else
114
+ raise JqlRuby::UnsupportedOperatorError, "assignee does not support #{operator}"
115
+ end
116
+ end
117
+
118
+ # functions resolve to a value at query time
119
+ config.function "currentUser" do |context|
120
+ context[:current_user].username
121
+ end
122
+
123
+ config.function "now" do |_context|
124
+ Time.current
125
+ end
126
+ end
127
+ ```
128
+
129
+ ### Querying
130
+
131
+ ```ruby
132
+ result = JqlRuby.parse('project = FOO AND status IN (Open, "In Progress") ORDER BY created DESC')
133
+ scope = adapter.apply(result.query, context: { current_user: current_user })
134
+ # => Issue.where(project_key: "FOO").where(status: ["Open", "In Progress"]).order(created_at: :desc)
135
+ ```
136
+
137
+ ### Operator mapping
138
+
139
+ | JQL operator | Arel method |
140
+ |---|---|
141
+ | `=` | `.eq` |
142
+ | `!=` | `.not_eq` |
143
+ | `<` / `>` / `<=` / `>=` | `.lt` / `.gt` / `.lteq` / `.gteq` |
144
+ | `~` | `.matches` (wraps with `%`) |
145
+ | `!~` | `.does_not_match` |
146
+ | `IN` | `.in` |
147
+ | `NOT IN` | `.not_in` |
148
+ | `IS EMPTY/NULL` | `.eq(nil)` |
149
+ | `IS NOT EMPTY/NULL` | `.not_eq(nil)` |
150
+
151
+ `WAS`, `WAS NOT`, `WAS IN`, `WAS NOT IN`, and `CHANGED` raise `UnsupportedOperatorError` since they require history tables. Use a custom field resolver block to handle these for your schema.
152
+
153
+ ## Building Custom Adapters
154
+
155
+ The adapter pattern is designed for extension. Subclass `JqlRuby::Adapters::Base` and implement six hooks:
156
+
157
+ ```ruby
158
+ class MySequelAdapter < JqlRuby::Adapters::Base
159
+ protected
160
+
161
+ def build_scope(model)
162
+ # return initial dataset/scope
163
+ end
164
+
165
+ def apply_and(scope, scopes)
166
+ # combine scopes with AND
167
+ end
168
+
169
+ def apply_or(scope, scopes)
170
+ # combine scopes with OR
171
+ end
172
+
173
+ def apply_not(scope, inner_scope)
174
+ # negate a scope
175
+ end
176
+
177
+ def apply_terminal(scope, field_def, operator, value)
178
+ # apply a single field comparison
179
+ # field_def.column gives you the mapped column name
180
+ end
181
+
182
+ def apply_order(scope, field_def, direction)
183
+ # apply ORDER BY (direction is :asc or :desc)
184
+ end
185
+ end
186
+ ```
187
+
188
+ The base class handles AST traversal, field/function resolution, and operand extraction. Your adapter only needs to translate those into ORM-specific calls.
189
+
190
+ ## Error Handling
191
+
192
+ ```ruby
193
+ result = JqlRuby.parse("invalid = = query")
194
+ result.success? # => false
195
+ result.errors # => [#<JqlRuby::ParseError ...>]
196
+ result.errors.first.message # => "expected value at position 10"
197
+ result.errors.first.position # => 10
198
+ ```
199
+
200
+ ### Error classes
201
+
202
+ | Class | Raised when |
203
+ |---|---|
204
+ | `JqlRuby::ParseError` | JQL syntax is invalid |
205
+ | `JqlRuby::LexerError` | Tokenization fails (e.g. unterminated string) |
206
+ | `JqlRuby::UnknownFieldError` | Adapter encounters an unmapped field |
207
+ | `JqlRuby::UnknownFunctionError` | Adapter encounters an unmapped function |
208
+ | `JqlRuby::UnsupportedOperatorError` | Adapter encounters an operator it can't handle |
209
+
210
+ ## Development
211
+
212
+ ```
213
+ git clone https://github.com/ignitionapp/jql_ruby.git
214
+ cd jql_ruby
215
+ bundle install
216
+ bundle exec rake spec
217
+ ```
218
+
219
+ ## Contributing
220
+
221
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/ignitionapp/jql_ruby).
222
+
223
+ 1. Fork the repo
224
+ 2. Create your feature branch (`git checkout -b my-feature`)
225
+ 3. Add tests for your changes
226
+ 4. Make sure all tests pass (`bundle exec rake spec`)
227
+ 5. Commit and open a pull request
228
+
229
+ ## License
230
+
231
+ Released under the [MIT License](LICENSE).
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JqlRuby
4
+ module Adapters
5
+ class ActiveRecord < Base
6
+ protected
7
+
8
+ def build_scope(model)
9
+ model.all
10
+ end
11
+
12
+ def apply_and(_scope, scopes)
13
+ scopes.reduce(:merge)
14
+ end
15
+
16
+ def apply_or(_scope, scopes)
17
+ scopes.reduce(:or)
18
+ end
19
+
20
+ def apply_not(scope, inner_scope)
21
+ scope.where(inner_scope.where_clause.ast.not)
22
+ end
23
+
24
+ def apply_terminal(scope, field_def, operator, value)
25
+ column = arel_column(field_def)
26
+
27
+ predicate = case operator
28
+ when :eq
29
+ column.eq(value)
30
+ when :not_eq
31
+ column.not_eq(value)
32
+ when :lt
33
+ column.lt(value)
34
+ when :gt
35
+ column.gt(value)
36
+ when :lteq
37
+ column.lteq(value)
38
+ when :gteq
39
+ column.gteq(value)
40
+ when :like
41
+ column.matches("%#{sanitize_like(value)}%", "\\", true)
42
+ when :not_like
43
+ column.does_not_match("%#{sanitize_like(value)}%", "\\", true)
44
+ when :in
45
+ column.in(value)
46
+ when :not_in
47
+ column.not_in(value)
48
+ when :is
49
+ column.eq(nil)
50
+ when :is_not
51
+ column.not_eq(nil)
52
+ else
53
+ raise UnsupportedOperatorError, "unsupported operator: #{operator.inspect}"
54
+ end
55
+
56
+ scope.where(predicate)
57
+ end
58
+
59
+ def apply_order(scope, field_def, direction)
60
+ column = arel_column(field_def)
61
+ scope.order(column.send(direction))
62
+ end
63
+
64
+ private
65
+
66
+ def arel_column(field_def)
67
+ model.arel_table[field_def.column]
68
+ end
69
+
70
+ def sanitize_like(value)
71
+ value.to_s.gsub("\\", "\\\\\\\\").gsub("%", "\\%").gsub("_", "\\_")
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JqlRuby
4
+ module Adapters
5
+ class Base
6
+ attr_reader :model, :config
7
+
8
+ def initialize(model, &block)
9
+ @model = model
10
+ @config = Configuration.new
11
+ block&.call(@config)
12
+ end
13
+
14
+ def apply(query, context: {})
15
+ scope = build_scope(model)
16
+ scope = visit_where(scope, query.where_clause, context) if query.where_clause
17
+ scope = visit_order_by(scope, query.order_by, context) if query.order_by
18
+ scope
19
+ end
20
+
21
+ protected
22
+
23
+ # subclasses must override these hooks
24
+
25
+ def build_scope(_model)
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def apply_and(_scope, _scopes)
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def apply_or(_scope, _scopes)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def apply_not(_scope, _inner_scope)
38
+ raise NotImplementedError
39
+ end
40
+
41
+ def apply_terminal(_scope, _field_def, _operator, _value)
42
+ raise NotImplementedError
43
+ end
44
+
45
+ def apply_order(_scope, _field_def, _direction)
46
+ raise NotImplementedError
47
+ end
48
+
49
+ private
50
+
51
+ def visit_where(scope, node, context)
52
+ case node
53
+ when Ast::OrClause
54
+ visit_or(scope, node, context)
55
+ when Ast::AndClause
56
+ visit_and(scope, node, context)
57
+ when Ast::NotClause
58
+ visit_not(scope, node, context)
59
+ when Ast::TerminalClause
60
+ visit_terminal(scope, node, context)
61
+ else
62
+ raise AdapterError, "unexpected AST node: #{node.class}"
63
+ end
64
+ end
65
+
66
+ def visit_or(scope, node, context)
67
+ scopes = node.clauses.map { |clause| visit_where(build_scope(model), clause, context) }
68
+ apply_or(scope, scopes)
69
+ end
70
+
71
+ def visit_and(scope, node, context)
72
+ scopes = node.clauses.map { |clause| visit_where(build_scope(model), clause, context) }
73
+ apply_and(scope, scopes)
74
+ end
75
+
76
+ def visit_not(scope, node, context)
77
+ inner = visit_where(build_scope(model), node.clause, context)
78
+ apply_not(scope, inner)
79
+ end
80
+
81
+ def visit_terminal(scope, node, context)
82
+ field_name = resolve_field_name(node.field)
83
+ field_def = config.resolve_field(field_name)
84
+ operator = node.operator.value
85
+ value = resolve_operand(node.operand, context)
86
+ predicates = resolve_predicates(node.predicates, context)
87
+
88
+ if field_def.custom?
89
+ if field_def.resolver.arity == 4 || field_def.resolver.arity < 0
90
+ field_def.resolver.call(scope, operator, value, predicates)
91
+ else
92
+ field_def.resolver.call(scope, operator, value)
93
+ end
94
+ else
95
+ validate_operator!(operator)
96
+ apply_terminal(scope, field_def, operator, value)
97
+ end
98
+ end
99
+
100
+ def visit_order_by(scope, node, _context)
101
+ node.fields.each do |sort|
102
+ field_name = resolve_field_name(sort.field)
103
+ field_def = config.resolve_field(field_name)
104
+ direction = sort.direction || :asc
105
+ scope = apply_order(scope, field_def, direction)
106
+ end
107
+ scope
108
+ end
109
+
110
+ def resolve_field_name(field)
111
+ case field
112
+ when Ast::Field
113
+ field.name
114
+ when Ast::CustomField
115
+ "cf[#{field.id}]"
116
+ else
117
+ raise AdapterError, "unexpected field type: #{field.class}"
118
+ end
119
+ end
120
+
121
+ def resolve_operand(operand, context)
122
+ case operand
123
+ when nil
124
+ nil
125
+ when Ast::ValueOperand
126
+ operand.value
127
+ when Ast::KeywordOperand
128
+ nil
129
+ when Ast::FunctionOperand
130
+ config.resolve_function(operand.name, context)
131
+ when Ast::ListOperand
132
+ operand.values.map { |v| resolve_operand(v, context) }
133
+ else
134
+ raise AdapterError, "unexpected operand type: #{operand.class}"
135
+ end
136
+ end
137
+
138
+ def resolve_predicates(predicates, context)
139
+ predicates.map do |pred|
140
+ { operator: pred.operator, operand: resolve_operand(pred.operand, context) }
141
+ end
142
+ end
143
+
144
+ UNSUPPORTED_OPERATORS = %i[was was_not was_in was_not_in changed].freeze
145
+
146
+ def validate_operator!(operator)
147
+ if UNSUPPORTED_OPERATORS.include?(operator)
148
+ raise UnsupportedOperatorError,
149
+ "operator #{operator.inspect} requires a custom field resolver (use a block)"
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JqlRuby
4
+ module Adapters
5
+ FieldDefinition = Struct.new(:name, :column, :resolver, keyword_init: true) do
6
+ def custom?
7
+ !resolver.nil?
8
+ end
9
+ end
10
+
11
+ class Configuration
12
+ attr_reader :field_definitions, :function_resolvers
13
+
14
+ def initialize
15
+ @field_definitions = {}
16
+ @function_resolvers = {}
17
+ end
18
+
19
+ def field(name, column: nil, &block)
20
+ normalized = name.to_s.downcase
21
+ col = column || name.to_sym
22
+ @field_definitions[normalized] = FieldDefinition.new(
23
+ name: normalized,
24
+ column: col,
25
+ resolver: block
26
+ )
27
+ end
28
+
29
+ def function(name, &block)
30
+ normalized = name.to_s.downcase
31
+ @function_resolvers[normalized] = block
32
+ end
33
+
34
+ def resolve_field(name)
35
+ normalized = name.to_s.downcase
36
+ @field_definitions.fetch(normalized) do
37
+ raise UnknownFieldError, "unknown JQL field: #{name.inspect}"
38
+ end
39
+ end
40
+
41
+ def resolve_function(name, context)
42
+ normalized = name.to_s.downcase
43
+ resolver = @function_resolvers.fetch(normalized) do
44
+ raise UnknownFunctionError, "unknown JQL function: #{name.inspect}"
45
+ end
46
+ resolver.call(context)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "adapters/configuration"
4
+ require_relative "adapters/base"
5
+ require_relative "adapters/active_record"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JqlRuby
4
+ module Ast
5
+ class OrClause < Node
6
+ attr_reader :clauses
7
+
8
+ def initialize(clauses:, **rest)
9
+ super(**rest)
10
+ @clauses = clauses
11
+ end
12
+
13
+ def accept(visitor)
14
+ visitor.visit_or_clause(self)
15
+ end
16
+ end
17
+
18
+ class AndClause < Node
19
+ attr_reader :clauses
20
+
21
+ def initialize(clauses:, **rest)
22
+ super(**rest)
23
+ @clauses = clauses
24
+ end
25
+
26
+ def accept(visitor)
27
+ visitor.visit_and_clause(self)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JqlRuby
4
+ module Ast
5
+ class Field < Node
6
+ attr_reader :name, :properties
7
+
8
+ def initialize(name:, properties: [], **rest)
9
+ super(**rest)
10
+ @name = name
11
+ @properties = properties
12
+ end
13
+
14
+ def accept(visitor)
15
+ visitor.visit_field(self)
16
+ end
17
+ end
18
+
19
+ class CustomField < Node
20
+ attr_reader :id, :properties
21
+
22
+ def initialize(id:, properties: [], **rest)
23
+ super(**rest)
24
+ @id = id
25
+ @properties = properties
26
+ end
27
+
28
+ def accept(visitor)
29
+ visitor.visit_custom_field(self)
30
+ end
31
+ end
32
+
33
+ class FieldProperty < Node
34
+ attr_reader :keys, :argument
35
+
36
+ def initialize(keys:, argument: nil, **rest)
37
+ super(**rest)
38
+ @keys = keys
39
+ @argument = argument
40
+ end
41
+
42
+ def accept(visitor)
43
+ visitor.visit_field_property(self)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JqlRuby
4
+ module Ast
5
+ class Node
6
+ attr_reader :position
7
+
8
+ def initialize(position: nil)
9
+ @position = position
10
+ end
11
+
12
+ def accept(visitor)
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def ==(other)
17
+ other.is_a?(self.class) && instance_variables.all? do |ivar|
18
+ instance_variable_get(ivar) == other.instance_variable_get(ivar)
19
+ end
20
+ end
21
+
22
+ alias_method :eql?, :==
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JqlRuby
4
+ module Ast
5
+ class NotClause < Node
6
+ attr_reader :clause, :operator
7
+
8
+ def initialize(clause:, operator: :not, **rest)
9
+ super(**rest)
10
+ @clause = clause
11
+ @operator = operator
12
+ end
13
+
14
+ def accept(visitor)
15
+ visitor.visit_not_clause(self)
16
+ end
17
+ end
18
+ end
19
+ end