Skip to content

Repository files navigation

JqlRuby

A pure-Ruby parser for Jira Query Language (JQL). Parses JQL strings into an abstract syntax tree and optionally converts them into ORM queries via an adapter pattern.

The grammar is modeled after Atlassian's @atlaskit/jql-parser.

Installation

Add to your Gemfile:

gem"jql_ruby"

Or install directly:

gem install jql_ruby

Quick Start

result=JqlRuby.parse('project = MYPROJ AND status = Open ORDER BY created DESC')result.success?# => trueresult.query# => JqlRuby::Ast::Queryresult.query.where_clause# => JqlRuby::Ast::AndClauseresult.query.order_by# => JqlRuby::Ast::OrderBy

Supported Grammar

JqlRuby supports the full JQL specification:

CategorySyntax
Equality=, !=
Comparison<, >, <=, >=
Contains~ (LIKE), !~ (NOT LIKE)
SetIN (...), NOT IN (...)
Null checksIS EMPTY, IS NOT EMPTY, IS NULL, IS NOT NULL
HistoryWAS, WAS NOT, WAS IN, WAS NOT IN
ChangeCHANGED
PredicatesAFTER, BEFORE, DURING, ON, BY, FROM, TO
LogicalAND, OR, NOT, !, parentheses
OrderingORDER BY field ASC/DESC
FunctionscurrentUser(), now(), any name(args...)
Custom fieldscf[10001]

AST Nodes

Parsing produces a tree of AST nodes:

Query
├── where_clause (one of:)
│ ├── TerminalClause — field, operator, operand, predicates
│ ├── AndClause — clauses[]
│ ├── OrClause — clauses[]
│ └── NotClause — clause, operator (:not or :bang)
└── order_by
└── OrderBy — fields[] of SearchSort (field, direction)

Operand types

  • ValueOperand — string or number literal
  • FunctionOperand — function name + arguments
  • ListOperand — parenthesized list of operands
  • KeywordOperandEMPTY or NULL

Working with the AST

result=JqlRuby.parse('priority = High AND duedate < now()')clause=result.query.where_clause# => AndClauseclause.clauses[0].field.name# => "priority"clause.clauses[0].operator.value# => :eqclause.clauses[0].operand.value# => "High"clause.clauses[1].operand# => FunctionOperand(name: "now")

Every node has an accept(visitor) method for implementing the visitor pattern.

ActiveRecord Adapter

The gem includes an adapter that converts parsed JQL into ActiveRecord scopes.

Setup

adapter=JqlRuby::Adapters::ActiveRecord.new(Issue)do |config|
# simple column mapping (field name defaults to column name)config.field"status"config.field"project",column: :project_keyconfig.field"votes"config.field"created",column: :created_atconfig.field"duedate",column: :due_date# custom resolver for fields that need joins or complex logicconfig.field"assignee"do |scope,operator,value|
caseoperatorwhen:eqscope.joins(:assignee).where(users: {username: value})when:isscope.where(assignee_id: nil)when:is_notscope.where.not(assignee_id: nil)elseraiseJqlRuby::UnsupportedOperatorError,"assignee does not support #{operator}"endend# functions resolve to a value at query timeconfig.function"currentUser"do |context|
context[:current_user].usernameendconfig.function"now"do |_context|
Time.currentendend

Querying

result=JqlRuby.parse('project = FOO AND status IN (Open, "In Progress") ORDER BY created DESC')scope=adapter.apply(result.query,context: {current_user: current_user})# => Issue.where(project_key: "FOO").where(status: ["Open", "In Progress"]).order(created_at: :desc)

Operator mapping

JQL operatorArel method
=.eq
!=.not_eq
< / > / <= / >=.lt / .gt / .lteq / .gteq
~.matches (wraps with %)
!~.does_not_match
IN.in
NOT IN.not_in
IS EMPTY/NULL.eq(nil)
IS NOT EMPTY/NULL.not_eq(nil)

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.

Building Custom Adapters

The adapter pattern is designed for extension. Subclass JqlRuby::Adapters::Base and implement six hooks:

classMySequelAdapter < JqlRuby::Adapters::Baseprotecteddefbuild_scope(model)# return initial dataset/scopeenddefapply_and(scope,scopes)# combine scopes with ANDenddefapply_or(scope,scopes)# combine scopes with ORenddefapply_not(scope,inner_scope)# negate a scopeenddefapply_terminal(scope,field_def,operator,value)# apply a single field comparison# field_def.column gives you the mapped column nameenddefapply_order(scope,field_def,direction)# apply ORDER BY (direction is :asc or :desc)endend

The base class handles AST traversal, field/function resolution, and operand extraction. Your adapter only needs to translate those into ORM-specific calls.

Error Handling

result=JqlRuby.parse("invalid = = query")result.success?# => falseresult.errors# => [#<JqlRuby::ParseError ...>]result.errors.first.message# => "expected value at position 10"result.errors.first.position# => 10

Error classes

ClassRaised when
JqlRuby::ParseErrorJQL syntax is invalid
JqlRuby::LexerErrorTokenization fails (e.g. unterminated string)
JqlRuby::UnknownFieldErrorAdapter encounters an unmapped field
JqlRuby::UnknownFunctionErrorAdapter encounters an unmapped function
JqlRuby::UnsupportedOperatorErrorAdapter encounters an operator it can't handle

Development

git clone https://github.com/ignitionapp/jql_ruby.git
cd jql_ruby
bundle install
bundle exec rake spec

Contributing

Bug reports and pull requests are welcome on GitHub.

  1. Fork the repo
  2. Create your feature branch (git checkout -b my-feature)
  3. Add tests for your changes
  4. Make sure all tests pass (bundle exec rake spec)
  5. Commit and open a pull request

License

Released under the MIT License.

About

A pure-Ruby parser for Jira Query Language (JQL)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages