A Ruby gem for parsing and evaluating directive-like expressions, built on top of Dentaku and Keisan.
Add this line to your application's Gemfile:
gem "directive_parser", github: "alek-pol/directive_parser"And then execute:
bundle installOr build and install locally:
bundle exec rake build
gem install pkg/directive_parser-$(ruby -r ./lib/directive_parser/version -e 'print DirectiveParser::VERSION').gem- Parsing: Parses directive strings like
directive_name(param1: "value", param2: { nested: "value" }). - Validation: Validates parameters against a provided JSON Schema definition, including nested objects, required
fields, types, and
additionalProperties. - Evaluation: Evaluates expressions within parameters using configured engines (Keisan by default, Dentaku available).
- Conditional Logic: Supports
if-like directives withthenandelsebranches based on evaluated conditions. - Switch Statements: Supports
switch-like directives for matching evaluated values against multiple cases. - Logical Combinations: Combines multiple conditions using
and,or,xor, etc. - Flexible Schema Input: Accepts schema definitions as Ruby Hashes, Arrays of Hashes, JSON strings, or paths to JSON files.
- Engine Selection: Allows specifying the evaluation engine (
keisan,dentaku) per parameter or globally for evaluation.
require 'directive_parser'Here's a quick example demonstrating parsing, validation, and evaluation in one go
require 'directive_parser'
schema = {
name: 'discount',
properties: {
rule1: { type: 'string', evaluate: { enabled: true, engine: :keisan } },
rule2: { type: 'string', evaluate: { enabled: true, engine: :dentaku } },
logic: { type: 'string', enum: ['all', 'any'], default: 'all' },
then: { type: 'object', properties: { discount: { type: 'string' }, vip_status: { type: 'boolean' } } },
else: { type: 'object', properties: { discount: { type: 'string' } } }
},
required: ['rule1']
}
parser = DirectiveParser::Parser.new(schema)
directive = parser.parse(
'discount(rule1: "total_spent > 1000", rule2: "years_active >= 7", logic: any, then: { discount: "10%" }, else: { discount: "0%" })'
)
puts directive.evaluate_branch({ total_spent: 1200, years_active: 2 }) # => { discount: "10%" }
puts directive.evaluate_branch({ total_spent: 900, years_active: 8 }) # => { discount: "0%" }
puts directive.evaluate_branch({ total_spent: 900, years_active: 2 }) # => { discount: "0%" }For more granular control and understanding of the process, see the sections below.
Define your directive schema (using JSON Schema style):
schema = {
"name": "my_if",
"type": "object",
"properties": {
"condition": { "type": "string", "evaluate": { "enabled": true } },
"then": { "type": "object", "properties": {} },
"else": { "type": "object", "properties": {} }
},
"required": ["condition", "then", "else"]
}
parser = DirectiveParser::Parser.new([schema])
directive = parser.parse('my_if(condition: "x > 5", then: { result: "high" }, else: { result: "low" })')
puts directive.name # => "my_if"
puts directive.parameters # => { condition: "x > 5", then: { result: "high" }, else: { result: "low" } }You can evaluate the directive using different methods depending on the desired output:
evaluate(context): Returns a hash containing the original parameters, raw evaluation results (:raw_results), processed boolean results (:evaluated_results), the final combined logic result (:combined_resultfor conditionals, :switch_resultfor switches), any errors (:errors), and the selected branch result (:branch_result).
context = { x: 10 }
full_result = directive.evaluate(context)
puts full_result
# Output includes:
# raw_results: { condition: true },
# evaluated_results: { condition: true },
# combined_result: true,
# branch_result: { result: "high" },
# ...evaluate_branch(context): Specifically returns the value from thethenorelsebranch based on the evaluation result
context = { x: 10 }
result = directive.evaluate_branch(context)
puts result # => { result: "high" }evaluate_result(context): Returns the final outcome of the expression itself. For conditional directives, this is the:combined_result(boolean). For switch directives, this would be the:switch_result.
context = { x: 10 }
expression_result = directive.evaluate_result(context)
puts expression_result # => true (the combined_result of the condition)DirectiveParser uses JSON Schema-compatible structures to define directives. Schemas act as blueprints, allowing you to specify the expected structure, validate input parameters, and designate specific parameters for expression evaluation. They can be provided as:
- A path to a JSON file.
- A JSON string.
- A Ruby Hash representing the schema.
- An Array of directive definitions (as Hashes).
- The schema definition can follow the standard JSON Schema Draft 2019-09 format (with a
propertieskey at the root) or represent a single directive definition directly.
Schemas used by this library should declare their meta-schema using the $schema keyword:
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Directive Definitions",
"type": "object"
}Standard JSON Schema keywords like type, properties, required, enum, default, description, items, etc.,
are fully supported for defining the structure and validation rules of directive parameters.
This library introduces custom keywords within the schema definition:
name(Optional for root-level definitions): Used internally by DirectiveParser to identify the directive. For the standard root schema format ({ "properties": { ... } }), the directive's name is the key within properties. For single directive definitions ({ "name": "directive_name", ... }), it explicitly defines the name.evaluate(Optional): Applied to a parameter's definition (e.g., within properties). Indicates that the parameter's value string should be evaluated as an expression.enabled(Boolean, default:trueifevaluateis present): Enables or disables evaluation for this parameter.type(String): Specifies the type of evaluation (e.g.,expression).engine(String, optional): Specifies the evaluation engine to use (e.g.,keisan,dentaku). If not specified, a default engine may be used.
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Directive Definitions",
"type": "object",
"properties": {
"if": {
"type": "object",
"properties": {
"condition": {
"type": "string",
"evaluate": {
"enabled": true,
"type": "expression"
}
},
"then": {
"type": "object"
},
"else": {
"type": "object"
},
"engine": {
"type": "string",
"enum": [
"keisan",
"dentaku"
],
"default": "keisan"
}
},
"required": [
"condition"
]
},
"page_numeration": {
"type": "object",
"properties": {
"length": {
"type": "integer"
},
"padding": {
"type": "string"
}
},
"required": [
"length",
"padding"
]
}
}
}{
"name": "my_custom_directive",
"type": "object",
"properties": {
"param1": {
"type": "string",
"evaluate": {
"enabled": true,
"type": "expression",
"engine": "dentaku"
}
}
},
"required": [
"param1"
]
}[
{
"name": "directive_one",
"type": "object",
"properties": {}
},
{
"name": "directive_two",
"type": "object",
"properties": {}
}
]After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can
also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the
version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version,
push git commits and the created tag, and push the .gem file to rubygems.org.
make test
# Or specifically
bundle exec rspec spec/lib
bundle exec rspec spec/integrationmake lintmake docBug reports and pull requests are welcome on GitHub at https://github.com/alek_pol/directive_parser. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the DirectiveParser project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.