Skip to content

alek-pol/directive_parser

Repository files navigation

DirectiveParser

A Ruby gem for parsing and evaluating directive-like expressions, built on top of Dentaku and Keisan.

Installation

Add this line to your application's Gemfile:

gem "directive_parser", github: "alek-pol/directive_parser"

And then execute:

bundle install

Or build and install locally:

bundle exec rake build
gem install pkg/directive_parser-$(ruby -r ./lib/directive_parser/version -e 'print DirectiveParser::VERSION').gem

Supported Features

  • 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 with then and else branches 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.

Usage

require 'directive_parser'

Basic Parsing and Evaluation Example

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%" }

More Detailed Examples

For more granular control and understanding of the process, see the sections below.

Basic Parsing

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" } }

Evaluation

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_result for conditionals, :switch_result for 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 the then or else branch 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)

Schemas

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 properties key at the root) or represent a single directive definition directly.

Meta-Schema Declaration

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

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.

Custom Keywords

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: true if evaluate is 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.

Example Schema (Root Format):

{
  "$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"
      ]
    }
  }
}

Example Schema (Single Definition):

{
  "name": "my_custom_directive",
  "type": "object",
  "properties": {
    "param1": {
      "type": "string",
      "evaluate": {
        "enabled": true,
        "type": "expression",
        "engine": "dentaku"
      }
    }
  },
  "required": [
    "param1"
  ]
}

Example Schema (Array Format):

[
  {
    "name": "directive_one",
    "type": "object",
    "properties": {}
  },
  {
    "name": "directive_two",
    "type": "object",
    "properties": {}
  }
]

Development

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.

Running Tests

make test
# Or specifically
bundle exec rspec spec/lib
bundle exec rspec spec/integration

Linting

make lint

Generating Documentation

make doc

Contributing

Bug 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.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the DirectiveParser project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

A Ruby gem for parsing and evaluating directive-like expressions

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors