Import of plugin's code
Change-Id: Ic773558145bd86d46f02151644f1dc435c7b23c2
This commit is contained in:
parent
97e4d12ca2
commit
983230fe86
|
@ -0,0 +1,71 @@
|
||||||
|
Sensu plugin for Fuel
|
||||||
|
=================================
|
||||||
|
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
|
Sensu plugin for Fuel extends Mirantis OpenStack functionality by adding
|
||||||
|
Sensu monitoring. It can be deployed on hosts with Stacklight plugins roles.
|
||||||
|
Sensu plugin is 100% hot-pluggable.
|
||||||
|
|
||||||
|
|
||||||
|
Compatible Fuel versions
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
9.0
|
||||||
|
|
||||||
|
|
||||||
|
User Guide
|
||||||
|
----------
|
||||||
|
|
||||||
|
1. Create an environment or open existing. In case of new environment select new nodes
|
||||||
|
with LMA Stacklights roles
|
||||||
|
2. Enable the plugin on the Settings/Other tab of the Fuel web UI and fill in form
|
||||||
|
fields:
|
||||||
|
* in development
|
||||||
|
|
||||||
|
3. Deploy the environment.
|
||||||
|
|
||||||
|
|
||||||
|
Installation Guide
|
||||||
|
==================
|
||||||
|
|
||||||
|
Sensu Plugin for Fuel installation
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
To install Sensu plugin, follow these steps:
|
||||||
|
|
||||||
|
1. Download the plugin
|
||||||
|
git clone https://github.com/openstack/fuel-plugin-sensu
|
||||||
|
|
||||||
|
2. Copy the plugin on already installed Fuel Master node; ssh can be used for
|
||||||
|
that. If you do not have the Fuel Master node yet, see
|
||||||
|
[Quick Start Guide](https://software.mirantis.com/quick-start/):
|
||||||
|
|
||||||
|
# scp fuel-plugin-sensu-0.1.1-1.noarch.rpm root@<Fuel_master_ip>:/tmp
|
||||||
|
|
||||||
|
3. Log into the Fuel Master node. Install the plugin:
|
||||||
|
|
||||||
|
# cd /tmp
|
||||||
|
# fuel plugins --install fuel-plugin-sensu-0.1.1-1.noarch.rpm
|
||||||
|
|
||||||
|
4. Check if the plugin was installed successfully:
|
||||||
|
|
||||||
|
# fuel plugins
|
||||||
|
id | name | version | package_version
|
||||||
|
---|---------------------------------|---------|----------------
|
||||||
|
1 | fuel-plugin-sensu | 0.1.1 | 4.0.0
|
||||||
|
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
| Requirement | Version/Comment |
|
||||||
|
|:---------------------------------|:----------------|
|
||||||
|
| Mirantis OpenStack compatibility | 9.0 |
|
||||||
|
|
||||||
|
|
||||||
|
Limitations
|
||||||
|
-----------
|
||||||
|
|
||||||
|
This plugin can be used only with Stacklight LMA nodes
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/opt/sensu/embedded/bin/ruby
|
||||||
|
#
|
||||||
|
# This file was generated by RubyGems.
|
||||||
|
#
|
||||||
|
# The application 'sensu-plugins-cpu-checks' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
version = ">= 0.a"
|
||||||
|
|
||||||
|
if ARGV.first
|
||||||
|
str = ARGV.first
|
||||||
|
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||||
|
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||||
|
version = $1
|
||||||
|
ARGV.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load Gem.activate_bin_path('sensu-plugins-cpu-checks', 'check-cpu.rb', version)
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/opt/sensu/embedded/bin/ruby
|
||||||
|
#
|
||||||
|
# This file was generated by RubyGems.
|
||||||
|
#
|
||||||
|
# The application 'sensu-plugins-influxdb' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
version = ">= 0.a"
|
||||||
|
|
||||||
|
if ARGV.first
|
||||||
|
str = ARGV.first
|
||||||
|
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||||
|
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||||
|
version = $1
|
||||||
|
ARGV.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load Gem.activate_bin_path('sensu-plugins-influxdb', 'check-influxdb-query.rb', version)
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/opt/sensu/embedded/bin/ruby
|
||||||
|
#
|
||||||
|
# This file was generated by RubyGems.
|
||||||
|
#
|
||||||
|
# The application 'sensu-plugins-influxdb' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
version = ">= 0.a"
|
||||||
|
|
||||||
|
if ARGV.first
|
||||||
|
str = ARGV.first
|
||||||
|
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||||
|
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||||
|
version = $1
|
||||||
|
ARGV.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load Gem.activate_bin_path('sensu-plugins-influxdb', 'check-influxdb.rb', version)
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/opt/sensu/embedded/bin/ruby
|
||||||
|
#
|
||||||
|
# This file was generated by RubyGems.
|
||||||
|
#
|
||||||
|
# The application 'jsonpath' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
version = ">= 0.a"
|
||||||
|
|
||||||
|
if ARGV.first
|
||||||
|
str = ARGV.first
|
||||||
|
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||||
|
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||||
|
version = $1
|
||||||
|
ARGV.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load Gem.activate_bin_path('jsonpath', 'jsonpath', version)
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/opt/sensu/embedded/bin/ruby
|
||||||
|
#
|
||||||
|
# This file was generated by RubyGems.
|
||||||
|
#
|
||||||
|
# The application 'sensu-plugins-cpu-checks' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
version = ">= 0.a"
|
||||||
|
|
||||||
|
if ARGV.first
|
||||||
|
str = ARGV.first
|
||||||
|
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||||
|
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||||
|
version = $1
|
||||||
|
ARGV.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load Gem.activate_bin_path('sensu-plugins-cpu-checks', 'metrics-cpu-mpstat.rb', version)
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/opt/sensu/embedded/bin/ruby
|
||||||
|
#
|
||||||
|
# This file was generated by RubyGems.
|
||||||
|
#
|
||||||
|
# The application 'sensu-plugins-cpu-checks' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
version = ">= 0.a"
|
||||||
|
|
||||||
|
if ARGV.first
|
||||||
|
str = ARGV.first
|
||||||
|
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||||
|
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||||
|
version = $1
|
||||||
|
ARGV.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load Gem.activate_bin_path('sensu-plugins-cpu-checks', 'metrics-cpu-pcnt-usage.rb', version)
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/opt/sensu/embedded/bin/ruby
|
||||||
|
#
|
||||||
|
# This file was generated by RubyGems.
|
||||||
|
#
|
||||||
|
# The application 'sensu-plugins-cpu-checks' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
version = ">= 0.a"
|
||||||
|
|
||||||
|
if ARGV.first
|
||||||
|
str = ARGV.first
|
||||||
|
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||||
|
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||||
|
version = $1
|
||||||
|
ARGV.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load Gem.activate_bin_path('sensu-plugins-cpu-checks', 'metrics-cpu.rb', version)
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/opt/sensu/embedded/bin/ruby
|
||||||
|
#
|
||||||
|
# This file was generated by RubyGems.
|
||||||
|
#
|
||||||
|
# The application 'sensu-plugins-influxdb' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
version = ">= 0.a"
|
||||||
|
|
||||||
|
if ARGV.first
|
||||||
|
str = ARGV.first
|
||||||
|
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||||
|
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||||
|
version = $1
|
||||||
|
ARGV.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load Gem.activate_bin_path('sensu-plugins-influxdb', 'metrics-influxdb.rb', version)
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/opt/sensu/embedded/bin/ruby
|
||||||
|
#
|
||||||
|
# This file was generated by RubyGems.
|
||||||
|
#
|
||||||
|
# The application 'sensu-plugins-cpu-checks' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
version = ">= 0.a"
|
||||||
|
|
||||||
|
if ARGV.first
|
||||||
|
str = ARGV.first
|
||||||
|
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||||
|
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||||
|
version = $1
|
||||||
|
ARGV.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load Gem.activate_bin_path('sensu-plugins-cpu-checks', 'metrics-numastat.rb', version)
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/opt/sensu/embedded/bin/ruby
|
||||||
|
#
|
||||||
|
# This file was generated by RubyGems.
|
||||||
|
#
|
||||||
|
# The application 'sensu-plugins-cpu-checks' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
version = ">= 0.a"
|
||||||
|
|
||||||
|
if ARGV.first
|
||||||
|
str = ARGV.first
|
||||||
|
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||||
|
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||||
|
version = $1
|
||||||
|
ARGV.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load Gem.activate_bin_path('sensu-plugins-cpu-checks', 'metrics-user-pct-usage.rb', version)
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/opt/sensu/embedded/bin/ruby
|
||||||
|
#
|
||||||
|
# This file was generated by RubyGems.
|
||||||
|
#
|
||||||
|
# The application 'sensu-plugins-influxdb' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
version = ">= 0.a"
|
||||||
|
|
||||||
|
if ARGV.first
|
||||||
|
str = ARGV.first
|
||||||
|
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||||
|
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||||
|
version = $1
|
||||||
|
ARGV.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load Gem.activate_bin_path('sensu-plugins-influxdb', 'mutator-influxdb-line-protocol.rb', version)
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2013 Conrad Irwin <conrad@bugsnag.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,32 @@
|
||||||
|
This is a back-port of Ruby 2.1.0's [`Exception#cause`](http://www.ruby-doc.org/core-2.1.0/Exception.html#method-i-cause) to older versions of Ruby.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Add `gem 'cause'` to your `Gemfile`, then run `bundle install`. If you're not
|
||||||
|
using bundler, then just `gem install cause`.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Just continue programming as normal. When you rescue from exceptions they'll
|
||||||
|
have a third property, cause, in addition to backtrace and message. The cause
|
||||||
|
is the exception object that was being handled when the error was raised.
|
||||||
|
|
||||||
|
While this is not directly useful in normal programming, it's very useful for
|
||||||
|
debugging. Exception trackers like [Bugsnag](https://bugsnag.com/) can then pick up
|
||||||
|
the cause of the exception to help you find out what went wrong.
|
||||||
|
|
||||||
|
Limitations
|
||||||
|
-----------
|
||||||
|
|
||||||
|
At the moment you cannot set the cause yourself. Overriding `raise` is hairy
|
||||||
|
business and I wrote this gem late at night, but with sufficient care it's
|
||||||
|
probably doable.
|
||||||
|
|
||||||
|
Meta-fu
|
||||||
|
-------
|
||||||
|
|
||||||
|
This gem is Copyright under the MIT licence. See LICENCE.MIT for details.
|
||||||
|
|
||||||
|
Contributions and bug-reports are welcome.
|
|
@ -0,0 +1,17 @@
|
||||||
|
Gem::Specification.new do |gem|
|
||||||
|
gem.name = 'cause'
|
||||||
|
gem.version = '0.1'
|
||||||
|
|
||||||
|
gem.summary = 'A backport of Exception#cause from Ruby 2.1.0'
|
||||||
|
gem.description = "Allows you access to the error that was being handled when this exception was raised."
|
||||||
|
|
||||||
|
gem.authors = ['Conrad Irwin']
|
||||||
|
gem.email = %w(conrad@bugsnag.com)
|
||||||
|
gem.homepage = 'http://github.com/ConradIrwin/cause'
|
||||||
|
|
||||||
|
gem.license = 'MIT'
|
||||||
|
|
||||||
|
gem.required_ruby_version = '>= 1.8.7'
|
||||||
|
|
||||||
|
gem.files = `git ls-files`.split("\n")
|
||||||
|
end
|
|
@ -0,0 +1,12 @@
|
||||||
|
class Exception
|
||||||
|
unless method_defined?(:cause)
|
||||||
|
attr_reader :cause
|
||||||
|
|
||||||
|
alias old_initialize initialize
|
||||||
|
|
||||||
|
def initialize(*a)
|
||||||
|
@cause = $!
|
||||||
|
old_initialize(*a)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
11
deployment_scripts/puppet/files/embedded/lib/ruby/gems/2.3.0/gems/dentaku-2.0.9/.gitignore
vendored
Normal file
11
deployment_scripts/puppet/files/embedded/lib/ruby/gems/2.3.0/gems/dentaku-2.0.9/.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
*.gem
|
||||||
|
.bundle
|
||||||
|
.rbenv-version
|
||||||
|
Gemfile.lock
|
||||||
|
bin/*
|
||||||
|
pkg/*
|
||||||
|
vendor/*
|
||||||
|
|
||||||
|
/.ruby-gemset
|
||||||
|
/.ruby-version
|
||||||
|
/.rspec
|
|
@ -0,0 +1,2 @@
|
||||||
|
require "bundler"
|
||||||
|
Bundler.require
|
|
@ -0,0 +1,13 @@
|
||||||
|
language: ruby
|
||||||
|
sudo: false
|
||||||
|
rvm:
|
||||||
|
- 1.9.3
|
||||||
|
- 2.0.0
|
||||||
|
- 2.1.0
|
||||||
|
- 2.1.1
|
||||||
|
- 2.2.0
|
||||||
|
- 2.2.1
|
||||||
|
- 2.2.2
|
||||||
|
- 2.2.3
|
||||||
|
- 2.3.0
|
||||||
|
- rbx-2
|
|
@ -0,0 +1,141 @@
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
## [v2.0.9] 2016-09-19
|
||||||
|
- namespace tokenization errors
|
||||||
|
- automatically coerce arguments to string functions as strings
|
||||||
|
- selectively disable or clear AST cache
|
||||||
|
|
||||||
|
## [v2.0.8] 2016-05-10
|
||||||
|
- numeric input validations
|
||||||
|
- fail with gem-specific error for invalid arithmetic operands
|
||||||
|
- add `LEFT`, `RIGHT`, `MID`, `LEN`, `FIND`, `SUBSTITUTE`, and `CONCAT` string functions
|
||||||
|
|
||||||
|
## [v2.0.7] 2016-02-25
|
||||||
|
- fail with gem-specific error for parsing issues
|
||||||
|
- support NULL literals and nil variables
|
||||||
|
- keep reference to variable that caused failure when bulk-solving
|
||||||
|
|
||||||
|
## [v2.0.6] 2016-01-26
|
||||||
|
- support array parameters for external functions
|
||||||
|
- support case statements
|
||||||
|
- support precision for `ROUNDUP` and `ROUNDDOWN` functions
|
||||||
|
- prevent errors from corrupting calculator memory
|
||||||
|
|
||||||
|
## [v2.0.5] 2015-09-03
|
||||||
|
- fix bug with detecting unbound nodes
|
||||||
|
- silence warnings
|
||||||
|
- allow registration of custom token scanners
|
||||||
|
|
||||||
|
## [v2.0.4] 2015-09-03
|
||||||
|
- fix BigDecimal conversion bug
|
||||||
|
- add caching for bulk expression solving dependency order
|
||||||
|
- allow for custom configuration for token scanners
|
||||||
|
|
||||||
|
## [v2.0.3] 2015-08-25
|
||||||
|
- bug fixes
|
||||||
|
- performance enhancements
|
||||||
|
- code cleanup
|
||||||
|
|
||||||
|
## [v2.0.1] 2015-08-15
|
||||||
|
- add support for boolean literals
|
||||||
|
- implement basic parse-time type checking
|
||||||
|
|
||||||
|
## [v2.0.0] 2015-08-07
|
||||||
|
- shunting-yard parser for performance enhancement and AST generation
|
||||||
|
- AST caching for performance enhancement
|
||||||
|
- support comments in formulas
|
||||||
|
- support all functions from the Ruby Math module
|
||||||
|
|
||||||
|
## [v1.2.6] 2015-05-30
|
||||||
|
- support custom error handlers for systems of formulas
|
||||||
|
|
||||||
|
## [v1.2.5] 2015-05-23
|
||||||
|
- fix memory leak
|
||||||
|
|
||||||
|
## [v1.2.2] 2014-12-19
|
||||||
|
- performance enhancements
|
||||||
|
- unary minus bug fixes
|
||||||
|
- preserve provided hash keys for systems of formulas
|
||||||
|
|
||||||
|
## [v1.2.0] 2014-10-21
|
||||||
|
- add dependency resolution to automatically solve systems of formulas
|
||||||
|
|
||||||
|
## [v1.1.0] 2014-07-30
|
||||||
|
- add strict evaluation mode to raise `UnboundVariableError` if not all variable values are provided
|
||||||
|
- return division results as `BigDecimal` values
|
||||||
|
|
||||||
|
## [v1.0.0] 2014-03-06
|
||||||
|
- cleanup and 1.0 release
|
||||||
|
|
||||||
|
## [v0.2.14] 2014-01-24
|
||||||
|
- add modulo operator
|
||||||
|
- add unary percentage operator
|
||||||
|
- support registration of custom functions at runtime
|
||||||
|
|
||||||
|
## [v0.2.10] 2012-12-10
|
||||||
|
- return integer result for exact division, decimal otherwise
|
||||||
|
|
||||||
|
## [v0.2.9] 2012-10-17
|
||||||
|
- add `ROUNDUP` / `ROUNDDOWN` functions
|
||||||
|
|
||||||
|
## [v0.2.8] 2012-09-30
|
||||||
|
- make function name matching case-insensitive
|
||||||
|
|
||||||
|
## [v0.2.7] 2012-09-26
|
||||||
|
- support passing arbitrary expressions as function arguments
|
||||||
|
|
||||||
|
## [v0.2.6] 2012-09-19
|
||||||
|
- add `NOT` function
|
||||||
|
|
||||||
|
## [v0.2.5] 2012-06-20
|
||||||
|
- add exponent operator
|
||||||
|
- add support for digits in variable identifiers
|
||||||
|
|
||||||
|
## [v0.2.4] 2012-02-29
|
||||||
|
- add support for `min < x < max` syntax for inequality ranges
|
||||||
|
|
||||||
|
## [v0.2.2] 2012-02-22
|
||||||
|
- support `ROUND` to arbitrary decimal place on older Rubies
|
||||||
|
- ensure case is preserved for string values
|
||||||
|
|
||||||
|
## [v0.2.1] 2012-02-12
|
||||||
|
- add `ROUND` function
|
||||||
|
|
||||||
|
## [v0.1.3] 2012-01-31
|
||||||
|
- add support for string datatype
|
||||||
|
|
||||||
|
## [v0.1.1] 2012-01-24
|
||||||
|
- change from square bracket to parentheses for top-level evaluation
|
||||||
|
- add `IF` function
|
||||||
|
|
||||||
|
## [v0.1.0] 2012-01-20
|
||||||
|
- initial release
|
||||||
|
|
||||||
|
[v2.0.9]: https://github.com/rubysolo/dentaku/compare/v2.0.8...v2.0.9
|
||||||
|
[v2.0.8]: https://github.com/rubysolo/dentaku/compare/v2.0.7...v2.0.8
|
||||||
|
[v2.0.7]: https://github.com/rubysolo/dentaku/compare/v2.0.6...v2.0.7
|
||||||
|
[v2.0.6]: https://github.com/rubysolo/dentaku/compare/v2.0.5...v2.0.6
|
||||||
|
[v2.0.5]: https://github.com/rubysolo/dentaku/compare/v2.0.4...v2.0.5
|
||||||
|
[v2.0.4]: https://github.com/rubysolo/dentaku/compare/v2.0.3...v2.0.4
|
||||||
|
[v2.0.3]: https://github.com/rubysolo/dentaku/compare/v2.0.1...v2.0.3
|
||||||
|
[v2.0.1]: https://github.com/rubysolo/dentaku/compare/v2.0.0...v2.0.1
|
||||||
|
[v2.0.0]: https://github.com/rubysolo/dentaku/compare/v1.2.6...v2.0.0
|
||||||
|
[v1.2.6]: https://github.com/rubysolo/dentaku/compare/v1.2.5...v1.2.6
|
||||||
|
[v1.2.5]: https://github.com/rubysolo/dentaku/compare/v1.2.2...v1.2.5
|
||||||
|
[v1.2.2]: https://github.com/rubysolo/dentaku/compare/v1.2.0...v1.2.2
|
||||||
|
[v1.2.0]: https://github.com/rubysolo/dentaku/compare/v1.1.0...v1.2.0
|
||||||
|
[v1.1.0]: https://github.com/rubysolo/dentaku/compare/v1.0.0...v1.1.0
|
||||||
|
[v1.0.0]: https://github.com/rubysolo/dentaku/compare/v0.2.14...v1.0.0
|
||||||
|
[v0.2.14]: https://github.com/rubysolo/dentaku/compare/v0.2.10...v0.2.14
|
||||||
|
[v0.2.10]: https://github.com/rubysolo/dentaku/compare/v0.2.9...v0.2.10
|
||||||
|
[v0.2.9]: https://github.com/rubysolo/dentaku/compare/v0.2.8...v0.2.9
|
||||||
|
[v0.2.8]: https://github.com/rubysolo/dentaku/compare/v0.2.7...v0.2.8
|
||||||
|
[v0.2.7]: https://github.com/rubysolo/dentaku/compare/v0.2.6...v0.2.7
|
||||||
|
[v0.2.6]: https://github.com/rubysolo/dentaku/compare/v0.2.5...v0.2.6
|
||||||
|
[v0.2.5]: https://github.com/rubysolo/dentaku/compare/v0.2.4...v0.2.5
|
||||||
|
[v0.2.4]: https://github.com/rubysolo/dentaku/compare/v0.2.2...v0.2.4
|
||||||
|
[v0.2.2]: https://github.com/rubysolo/dentaku/compare/v0.2.1...v0.2.2
|
||||||
|
[v0.2.1]: https://github.com/rubysolo/dentaku/compare/v0.1.3...v0.2.1
|
||||||
|
[v0.1.3]: https://github.com/rubysolo/dentaku/compare/v0.1.1...v0.1.3
|
||||||
|
[v0.1.1]: https://github.com/rubysolo/dentaku/compare/v0.1.0...v0.1.1
|
||||||
|
[v0.1.0]: https://github.com/rubysolo/dentaku/commit/68724fd9c8fa637baf7b9d5515df0caa31e226bd
|
|
@ -0,0 +1,9 @@
|
||||||
|
source "http://rubygems.org"
|
||||||
|
|
||||||
|
# Specify your gem's dependencies in dentaku.gemspec
|
||||||
|
gemspec
|
||||||
|
|
||||||
|
if RUBY_VERSION.to_f >= 2.0 && RUBY_ENGINE == 'ruby'
|
||||||
|
gem 'pry-byebug'
|
||||||
|
gem 'pry-stack_explorer'
|
||||||
|
end
|
|
@ -0,0 +1,297 @@
|
||||||
|
Dentaku
|
||||||
|
=======
|
||||||
|
|
||||||
|
[![Join the chat at https://gitter.im/rubysolo/dentaku](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/rubysolo/dentaku?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
[![Gem Version](https://badge.fury.io/rb/dentaku.png)](http://badge.fury.io/rb/dentaku)
|
||||||
|
[![Build Status](https://travis-ci.org/rubysolo/dentaku.png?branch=master)](https://travis-ci.org/rubysolo/dentaku)
|
||||||
|
[![Code Climate](https://codeclimate.com/github/rubysolo/dentaku.png)](https://codeclimate.com/github/rubysolo/dentaku)
|
||||||
|
[![Hakiri](https://hakiri.io/github/rubysolo/dentaku/master.svg)](https://hakiri.io/github/rubysolo/dentaku)
|
||||||
|
|
||||||
|
DESCRIPTION
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Dentaku is a parser and evaluator for a mathematical and logical formula
|
||||||
|
language that allows run-time binding of values to variables referenced in the
|
||||||
|
formulas. It is intended to safely evaluate untrusted expressions without
|
||||||
|
opening security holes.
|
||||||
|
|
||||||
|
EXAMPLE
|
||||||
|
-------
|
||||||
|
|
||||||
|
This is probably simplest to illustrate in code:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
calculator = Dentaku::Calculator.new
|
||||||
|
calculator.evaluate('10 * 2')
|
||||||
|
#=> 20
|
||||||
|
```
|
||||||
|
|
||||||
|
Okay, not terribly exciting. But what if you want to have a reference to a
|
||||||
|
variable, and evaluate it at run-time? Here's how that would look:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
calculator.evaluate('kiwi + 5', kiwi: 2)
|
||||||
|
#=> 7
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also store the variable values in the calculator's memory and then
|
||||||
|
evaluate expressions against those stored values:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
calculator.store(peaches: 15)
|
||||||
|
calculator.evaluate('peaches - 5')
|
||||||
|
#=> 10
|
||||||
|
calculator.evaluate('peaches >= 15')
|
||||||
|
#=> true
|
||||||
|
```
|
||||||
|
|
||||||
|
For maximum CS geekery, `bind` is an alias of `store`.
|
||||||
|
|
||||||
|
Dentaku understands precedence order and using parentheses to group expressions
|
||||||
|
to ensure proper evaluation:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
calculator.evaluate('5 + 3 * 2')
|
||||||
|
#=> 11
|
||||||
|
calculator.evaluate('(5 + 3) * 2')
|
||||||
|
#=> 16
|
||||||
|
```
|
||||||
|
|
||||||
|
The `evaluate` method will return `nil` if there is an error in the formula.
|
||||||
|
If this is not the desired behavior, use `evaluate!`, which will raise an
|
||||||
|
exception.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
calculator.evaluate('10 * x')
|
||||||
|
#=> nil
|
||||||
|
calculator.evaluate!('10 * x')
|
||||||
|
Dentaku::UnboundVariableError: Dentaku::UnboundVariableError
|
||||||
|
```
|
||||||
|
|
||||||
|
Dentaku has built-in functions (including `if`, `not`, `min`, `max`, and
|
||||||
|
`round`) and the ability to define custom functions (see below). Functions
|
||||||
|
generally work like their counterparts in Excel:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
calculator.evaluate('if (pears < 10, 10, 20)', pears: 5)
|
||||||
|
#=> 10
|
||||||
|
calculator.evaluate('if (pears < 10, 10, 20)', pears: 15)
|
||||||
|
#=> 20
|
||||||
|
```
|
||||||
|
|
||||||
|
`round` can be called with or without the number of decimal places:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
calculator.evaluate('round(8.2)')
|
||||||
|
#=> 8
|
||||||
|
calculator.evaluate('round(8.2759, 2)')
|
||||||
|
#=> 8.28
|
||||||
|
```
|
||||||
|
|
||||||
|
`round` follows rounding rules, while `roundup` and `rounddown` are `ceil` and
|
||||||
|
`floor`, respectively.
|
||||||
|
|
||||||
|
If you're too lazy to be building calculator objects, there's a shortcut just
|
||||||
|
for you:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
Dentaku('plums * 1.5', plums: 2)
|
||||||
|
#=> 3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
PERFORMANCE
|
||||||
|
-----------
|
||||||
|
|
||||||
|
The flexibility and safety of Dentaku don't come without a price. Tokenizing a
|
||||||
|
string, parsing to an AST, and then evaluating that AST are about 2 orders of
|
||||||
|
magnitude slower than doing the same math in pure Ruby!
|
||||||
|
|
||||||
|
The good news is that most of the time is spent in the tokenization and parsing
|
||||||
|
phases, so if performance is a concern, you can enable AST caching:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
Dentaku.enable_ast_cache!
|
||||||
|
```
|
||||||
|
|
||||||
|
After this, Dentaku will cache the AST of each formula that it evaluates, so
|
||||||
|
subsequent evaluations (even with different values for variables) will be much
|
||||||
|
faster -- closer to 4x native Ruby speed. As usual, these benchmarks should be
|
||||||
|
considered rough estimates, and you should measure with representative formulas
|
||||||
|
from your application. Also, if new formulas are constantly introduced to your
|
||||||
|
application, AST caching will consume more memory with each new formula.
|
||||||
|
|
||||||
|
BUILT-IN OPERATORS AND FUNCTIONS
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
Math: `+`, `-`, `*`, `/`, `%`
|
||||||
|
|
||||||
|
Logic: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`, `AND`, `OR`
|
||||||
|
|
||||||
|
Functions: `IF`, `NOT`, `MIN`, `MAX`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
|
||||||
|
|
||||||
|
Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L292))
|
||||||
|
|
||||||
|
Math: all functions from Ruby's Math module, including `SIN`, `COS`, `TAN`, etc.
|
||||||
|
|
||||||
|
String: `LEFT`, `RIGHT`, `MID`, `LEN`, `FIND`, `SUBSTITUTE`, `CONCAT`
|
||||||
|
|
||||||
|
RESOLVING DEPENDENCIES
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
If your formulas rely on one another, they may need to be resolved in a
|
||||||
|
particular order. For example:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
calc = Dentaku::Calculator.new
|
||||||
|
calc.store(monthly_income: 50)
|
||||||
|
need_to_compute = {
|
||||||
|
income_taxes: "annual_income / 5",
|
||||||
|
annual_income: "monthly_income * 12"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the example, `annual_income` needs to be computed (and stored) before
|
||||||
|
`income_taxes`.
|
||||||
|
|
||||||
|
Dentaku provides two methods to help resolve formulas in order:
|
||||||
|
|
||||||
|
#### Calculator.dependencies
|
||||||
|
Pass a (string) expression to Dependencies and get back a list of variables (as
|
||||||
|
`:symbols`) that are required for the expression. `Dependencies` also takes
|
||||||
|
into account variables already (explicitly) stored into the calculator.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
calc.dependencies("monthly_income * 12")
|
||||||
|
#=> []
|
||||||
|
# (since monthly_income is in memory)
|
||||||
|
|
||||||
|
calc.dependencies("annual_income / 5")
|
||||||
|
#=> [:annual_income]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Calculator.solve! / Calculator.solve
|
||||||
|
Have Dentaku figure out the order in which your formulas need to be evaluated.
|
||||||
|
|
||||||
|
Pass in a hash of `{eventual_variable_name: "expression"}` to `solve!` and
|
||||||
|
have Dentaku resolve dependencies (using `TSort`) for you.
|
||||||
|
|
||||||
|
Raises `TSort::Cyclic` when a valid expression order cannot be found.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
calc = Dentaku::Calculator.new
|
||||||
|
calc.store(monthly_income: 50)
|
||||||
|
need_to_compute = {
|
||||||
|
income_taxes: "annual_income / 5",
|
||||||
|
annual_income: "monthly_income * 12"
|
||||||
|
}
|
||||||
|
calc.solve!(need_to_compute)
|
||||||
|
#=> {annual_income: 600, income_taxes: 120}
|
||||||
|
|
||||||
|
calc.solve!(
|
||||||
|
make_money: "have_money",
|
||||||
|
have_money: "make_money"
|
||||||
|
}
|
||||||
|
#=> raises TSort::Cyclic
|
||||||
|
```
|
||||||
|
|
||||||
|
`solve!` will also raise an exception if any of the formulas in the set cannot
|
||||||
|
be evaluated (e.g. raise `ZeroDivisionError`). The non-bang `solve` method will
|
||||||
|
find as many solutions as possible and return the symbol `:undefined` for the
|
||||||
|
problem formulas.
|
||||||
|
|
||||||
|
INLINE COMMENTS
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
If your expressions grow long or complex, you may add inline comments for future
|
||||||
|
reference. This is particularly useful if you save your expressions in a model.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
calculator.evaluate('kiwi + 5 /* This is a comment */', kiwi: 2)
|
||||||
|
#=> 7
|
||||||
|
```
|
||||||
|
|
||||||
|
Comments can be single or multi-line. The following are also valid.
|
||||||
|
|
||||||
|
```
|
||||||
|
/*
|
||||||
|
* This is a multi-line comment
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is another type of multi-line comment
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
EXTERNAL FUNCTIONS
|
||||||
|
------------------
|
||||||
|
|
||||||
|
I don't know everything, so I might not have implemented all the functions you
|
||||||
|
need. Please implement your favorites and send a pull request! Okay, so maybe
|
||||||
|
that's not feasible because:
|
||||||
|
|
||||||
|
1. You can't be bothered to share
|
||||||
|
1. You can't wait for me to respond to a pull request, you need it `NOW()`
|
||||||
|
1. The formula is the secret sauce for your startup
|
||||||
|
|
||||||
|
Whatever your reasons, Dentaku supports adding functions at runtime. To add a
|
||||||
|
function, you'll need to specify a name, a return type, and a lambda that
|
||||||
|
accepts all function arguments and returns the result value.
|
||||||
|
|
||||||
|
Here's an example of adding a function named `POW` that implements
|
||||||
|
exponentiation.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
> c = Dentaku::Calculator.new
|
||||||
|
> c.add_function(:pow, :numeric, ->(mantissa, exponent) { mantissa ** exponent })
|
||||||
|
> c.evaluate('POW(3,2)')
|
||||||
|
#=> 9
|
||||||
|
> c.evaluate('POW(2,3)')
|
||||||
|
#=> 8
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's an example of adding a variadic function:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
> c = Dentaku::Calculator.new
|
||||||
|
> c.add_function(:max, :numeric, ->(*args) { args.max })
|
||||||
|
> c.evaluate 'MAX(8,6,7,5,3,0,9)'
|
||||||
|
#=> 9
|
||||||
|
```
|
||||||
|
|
||||||
|
(However both of these are already built-in -- the `^` operator and the `MAX`
|
||||||
|
function)
|
||||||
|
|
||||||
|
Functions can be added individually using Calculator#add_function, or en masse
|
||||||
|
using Calculator#add_functions.
|
||||||
|
|
||||||
|
THANKS
|
||||||
|
------
|
||||||
|
|
||||||
|
Big thanks to [ElkStone Basements](http://www.elkstonebasements.com/) for
|
||||||
|
allowing me to extract and open source this code. Thanks also to all the
|
||||||
|
[contributors](https://github.com/rubysolo/dentaku/graphs/contributors)!
|
||||||
|
|
||||||
|
|
||||||
|
LICENSE
|
||||||
|
-------
|
||||||
|
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright © 2012-2016 Solomon White
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the ‘Software’), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,28 @@
|
||||||
|
require 'bundler/gem_tasks'
|
||||||
|
require 'rspec/core/rake_task'
|
||||||
|
|
||||||
|
desc "Run specs"
|
||||||
|
task :spec do
|
||||||
|
RSpec::Core::RakeTask.new(:spec) do |t|
|
||||||
|
t.rspec_opts = %w{--colour --format progress}
|
||||||
|
t.pattern = 'spec/**/*_spec.rb'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Default: run specs."
|
||||||
|
task default: :spec
|
||||||
|
|
||||||
|
task :console do
|
||||||
|
begin
|
||||||
|
require 'pry'
|
||||||
|
console = Pry
|
||||||
|
rescue LoadError
|
||||||
|
require 'irb'
|
||||||
|
require 'irb/completion'
|
||||||
|
console = IRB
|
||||||
|
end
|
||||||
|
|
||||||
|
require 'dentaku'
|
||||||
|
ARGV.clear
|
||||||
|
console.start
|
||||||
|
end
|
|
@ -0,0 +1,27 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
$:.push File.expand_path("../lib", __FILE__)
|
||||||
|
require "dentaku/version"
|
||||||
|
|
||||||
|
Gem::Specification.new do |s|
|
||||||
|
s.name = "dentaku"
|
||||||
|
s.version = Dentaku::VERSION
|
||||||
|
s.authors = ["Solomon White"]
|
||||||
|
s.email = ["rubysolo@gmail.com"]
|
||||||
|
s.homepage = "http://github.com/rubysolo/dentaku"
|
||||||
|
s.licenses = %w(MIT)
|
||||||
|
s.summary = %q{A formula language parser and evaluator}
|
||||||
|
s.description = <<-DESC
|
||||||
|
Dentaku is a parser and evaluator for mathematical formulas
|
||||||
|
DESC
|
||||||
|
|
||||||
|
s.rubyforge_project = "dentaku"
|
||||||
|
|
||||||
|
s.add_development_dependency('rake')
|
||||||
|
s.add_development_dependency('rspec')
|
||||||
|
s.add_development_dependency('pry')
|
||||||
|
|
||||||
|
s.files = `git ls-files`.split("\n")
|
||||||
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
||||||
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
||||||
|
s.require_paths = ["lib"]
|
||||||
|
end
|
|
@ -0,0 +1,43 @@
|
||||||
|
require "bigdecimal"
|
||||||
|
require "dentaku/calculator"
|
||||||
|
require "dentaku/version"
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
@enable_ast_caching = false
|
||||||
|
@enable_dependency_order_caching = false
|
||||||
|
|
||||||
|
def self.evaluate(expression, data={})
|
||||||
|
calculator.evaluate(expression, data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.enable_caching!
|
||||||
|
enable_ast_cache!
|
||||||
|
enable_dependency_order_cache!
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.enable_ast_cache!
|
||||||
|
@enable_ast_caching = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.cache_ast?
|
||||||
|
@enable_ast_caching
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.enable_dependency_order_cache!
|
||||||
|
@enable_dependency_order_caching = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.cache_dependency_order?
|
||||||
|
@enable_dependency_order_caching
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def self.calculator
|
||||||
|
@calculator ||= Dentaku::Calculator.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def Dentaku(expression, data={})
|
||||||
|
Dentaku.evaluate(expression, data)
|
||||||
|
end
|
|
@ -0,0 +1,21 @@
|
||||||
|
require_relative './ast/node'
|
||||||
|
require_relative './ast/nil'
|
||||||
|
require_relative './ast/numeric'
|
||||||
|
require_relative './ast/logical'
|
||||||
|
require_relative './ast/string'
|
||||||
|
require_relative './ast/identifier'
|
||||||
|
require_relative './ast/arithmetic'
|
||||||
|
require_relative './ast/negation'
|
||||||
|
require_relative './ast/comparators'
|
||||||
|
require_relative './ast/combinators'
|
||||||
|
require_relative './ast/grouping'
|
||||||
|
require_relative './ast/case'
|
||||||
|
require_relative './ast/functions/if'
|
||||||
|
require_relative './ast/functions/max'
|
||||||
|
require_relative './ast/functions/min'
|
||||||
|
require_relative './ast/functions/not'
|
||||||
|
require_relative './ast/functions/round'
|
||||||
|
require_relative './ast/functions/roundup'
|
||||||
|
require_relative './ast/functions/rounddown'
|
||||||
|
require_relative './ast/functions/ruby_math'
|
||||||
|
require_relative './ast/functions/string_functions'
|
|
@ -0,0 +1,129 @@
|
||||||
|
require_relative './operation'
|
||||||
|
require 'bigdecimal'
|
||||||
|
require 'bigdecimal/util'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Arithmetic < Operation
|
||||||
|
def initialize(*)
|
||||||
|
super
|
||||||
|
unless valid_node?(left) && valid_node?(right)
|
||||||
|
fail ParseError, "#{ self.class } requires numeric operands"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
:numeric
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
l = cast(left.value(context))
|
||||||
|
r = cast(right.value(context))
|
||||||
|
l.public_send(operator, r)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def cast(value, prefer_integer=true)
|
||||||
|
validate_numeric(value)
|
||||||
|
v = BigDecimal.new(value, Float::DIG+1)
|
||||||
|
v = v.to_i if prefer_integer && v.frac.zero?
|
||||||
|
v
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_node?(node)
|
||||||
|
node && (node.dependencies.any? || node.type == :numeric)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_numeric(value)
|
||||||
|
Float(value)
|
||||||
|
rescue ::ArgumentError, ::TypeError
|
||||||
|
fail Dentaku::ArgumentError, "#{ self.class } requires numeric operands"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Addition < Arithmetic
|
||||||
|
def operator
|
||||||
|
:+
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.precedence
|
||||||
|
10
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Subtraction < Arithmetic
|
||||||
|
def operator
|
||||||
|
:-
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.precedence
|
||||||
|
10
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Multiplication < Arithmetic
|
||||||
|
def operator
|
||||||
|
:*
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.precedence
|
||||||
|
20
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Division < Arithmetic
|
||||||
|
def value(context={})
|
||||||
|
r = cast(right.value(context), false)
|
||||||
|
raise Dentaku::ZeroDivisionError if r.zero?
|
||||||
|
|
||||||
|
cast(cast(left.value(context)) / r)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.precedence
|
||||||
|
20
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Modulo < Arithmetic
|
||||||
|
def initialize(left, right)
|
||||||
|
@left = left
|
||||||
|
@right = right
|
||||||
|
|
||||||
|
unless (valid_node?(left) || left.nil?) && valid_node?(right)
|
||||||
|
fail ParseError, "#{ self.class } requires numeric operands"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def percent?
|
||||||
|
left.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
if percent?
|
||||||
|
cast(right.value(context)) * 0.01
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def operator
|
||||||
|
:%
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.precedence
|
||||||
|
20
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Exponentiation < Arithmetic
|
||||||
|
def operator
|
||||||
|
:**
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.precedence
|
||||||
|
30
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,52 @@
|
||||||
|
require_relative './case/case_conditional'
|
||||||
|
require_relative './case/case_when'
|
||||||
|
require_relative './case/case_then'
|
||||||
|
require_relative './case/case_switch_variable'
|
||||||
|
require_relative './case/case_else'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Case < Node
|
||||||
|
def initialize(*nodes)
|
||||||
|
@switch = nodes.shift
|
||||||
|
|
||||||
|
unless @switch.is_a?(AST::CaseSwitchVariable)
|
||||||
|
raise 'Case missing switch variable'
|
||||||
|
end
|
||||||
|
|
||||||
|
@conditions = nodes
|
||||||
|
|
||||||
|
@else = @conditions.pop if @conditions.last.is_a?(AST::CaseElse)
|
||||||
|
|
||||||
|
@conditions.each do |condition|
|
||||||
|
unless condition.is_a?(AST::CaseConditional)
|
||||||
|
raise "#{condition} is not a CaseConditional"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
switch_value = @switch.value(context)
|
||||||
|
@conditions.each do |condition|
|
||||||
|
if condition.when.value(context) == switch_value
|
||||||
|
return condition.then.value(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if @else
|
||||||
|
return @else.value(context)
|
||||||
|
else
|
||||||
|
raise "No block matched the switch value '#{switch_value}'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
# TODO: should short-circuit
|
||||||
|
@switch.dependencies(context) +
|
||||||
|
@conditions.flat_map do |condition|
|
||||||
|
condition.dependencies(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,23 @@
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class CaseConditional < Node
|
||||||
|
attr_reader :when,
|
||||||
|
:then
|
||||||
|
|
||||||
|
def initialize(when_statement, then_statement)
|
||||||
|
@when = when_statement
|
||||||
|
unless @when.is_a?(AST::CaseWhen)
|
||||||
|
raise 'Expected first argument to be a CaseWhen'
|
||||||
|
end
|
||||||
|
@then = then_statement
|
||||||
|
unless @then.is_a?(AST::CaseThen)
|
||||||
|
raise 'Expected second argument to be a CaseThen'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
@when.dependencies(context) + @then.dependencies(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,21 @@
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class CaseElse < Node
|
||||||
|
def self.arity
|
||||||
|
1
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(node)
|
||||||
|
@node = node
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
@node.value(context)
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
@node.dependencies(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,21 @@
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class CaseSwitchVariable < Node
|
||||||
|
def initialize(node)
|
||||||
|
@node = node
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
@node.value(context)
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
@node.dependencies(context)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.arity
|
||||||
|
1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,21 @@
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class CaseThen < Node
|
||||||
|
def self.arity
|
||||||
|
1
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(node)
|
||||||
|
@node = node
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
@node.value(context)
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
@node.dependencies(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,21 @@
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class CaseWhen < Operation
|
||||||
|
def self.arity
|
||||||
|
1
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(node)
|
||||||
|
@node = node
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
@node.value(context)
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
@node.dependencies(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,36 @@
|
||||||
|
require_relative './operation'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Combinator < Operation
|
||||||
|
def initialize(*)
|
||||||
|
super
|
||||||
|
unless valid_node?(left) && valid_node?(right)
|
||||||
|
fail ParseError, "#{ self.class } requires logical operands"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
:logical
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def valid_node?(node)
|
||||||
|
node.dependencies.any? || node.type == :logical
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class And < Combinator
|
||||||
|
def value(context={})
|
||||||
|
left.value(context) && right.value(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Or < Combinator
|
||||||
|
def value(context={})
|
||||||
|
left.value(context) || right.value(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,51 @@
|
||||||
|
require_relative './operation'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Comparator < Operation
|
||||||
|
def self.precedence
|
||||||
|
5
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
:logical
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class LessThan < Comparator
|
||||||
|
def value(context={})
|
||||||
|
left.value(context) < right.value(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class LessThanOrEqual < Comparator
|
||||||
|
def value(context={})
|
||||||
|
left.value(context) <= right.value(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class GreaterThan < Comparator
|
||||||
|
def value(context={})
|
||||||
|
left.value(context) > right.value(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class GreaterThanOrEqual < Comparator
|
||||||
|
def value(context={})
|
||||||
|
left.value(context) >= right.value(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class NotEqual < Comparator
|
||||||
|
def value(context={})
|
||||||
|
left.value(context) != right.value(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Equal < Comparator
|
||||||
|
def value(context={})
|
||||||
|
left.value(context) == right.value(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,73 @@
|
||||||
|
require_relative 'node'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Function < Node
|
||||||
|
def initialize(*args)
|
||||||
|
@args = args
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
@args.flat_map { |a| a.dependencies(context) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get(name)
|
||||||
|
registry.fetch(function_name(name)) {
|
||||||
|
fail ParseError, "Undefined function #{ name }"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.register(name, type, implementation)
|
||||||
|
function = Class.new(self) do
|
||||||
|
def self.implementation=(impl)
|
||||||
|
@implementation = impl
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.implementation
|
||||||
|
@implementation
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.type=(type)
|
||||||
|
@type = type
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.type
|
||||||
|
@type
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
args = @args.map { |a| a.value(context) }
|
||||||
|
self.class.implementation.call(*args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
self.class.type
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function_class = name.to_s.capitalize
|
||||||
|
Dentaku::AST.send(:remove_const, function_class) if Dentaku::AST.const_defined?(function_class)
|
||||||
|
Dentaku::AST.const_set(function_class, function)
|
||||||
|
|
||||||
|
function.implementation = implementation
|
||||||
|
function.type = type
|
||||||
|
|
||||||
|
registry[function_name(name)] = function
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.register_class(name, function_class)
|
||||||
|
registry[function_name(name)] = function_class
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def self.function_name(name)
|
||||||
|
name.to_s.downcase
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.registry
|
||||||
|
@registry ||= {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,30 @@
|
||||||
|
require_relative '../function'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class If < Function
|
||||||
|
attr_reader :predicate, :left, :right
|
||||||
|
|
||||||
|
def initialize(predicate, left, right)
|
||||||
|
@predicate = predicate
|
||||||
|
@left = left
|
||||||
|
@right = right
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
predicate.value(context) ? left.value(context) : right.value(context)
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
left.type
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
# TODO : short-circuit?
|
||||||
|
(predicate.dependencies(context) + left.dependencies(context) + right.dependencies(context)).uniq
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Dentaku::AST::Function.register_class(:if, Dentaku::AST::If)
|
|
@ -0,0 +1,5 @@
|
||||||
|
require_relative '../function'
|
||||||
|
|
||||||
|
Dentaku::AST::Function.register(:max, :numeric, ->(*args) {
|
||||||
|
args.max
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
require_relative '../function'
|
||||||
|
|
||||||
|
Dentaku::AST::Function.register(:min, :numeric, ->(*args) {
|
||||||
|
args.min
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
require_relative '../function'
|
||||||
|
|
||||||
|
Dentaku::AST::Function.register(:not, :logical, ->(logical) {
|
||||||
|
! logical
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
require_relative '../function'
|
||||||
|
|
||||||
|
Dentaku::AST::Function.register(:round, :numeric, ->(numeric, places=nil) {
|
||||||
|
numeric.round(places || 0)
|
||||||
|
})
|
|
@ -0,0 +1,7 @@
|
||||||
|
require_relative '../function'
|
||||||
|
|
||||||
|
Dentaku::AST::Function.register(:rounddown, :numeric, ->(numeric, precision=0) {
|
||||||
|
tens = 10.0**precision
|
||||||
|
result = (numeric * tens).floor / tens
|
||||||
|
precision <= 0 ? result.to_i : result
|
||||||
|
})
|
|
@ -0,0 +1,7 @@
|
||||||
|
require_relative '../function'
|
||||||
|
|
||||||
|
Dentaku::AST::Function.register(:roundup, :numeric, ->(numeric, precision=0) {
|
||||||
|
tens = 10.0**precision
|
||||||
|
result = (numeric * tens).ceil / tens
|
||||||
|
precision <= 0 ? result.to_i : result
|
||||||
|
})
|
|
@ -0,0 +1,8 @@
|
||||||
|
# import all functions from Ruby's Math module
|
||||||
|
require_relative "../function"
|
||||||
|
|
||||||
|
Math.methods(false).each do |method|
|
||||||
|
Dentaku::AST::Function.register(method, :numeric, ->(*args) {
|
||||||
|
Math.send(method, *args)
|
||||||
|
})
|
||||||
|
end
|
|
@ -0,0 +1,111 @@
|
||||||
|
require_relative '../function'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
module StringFunctions
|
||||||
|
class Left < Function
|
||||||
|
def initialize(string, length)
|
||||||
|
@string = string
|
||||||
|
@length = length
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
string = @string.value(context).to_s
|
||||||
|
length = @length.value(context)
|
||||||
|
string[0, length]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Right < Function
|
||||||
|
def initialize(string, length)
|
||||||
|
@string = string
|
||||||
|
@length = length
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
string = @string.value(context).to_s
|
||||||
|
length = @length.value(context)
|
||||||
|
string[length * -1, length] || string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Mid < Function
|
||||||
|
def initialize(string, offset, length)
|
||||||
|
@string = string
|
||||||
|
@offset = offset
|
||||||
|
@length = length
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
string = @string.value(context).to_s
|
||||||
|
offset = @offset.value(context)
|
||||||
|
length = @length.value(context)
|
||||||
|
string[offset - 1, length].to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Len < Function
|
||||||
|
def initialize(string)
|
||||||
|
@string = string
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
string = @string.value(context).to_s
|
||||||
|
string.length
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Find < Function
|
||||||
|
def initialize(needle, haystack)
|
||||||
|
@needle = needle
|
||||||
|
@haystack = haystack
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
needle = @needle.value(context)
|
||||||
|
needle = needle.to_s unless needle.is_a?(Regexp)
|
||||||
|
haystack = @haystack.value(context).to_s
|
||||||
|
pos = haystack.index(needle)
|
||||||
|
pos && pos + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Substitute < Function
|
||||||
|
def initialize(original, search, replacement)
|
||||||
|
@original = original
|
||||||
|
@search = search
|
||||||
|
@replacement = replacement
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
original = @original.value(context).to_s
|
||||||
|
search = @search.value(context)
|
||||||
|
search = search.to_s unless search.is_a?(Regexp)
|
||||||
|
replacement = @replacement.value(context).to_s
|
||||||
|
original.sub(search, replacement)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Concat < Function
|
||||||
|
def initialize(left, right)
|
||||||
|
@left = left
|
||||||
|
@right = right
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
left = @left.value(context).to_s
|
||||||
|
right = @right.value(context).to_s
|
||||||
|
left + right
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Dentaku::AST::Function.register_class(:left, Dentaku::AST::StringFunctions::Left)
|
||||||
|
Dentaku::AST::Function.register_class(:right, Dentaku::AST::StringFunctions::Right)
|
||||||
|
Dentaku::AST::Function.register_class(:mid, Dentaku::AST::StringFunctions::Mid)
|
||||||
|
Dentaku::AST::Function.register_class(:len, Dentaku::AST::StringFunctions::Len)
|
||||||
|
Dentaku::AST::Function.register_class(:find, Dentaku::AST::StringFunctions::Find)
|
||||||
|
Dentaku::AST::Function.register_class(:substitute, Dentaku::AST::StringFunctions::Substitute)
|
||||||
|
Dentaku::AST::Function.register_class(:concat, Dentaku::AST::StringFunctions::Concat)
|
|
@ -0,0 +1,21 @@
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Grouping
|
||||||
|
def initialize(node)
|
||||||
|
@node = node
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
@node.value(context)
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
@node.type
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
@node.dependencies(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,36 @@
|
||||||
|
require_relative '../exceptions'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Identifier < Node
|
||||||
|
attr_reader :identifier
|
||||||
|
|
||||||
|
def initialize(token)
|
||||||
|
@identifier = token.value.downcase
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
v = context.fetch(identifier) do
|
||||||
|
raise UnboundVariableError.new([identifier])
|
||||||
|
end
|
||||||
|
|
||||||
|
case v
|
||||||
|
when Node
|
||||||
|
v.value(context)
|
||||||
|
else
|
||||||
|
v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
context.has_key?(identifier) ? dependencies_of(context[identifier]) : [identifier]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def dependencies_of(node)
|
||||||
|
node.respond_to?(:dependencies) ? node.dependencies : []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,20 @@
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Literal < Node
|
||||||
|
attr_reader :type
|
||||||
|
|
||||||
|
def initialize(token)
|
||||||
|
@value = token.value
|
||||||
|
@type = token.category
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(*)
|
||||||
|
@value
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(*)
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
require_relative "./literal"
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Logical < Literal
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,40 @@
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Negation < Operation
|
||||||
|
def initialize(node)
|
||||||
|
@node = node
|
||||||
|
fail ParseError, "Negation requires numeric operand" unless valid_node?(node)
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(context={})
|
||||||
|
@node.value(context) * -1
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
:numeric
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.arity
|
||||||
|
1
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.right_associative?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.precedence
|
||||||
|
40
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
@node.dependencies(context)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def valid_node?(node)
|
||||||
|
node && (node.dependencies.any? || node.type == :numeric)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Nil < Node
|
||||||
|
def value(*)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Node
|
||||||
|
def self.precedence
|
||||||
|
0
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.arity
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
require_relative "./literal"
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Numeric < Literal
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,22 @@
|
||||||
|
require_relative './node'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class Operation < Node
|
||||||
|
attr_reader :left, :right
|
||||||
|
|
||||||
|
def initialize(left, right)
|
||||||
|
@left = left
|
||||||
|
@right = right
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(context={})
|
||||||
|
(left.dependencies(context) + right.dependencies(context)).uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.right_associative?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
require_relative "./literal"
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
module AST
|
||||||
|
class String < Literal
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,99 @@
|
||||||
|
require 'dentaku/calculator'
|
||||||
|
require 'dentaku/dependency_resolver'
|
||||||
|
require 'dentaku/exceptions'
|
||||||
|
require 'dentaku/parser'
|
||||||
|
require 'dentaku/tokenizer'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
class BulkExpressionSolver
|
||||||
|
def initialize(expression_hash, calculator)
|
||||||
|
self.expression_hash = expression_hash
|
||||||
|
self.calculator = calculator
|
||||||
|
end
|
||||||
|
|
||||||
|
def solve!
|
||||||
|
solve(&raise_exception_handler)
|
||||||
|
end
|
||||||
|
|
||||||
|
def solve(&block)
|
||||||
|
error_handler = block || return_undefined_handler
|
||||||
|
results = load_results(&error_handler)
|
||||||
|
|
||||||
|
expression_hash.each_with_object({}) do |(k, _), r|
|
||||||
|
r[k] = results[k.to_s]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def self.dependency_cache
|
||||||
|
@dep_cache ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_accessor :expression_hash, :calculator
|
||||||
|
|
||||||
|
def return_undefined_handler
|
||||||
|
->(*) { :undefined }
|
||||||
|
end
|
||||||
|
|
||||||
|
def raise_exception_handler
|
||||||
|
->(ex) { raise ex }
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_results(&block)
|
||||||
|
variables_in_resolve_order.each_with_object({}) do |var_name, r|
|
||||||
|
begin
|
||||||
|
value_from_memory = calculator.memory[var_name]
|
||||||
|
|
||||||
|
if value_from_memory.nil? &&
|
||||||
|
expressions[var_name].nil? &&
|
||||||
|
!calculator.memory.has_key?(var_name)
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
value = value_from_memory ||
|
||||||
|
evaluate!(expressions[var_name], expressions.merge(r))
|
||||||
|
|
||||||
|
r[var_name] = value
|
||||||
|
rescue Dentaku::UnboundVariableError, ZeroDivisionError => ex
|
||||||
|
ex.recipient_variable = var_name
|
||||||
|
r[var_name] = block.call(ex)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def expressions
|
||||||
|
@expressions ||= Hash[expression_hash.map { |k,v| [k.to_s, v] }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def expression_dependencies
|
||||||
|
Hash[expressions.map { |var, expr| [var, calculator.dependencies(expr)] }].tap do |d|
|
||||||
|
d.values.each do |deps|
|
||||||
|
unresolved = deps.reject { |ud| d.has_key?(ud) }
|
||||||
|
unresolved.each { |u| add_dependencies(d, u) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_dependencies(current_dependencies, variable)
|
||||||
|
node = calculator.memory[variable]
|
||||||
|
if node.respond_to?(:dependencies)
|
||||||
|
current_dependencies[variable] = node.dependencies
|
||||||
|
node.dependencies.each { |d| add_dependencies(current_dependencies, d) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def variables_in_resolve_order
|
||||||
|
cache_key = expressions.keys.map(&:to_s).sort.join("|")
|
||||||
|
@ordered_deps ||= self.class.dependency_cache.fetch(cache_key) {
|
||||||
|
DependencyResolver.find_resolve_order(expression_dependencies).tap do |d|
|
||||||
|
self.class.dependency_cache[cache_key] = d if Dentaku.cache_dependency_order?
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def evaluate!(expression, results)
|
||||||
|
calculator.evaluate!(expression, results)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,124 @@
|
||||||
|
require 'dentaku/bulk_expression_solver'
|
||||||
|
require 'dentaku/exceptions'
|
||||||
|
require 'dentaku/token'
|
||||||
|
require 'dentaku/dependency_resolver'
|
||||||
|
require 'dentaku/parser'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
class Calculator
|
||||||
|
attr_reader :result, :memory, :tokenizer
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
clear
|
||||||
|
@tokenizer = Tokenizer.new
|
||||||
|
@ast_cache = {}
|
||||||
|
@disable_ast_cache = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_function(name, type, body)
|
||||||
|
Dentaku::AST::Function.register(name, type, body)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_functions(fns)
|
||||||
|
fns.each { |(name, type, body)| add_function(name, type, body) }
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable_cache
|
||||||
|
@disable_ast_cache = true
|
||||||
|
yield(self) if block_given?
|
||||||
|
ensure
|
||||||
|
@disable_ast_cache = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def evaluate(expression, data={})
|
||||||
|
evaluate!(expression, data)
|
||||||
|
rescue UnboundVariableError, ArgumentError
|
||||||
|
yield expression if block_given?
|
||||||
|
end
|
||||||
|
|
||||||
|
def evaluate!(expression, data={})
|
||||||
|
store(data) do
|
||||||
|
node = expression
|
||||||
|
node = ast(node) unless node.is_a?(AST::Node)
|
||||||
|
node.value(memory)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def solve!(expression_hash)
|
||||||
|
BulkExpressionSolver.new(expression_hash, self).solve!
|
||||||
|
end
|
||||||
|
|
||||||
|
def solve(expression_hash, &block)
|
||||||
|
BulkExpressionSolver.new(expression_hash, self).solve(&block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def dependencies(expression)
|
||||||
|
ast(expression).dependencies(memory)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ast(expression)
|
||||||
|
@ast_cache.fetch(expression) {
|
||||||
|
Parser.new(tokenizer.tokenize(expression)).parse.tap do |node|
|
||||||
|
@ast_cache[expression] = node if cache_ast?
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_cache(pattern=:all)
|
||||||
|
case pattern
|
||||||
|
when :all
|
||||||
|
@ast_cache = {}
|
||||||
|
when String
|
||||||
|
@ast_cache.delete(pattern)
|
||||||
|
when Regexp
|
||||||
|
@ast_cache.delete_if { |k,_| k =~ pattern }
|
||||||
|
else
|
||||||
|
fail Dentaku::ArgumentError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def store(key_or_hash, value=nil)
|
||||||
|
restore = Hash[memory]
|
||||||
|
|
||||||
|
if value.nil?
|
||||||
|
key_or_hash.each do |key, val|
|
||||||
|
memory[key.to_s.downcase] = val
|
||||||
|
end
|
||||||
|
else
|
||||||
|
memory[key_or_hash.to_s.downcase] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
if block_given?
|
||||||
|
begin
|
||||||
|
result = yield
|
||||||
|
@memory = restore
|
||||||
|
return result
|
||||||
|
rescue => e
|
||||||
|
@memory = restore
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self
|
||||||
|
end
|
||||||
|
alias_method :bind, :store
|
||||||
|
|
||||||
|
def store_formula(key, formula)
|
||||||
|
store(key, ast(formula))
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear
|
||||||
|
@memory = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def empty?
|
||||||
|
memory.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_ast?
|
||||||
|
Dentaku.cache_ast? && !@disable_ast_cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
require 'tsort'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
class DependencyResolver
|
||||||
|
include TSort
|
||||||
|
|
||||||
|
def self.find_resolve_order(vars_to_dependencies_hash)
|
||||||
|
self.new(vars_to_dependencies_hash).tsort
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(vars_to_dependencies_hash)
|
||||||
|
# ensure variables are strings
|
||||||
|
@vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.to_s, v]}]
|
||||||
|
end
|
||||||
|
|
||||||
|
def tsort_each_node(&block)
|
||||||
|
@vars_to_deps.each_key(&block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def tsort_each_child(node, &block)
|
||||||
|
@vars_to_deps.fetch(node.to_s, []).each(&block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,25 @@
|
||||||
|
module Dentaku
|
||||||
|
class UnboundVariableError < StandardError
|
||||||
|
attr_accessor :recipient_variable
|
||||||
|
|
||||||
|
attr_reader :unbound_variables
|
||||||
|
|
||||||
|
def initialize(unbound_variables)
|
||||||
|
@unbound_variables = unbound_variables
|
||||||
|
super("no value provided for variables: #{ unbound_variables.join(', ') }")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ParseError < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
|
class TokenizerError < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
|
class ArgumentError < ::ArgumentError
|
||||||
|
end
|
||||||
|
|
||||||
|
class ZeroDivisionError < ::ZeroDivisionError
|
||||||
|
attr_accessor :recipient_variable
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,222 @@
|
||||||
|
require_relative './ast'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
class Parser
|
||||||
|
attr_reader :input, :output, :operations, :arities
|
||||||
|
|
||||||
|
def initialize(tokens, options={})
|
||||||
|
@input = tokens.dup
|
||||||
|
@output = []
|
||||||
|
@operations = options.fetch(:operations, [])
|
||||||
|
@arities = options.fetch(:arities, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_args(count)
|
||||||
|
Array.new(count) { output.pop }.reverse
|
||||||
|
end
|
||||||
|
|
||||||
|
def consume(count=2)
|
||||||
|
operator = operations.pop
|
||||||
|
output.push operator.new(*get_args(operator.arity || count))
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse
|
||||||
|
return AST::Nil.new if input.empty?
|
||||||
|
|
||||||
|
while token = input.shift
|
||||||
|
case token.category
|
||||||
|
when :numeric
|
||||||
|
output.push AST::Numeric.new(token)
|
||||||
|
|
||||||
|
when :logical
|
||||||
|
output.push AST::Logical.new(token)
|
||||||
|
|
||||||
|
when :string
|
||||||
|
output.push AST::String.new(token)
|
||||||
|
|
||||||
|
when :identifier
|
||||||
|
output.push AST::Identifier.new(token)
|
||||||
|
|
||||||
|
when :operator, :comparator, :combinator
|
||||||
|
op_class = operation(token)
|
||||||
|
|
||||||
|
if op_class.right_associative?
|
||||||
|
while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
|
||||||
|
consume
|
||||||
|
end
|
||||||
|
|
||||||
|
operations.push op_class
|
||||||
|
else
|
||||||
|
while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
|
||||||
|
consume
|
||||||
|
end
|
||||||
|
|
||||||
|
operations.push op_class
|
||||||
|
end
|
||||||
|
|
||||||
|
when :null
|
||||||
|
output.push AST::Nil.new
|
||||||
|
|
||||||
|
when :function
|
||||||
|
arities.push 0
|
||||||
|
operations.push function(token)
|
||||||
|
|
||||||
|
when :case
|
||||||
|
case token.value
|
||||||
|
when :open
|
||||||
|
# special handling for case nesting: strip out inner case
|
||||||
|
# statements and parse their AST segments recursively
|
||||||
|
if operations.include?(AST::Case)
|
||||||
|
last_case_close_index = nil
|
||||||
|
first_nested_case_close_index = nil
|
||||||
|
input.each_with_index do |token, index|
|
||||||
|
first_nested_case_close_index = last_case_close_index
|
||||||
|
if token.category == :case && token.value == :close
|
||||||
|
last_case_close_index = index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
inner_case_inputs = input.slice!(0..first_nested_case_close_index)
|
||||||
|
subparser = Parser.new(
|
||||||
|
inner_case_inputs,
|
||||||
|
operations: [AST::Case],
|
||||||
|
arities: [0]
|
||||||
|
)
|
||||||
|
subparser.parse
|
||||||
|
output.concat(subparser.output)
|
||||||
|
else
|
||||||
|
operations.push AST::Case
|
||||||
|
arities.push(0)
|
||||||
|
end
|
||||||
|
when :close
|
||||||
|
if operations[1] == AST::CaseThen
|
||||||
|
while operations.last != AST::Case
|
||||||
|
consume
|
||||||
|
end
|
||||||
|
|
||||||
|
operations.push(AST::CaseConditional)
|
||||||
|
consume(2)
|
||||||
|
arities[-1] += 1
|
||||||
|
elsif operations[1] == AST::CaseElse
|
||||||
|
while operations.last != AST::Case
|
||||||
|
consume
|
||||||
|
end
|
||||||
|
|
||||||
|
arities[-1] += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
unless operations.count == 1 && operations.last == AST::Case
|
||||||
|
fail ParseError, "Unprocessed token #{ token.value }"
|
||||||
|
end
|
||||||
|
consume(arities.pop.succ)
|
||||||
|
when :when
|
||||||
|
if operations[1] == AST::CaseThen
|
||||||
|
while ![AST::CaseWhen, AST::Case].include?(operations.last)
|
||||||
|
consume
|
||||||
|
end
|
||||||
|
operations.push(AST::CaseConditional)
|
||||||
|
consume(2)
|
||||||
|
arities[-1] += 1
|
||||||
|
elsif operations.last == AST::Case
|
||||||
|
operations.push(AST::CaseSwitchVariable)
|
||||||
|
consume
|
||||||
|
end
|
||||||
|
|
||||||
|
operations.push(AST::CaseWhen)
|
||||||
|
when :then
|
||||||
|
if operations[1] == AST::CaseWhen
|
||||||
|
while ![AST::CaseThen, AST::Case].include?(operations.last)
|
||||||
|
consume
|
||||||
|
end
|
||||||
|
end
|
||||||
|
operations.push(AST::CaseThen)
|
||||||
|
when :else
|
||||||
|
if operations[1] == AST::CaseThen
|
||||||
|
while operations.last != AST::Case
|
||||||
|
consume
|
||||||
|
end
|
||||||
|
|
||||||
|
operations.push(AST::CaseConditional)
|
||||||
|
consume(2)
|
||||||
|
arities[-1] += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
operations.push(AST::CaseElse)
|
||||||
|
else
|
||||||
|
fail ParseError, "Unknown case token #{ token.value }"
|
||||||
|
end
|
||||||
|
|
||||||
|
when :grouping
|
||||||
|
case token.value
|
||||||
|
when :open
|
||||||
|
if input.first && input.first.value == :close
|
||||||
|
input.shift
|
||||||
|
consume(0)
|
||||||
|
else
|
||||||
|
operations.push AST::Grouping
|
||||||
|
end
|
||||||
|
|
||||||
|
when :close
|
||||||
|
while operations.any? && operations.last != AST::Grouping
|
||||||
|
consume
|
||||||
|
end
|
||||||
|
|
||||||
|
lparen = operations.pop
|
||||||
|
fail ParseError, "Unbalanced parenthesis" unless lparen == AST::Grouping
|
||||||
|
|
||||||
|
if operations.last && operations.last < AST::Function
|
||||||
|
consume(arities.pop.succ)
|
||||||
|
end
|
||||||
|
|
||||||
|
when :comma
|
||||||
|
arities[-1] += 1
|
||||||
|
while operations.any? && operations.last != AST::Grouping
|
||||||
|
consume
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
fail ParseError, "Unknown grouping token #{ token.value }"
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
fail ParseError, "Not implemented for tokens of category #{ token.category }"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
while operations.any?
|
||||||
|
consume
|
||||||
|
end
|
||||||
|
|
||||||
|
unless output.count == 1
|
||||||
|
fail ParseError, "Invalid statement"
|
||||||
|
end
|
||||||
|
|
||||||
|
output.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def operation(token)
|
||||||
|
{
|
||||||
|
add: AST::Addition,
|
||||||
|
subtract: AST::Subtraction,
|
||||||
|
multiply: AST::Multiplication,
|
||||||
|
divide: AST::Division,
|
||||||
|
pow: AST::Exponentiation,
|
||||||
|
negate: AST::Negation,
|
||||||
|
mod: AST::Modulo,
|
||||||
|
|
||||||
|
lt: AST::LessThan,
|
||||||
|
gt: AST::GreaterThan,
|
||||||
|
le: AST::LessThanOrEqual,
|
||||||
|
ge: AST::GreaterThanOrEqual,
|
||||||
|
ne: AST::NotEqual,
|
||||||
|
eq: AST::Equal,
|
||||||
|
|
||||||
|
and: AST::And,
|
||||||
|
or: AST::Or,
|
||||||
|
}.fetch(token.value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def function(token)
|
||||||
|
Dentaku::AST::Function.get(token.value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,32 @@
|
||||||
|
module Dentaku
|
||||||
|
class Token
|
||||||
|
attr_reader :category, :raw_value, :value
|
||||||
|
|
||||||
|
def initialize(category, value, raw_value=nil)
|
||||||
|
@category = category
|
||||||
|
@value = value
|
||||||
|
@raw_value = raw_value
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
raw_value || value
|
||||||
|
end
|
||||||
|
|
||||||
|
def length
|
||||||
|
raw_value.to_s.length
|
||||||
|
end
|
||||||
|
|
||||||
|
def grouping?
|
||||||
|
is?(:grouping)
|
||||||
|
end
|
||||||
|
|
||||||
|
def is?(c)
|
||||||
|
category == c
|
||||||
|
end
|
||||||
|
|
||||||
|
def ==(other)
|
||||||
|
(category.nil? || other.category.nil? || category == other.category) &&
|
||||||
|
(value.nil? || other.value.nil? || value == other.value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,137 @@
|
||||||
|
require 'dentaku/token'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
class TokenMatcher
|
||||||
|
attr_reader :children, :categories, :values
|
||||||
|
|
||||||
|
def initialize(categories=nil, values=nil, children=[])
|
||||||
|
# store categories and values as hash to optimize key lookup, h/t @jan-mangs
|
||||||
|
@categories = [categories].compact.flatten.each_with_object({}) { |c,h| h[c] = 1 }
|
||||||
|
@values = [values].compact.flatten.each_with_object({}) { |v,h| h[v] = 1 }
|
||||||
|
@children = children.compact
|
||||||
|
@invert = false
|
||||||
|
|
||||||
|
@min = 1
|
||||||
|
@max = 1
|
||||||
|
@range = (@min..@max)
|
||||||
|
end
|
||||||
|
|
||||||
|
def | (other_matcher)
|
||||||
|
self.class.new(:nomatch, :nomatch, leaf_matchers + other_matcher.leaf_matchers)
|
||||||
|
end
|
||||||
|
|
||||||
|
def invert
|
||||||
|
@invert = ! @invert
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def ==(token)
|
||||||
|
leaf_matcher? ? matches_token?(token) : any_child_matches_token?(token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def match(token_stream, offset=0)
|
||||||
|
matched_tokens = []
|
||||||
|
matched = false
|
||||||
|
|
||||||
|
while self == token_stream[matched_tokens.length + offset] && matched_tokens.length < @max
|
||||||
|
matched_tokens << token_stream[matched_tokens.length + offset]
|
||||||
|
end
|
||||||
|
|
||||||
|
if @range.cover?(matched_tokens.length)
|
||||||
|
matched = true
|
||||||
|
end
|
||||||
|
|
||||||
|
[matched, matched_tokens]
|
||||||
|
end
|
||||||
|
|
||||||
|
def caret
|
||||||
|
@caret = true
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def caret?
|
||||||
|
@caret
|
||||||
|
end
|
||||||
|
|
||||||
|
def star
|
||||||
|
@min = 0
|
||||||
|
@max = Float::INFINITY
|
||||||
|
@range = (@min..@max)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def plus
|
||||||
|
@max = Float::INFINITY
|
||||||
|
@range = (@min..@max)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def leaf_matcher?
|
||||||
|
children.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def leaf_matchers
|
||||||
|
leaf_matcher? ? [self] : children
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def any_child_matches_token?(token)
|
||||||
|
children.any? { |child| child == token }
|
||||||
|
end
|
||||||
|
|
||||||
|
def matches_token?(token)
|
||||||
|
return false if token.nil?
|
||||||
|
(category_match(token.category) && value_match(token.value)) ^ @invert
|
||||||
|
end
|
||||||
|
|
||||||
|
def category_match(category)
|
||||||
|
@categories.empty? || @categories.key?(category)
|
||||||
|
end
|
||||||
|
|
||||||
|
def value_match(value)
|
||||||
|
@values.empty? || @values.key?(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.numeric; new(:numeric); end
|
||||||
|
def self.string; new(:string); end
|
||||||
|
def self.logical; new(:logical); end
|
||||||
|
def self.value
|
||||||
|
new(:numeric) | new(:string) | new(:logical)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.addsub; new(:operator, [:add, :subtract]); end
|
||||||
|
def self.subtract; new(:operator, :subtract); end
|
||||||
|
def self.anchored_minus; new(:operator, :subtract).caret; end
|
||||||
|
def self.muldiv; new(:operator, [:multiply, :divide]); end
|
||||||
|
def self.pow; new(:operator, :pow); end
|
||||||
|
def self.mod; new(:operator, :mod); end
|
||||||
|
def self.combinator; new(:combinator); end
|
||||||
|
|
||||||
|
def self.comparator; new(:comparator); end
|
||||||
|
def self.comp_gt; new(:comparator, [:gt, :ge]); end
|
||||||
|
def self.comp_lt; new(:comparator, [:lt, :le]); end
|
||||||
|
|
||||||
|
def self.open; new(:grouping, :open); end
|
||||||
|
def self.close; new(:grouping, :close); end
|
||||||
|
def self.comma; new(:grouping, :comma); end
|
||||||
|
def self.non_group; new(:grouping).invert; end
|
||||||
|
def self.non_group_star; new(:grouping).invert.star; end
|
||||||
|
def self.non_close_plus; new(:grouping, :close).invert.plus; end
|
||||||
|
def self.arguments; (value | comma).plus; end
|
||||||
|
|
||||||
|
def self.if; new(:function, :if); end
|
||||||
|
def self.round; new(:function, :round); end
|
||||||
|
def self.roundup; new(:function, :roundup); end
|
||||||
|
def self.rounddown; new(:function, :rounddown); end
|
||||||
|
def self.not; new(:function, :not); end
|
||||||
|
|
||||||
|
def self.method_missing(name, *args, &block)
|
||||||
|
new(:function, name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.respond_to_missing?(name, include_priv)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
module Dentaku
|
||||||
|
module TokenMatchers
|
||||||
|
def self.token_matchers(*symbols)
|
||||||
|
symbols.map { |s| matcher(s) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.function_token_matchers(function_name, *symbols)
|
||||||
|
token_matchers(:open, *symbols, :close).unshift(
|
||||||
|
TokenMatcher.send(function_name)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.matcher(symbol)
|
||||||
|
@matchers ||= [
|
||||||
|
:numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
|
||||||
|
:comparator, :comp_gt, :comp_lt, :open, :close, :comma,
|
||||||
|
:non_close_plus, :non_group, :non_group_star, :arguments,
|
||||||
|
:logical, :combinator, :if, :round, :roundup, :rounddown, :not,
|
||||||
|
:anchored_minus, :math_neg_pow, :math_neg_mul
|
||||||
|
].each_with_object({}) do |name, matchers|
|
||||||
|
matchers[name] = TokenMatcher.send(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
@matchers.fetch(symbol) do
|
||||||
|
raise "Unknown token symbol #{ symbol }"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,142 @@
|
||||||
|
require 'bigdecimal'
|
||||||
|
require 'dentaku/token'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
class TokenScanner
|
||||||
|
def initialize(category, regexp, converter=nil, condition=nil)
|
||||||
|
@category = category
|
||||||
|
@regexp = %r{\A(#{ regexp })}i
|
||||||
|
@converter = converter
|
||||||
|
@condition = condition || ->(*) { true }
|
||||||
|
end
|
||||||
|
|
||||||
|
def scan(string, last_token=nil)
|
||||||
|
if (m = @regexp.match(string)) && @condition.call(last_token)
|
||||||
|
value = raw = m.to_s
|
||||||
|
value = @converter.call(raw) if @converter
|
||||||
|
|
||||||
|
return Array(value).map do |v|
|
||||||
|
Token === v ? v : Token.new(@category, v, raw)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def available_scanners
|
||||||
|
[
|
||||||
|
:null,
|
||||||
|
:whitespace,
|
||||||
|
:numeric,
|
||||||
|
:double_quoted_string,
|
||||||
|
:single_quoted_string,
|
||||||
|
:negate,
|
||||||
|
:operator,
|
||||||
|
:grouping,
|
||||||
|
:case_statement,
|
||||||
|
:comparator,
|
||||||
|
:combinator,
|
||||||
|
:boolean,
|
||||||
|
:function,
|
||||||
|
:identifier
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def register_default_scanners
|
||||||
|
register_scanners(available_scanners)
|
||||||
|
end
|
||||||
|
|
||||||
|
def register_scanners(scanner_ids)
|
||||||
|
@scanners = scanner_ids.each_with_object({}) do |id, scanners|
|
||||||
|
scanners[id] = self.send(id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def register_scanner(id, scanner)
|
||||||
|
@scanners[id] = scanner
|
||||||
|
end
|
||||||
|
|
||||||
|
def scanners=(scanner_ids)
|
||||||
|
@scanners.select! { |k,v| scanner_ids.include?(k) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def scanners
|
||||||
|
@scanners.values
|
||||||
|
end
|
||||||
|
|
||||||
|
def whitespace
|
||||||
|
new(:whitespace, '\s+')
|
||||||
|
end
|
||||||
|
|
||||||
|
def null
|
||||||
|
new(:null, 'null\b')
|
||||||
|
end
|
||||||
|
|
||||||
|
def numeric
|
||||||
|
new(:numeric, '(\d+(\.\d+)?|\.\d+)\b', lambda { |raw| raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i })
|
||||||
|
end
|
||||||
|
|
||||||
|
def double_quoted_string
|
||||||
|
new(:string, '"[^"]*"', lambda { |raw| raw.gsub(/^"|"$/, '') })
|
||||||
|
end
|
||||||
|
|
||||||
|
def single_quoted_string
|
||||||
|
new(:string, "'[^']*'", lambda { |raw| raw.gsub(/^'|'$/, '') })
|
||||||
|
end
|
||||||
|
|
||||||
|
def negate
|
||||||
|
new(:operator, '-', lambda { |raw| :negate }, lambda { |last_token|
|
||||||
|
last_token.nil? ||
|
||||||
|
last_token.is?(:operator) ||
|
||||||
|
last_token.is?(:comparator) ||
|
||||||
|
last_token.is?(:combinator) ||
|
||||||
|
last_token.value == :open ||
|
||||||
|
last_token.value == :comma
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def operator
|
||||||
|
names = { pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%' }.invert
|
||||||
|
new(:operator, '\^|\+|-|\*|\/|%', lambda { |raw| names[raw] })
|
||||||
|
end
|
||||||
|
|
||||||
|
def grouping
|
||||||
|
names = { open: '(', close: ')', comma: ',' }.invert
|
||||||
|
new(:grouping, '\(|\)|,', lambda { |raw| names[raw] })
|
||||||
|
end
|
||||||
|
|
||||||
|
def case_statement
|
||||||
|
names = { open: 'case', close: 'end', then: 'then', when: 'when', else: 'else' }.invert
|
||||||
|
new(:case, '(case|end|then|when|else)\b', lambda { |raw| names[raw.downcase] })
|
||||||
|
end
|
||||||
|
|
||||||
|
def comparator
|
||||||
|
names = { le: '<=', ge: '>=', ne: '!=', lt: '<', gt: '>', eq: '=' }.invert
|
||||||
|
alternate = { ne: '<>', eq: '==' }.invert
|
||||||
|
new(:comparator, '<=|>=|!=|<>|<|>|==|=', lambda { |raw| names[raw] || alternate[raw] })
|
||||||
|
end
|
||||||
|
|
||||||
|
def combinator
|
||||||
|
new(:combinator, '(and|or)\b', lambda { |raw| raw.strip.downcase.to_sym })
|
||||||
|
end
|
||||||
|
|
||||||
|
def boolean
|
||||||
|
new(:logical, '(true|false)\b', lambda { |raw| raw.strip.downcase == 'true' })
|
||||||
|
end
|
||||||
|
|
||||||
|
def function
|
||||||
|
new(:function, '\w+\s*\(', lambda do |raw|
|
||||||
|
function_name = raw.gsub('(', '')
|
||||||
|
[Token.new(:function, function_name.strip.downcase.to_sym, function_name), Token.new(:grouping, :open, '(')]
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def identifier
|
||||||
|
new(:identifier, '\w+\b', lambda { |raw| raw.strip.downcase })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
register_default_scanners
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,54 @@
|
||||||
|
require 'dentaku/token'
|
||||||
|
require 'dentaku/token_matcher'
|
||||||
|
require 'dentaku/token_scanner'
|
||||||
|
|
||||||
|
module Dentaku
|
||||||
|
class Tokenizer
|
||||||
|
LPAREN = TokenMatcher.new(:grouping, :open)
|
||||||
|
RPAREN = TokenMatcher.new(:grouping, :close)
|
||||||
|
|
||||||
|
def tokenize(string)
|
||||||
|
@nesting = 0
|
||||||
|
@tokens = []
|
||||||
|
input = strip_comments(string.to_s.dup)
|
||||||
|
|
||||||
|
until input.empty?
|
||||||
|
fail TokenizerError, "parse error at: '#{ input }'" unless TokenScanner.scanners.any? do |scanner|
|
||||||
|
scanned, input = scan(input, scanner)
|
||||||
|
scanned
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
fail TokenizerError, "too many opening parentheses" if @nesting > 0
|
||||||
|
|
||||||
|
@tokens
|
||||||
|
end
|
||||||
|
|
||||||
|
def last_token
|
||||||
|
@tokens.last
|
||||||
|
end
|
||||||
|
|
||||||
|
def scan(string, scanner)
|
||||||
|
if tokens = scanner.scan(string, last_token)
|
||||||
|
tokens.each do |token|
|
||||||
|
fail TokenizerError, "unexpected zero-width match (:#{ token.category }) at '#{ string }'" if token.length == 0
|
||||||
|
|
||||||
|
@nesting += 1 if LPAREN == token
|
||||||
|
@nesting -= 1 if RPAREN == token
|
||||||
|
fail TokenizerError, "too many closing parentheses" if @nesting < 0
|
||||||
|
|
||||||
|
@tokens << token unless token.is?(:whitespace)
|
||||||
|
end
|
||||||
|
|
||||||
|
match_length = tokens.map(&:length).reduce(:+)
|
||||||
|
[true, string[match_length..-1]]
|
||||||
|
else
|
||||||
|
[false, string]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def strip_comments(input)
|
||||||
|
input.gsub(/\/\*[^*]*\*+(?:[^*\/][^*]*\*+)*\//, '')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
module Dentaku
|
||||||
|
VERSION = "2.0.9"
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/ast/arithmetic'
|
||||||
|
|
||||||
|
require 'dentaku/token'
|
||||||
|
|
||||||
|
describe Dentaku::AST::Addition do
|
||||||
|
let(:five) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 5) }
|
||||||
|
let(:six) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 6) }
|
||||||
|
|
||||||
|
let(:t) { Dentaku::AST::Numeric.new Dentaku::Token.new(:logical, true) }
|
||||||
|
|
||||||
|
it 'performs addition' do
|
||||||
|
node = described_class.new(five, six)
|
||||||
|
expect(node.value).to eq 11
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'requires numeric operands' do
|
||||||
|
expect {
|
||||||
|
described_class.new(five, t)
|
||||||
|
}.to raise_error(Dentaku::ParseError, /requires numeric operands/)
|
||||||
|
|
||||||
|
expression = Dentaku::AST::Multiplication.new(five, five)
|
||||||
|
group = Dentaku::AST::Grouping.new(expression)
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.new(group, five)
|
||||||
|
}.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,32 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/ast/combinators'
|
||||||
|
|
||||||
|
require 'dentaku/token'
|
||||||
|
|
||||||
|
describe Dentaku::AST::And do
|
||||||
|
let(:t) { Dentaku::AST::Logical.new Dentaku::Token.new(:logical, true) }
|
||||||
|
let(:f) { Dentaku::AST::Logical.new Dentaku::Token.new(:logical, false) }
|
||||||
|
|
||||||
|
let(:five) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 5) }
|
||||||
|
|
||||||
|
it 'performs logical AND' do
|
||||||
|
node = described_class.new(t, f)
|
||||||
|
expect(node.value).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'requires logical operands' do
|
||||||
|
expect {
|
||||||
|
described_class.new(t, five)
|
||||||
|
}.to raise_error(Dentaku::ParseError, /requires logical operands/)
|
||||||
|
|
||||||
|
expression = Dentaku::AST::LessThanOrEqual.new(five, five)
|
||||||
|
expect {
|
||||||
|
described_class.new(t, expression)
|
||||||
|
}.not_to raise_error
|
||||||
|
|
||||||
|
expression = Dentaku::AST::Or.new(t, f)
|
||||||
|
expect {
|
||||||
|
described_class.new(t, expression)
|
||||||
|
}.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,80 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/ast/operation'
|
||||||
|
require 'dentaku/ast/logical'
|
||||||
|
require 'dentaku/ast/identifier'
|
||||||
|
require 'dentaku/ast/arithmetic'
|
||||||
|
require 'dentaku/ast/case'
|
||||||
|
|
||||||
|
require 'dentaku/token'
|
||||||
|
|
||||||
|
describe Dentaku::AST::Case do
|
||||||
|
let!(:one) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 1) }
|
||||||
|
let!(:two) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 2) }
|
||||||
|
let!(:apple) do
|
||||||
|
Dentaku::AST::Logical.new Dentaku::Token.new(:string, 'apple')
|
||||||
|
end
|
||||||
|
let!(:banana) do
|
||||||
|
Dentaku::AST::Logical.new Dentaku::Token.new(:string, 'banana')
|
||||||
|
end
|
||||||
|
let!(:identifier) do
|
||||||
|
Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, :fruit))
|
||||||
|
end
|
||||||
|
let!(:switch) { Dentaku::AST::CaseSwitchVariable.new(identifier) }
|
||||||
|
|
||||||
|
let!(:when1) { Dentaku::AST::CaseWhen.new(apple) }
|
||||||
|
let!(:then1) { Dentaku::AST::CaseThen.new(one) }
|
||||||
|
let!(:conditional1) { Dentaku::AST::CaseConditional.new(when1, then1) }
|
||||||
|
|
||||||
|
let!(:when2) { Dentaku::AST::CaseWhen.new(banana) }
|
||||||
|
let!(:then2) { Dentaku::AST::CaseThen.new(two) }
|
||||||
|
let!(:conditional2) { Dentaku::AST::CaseConditional.new(when2, then2) }
|
||||||
|
|
||||||
|
describe '#value' do
|
||||||
|
it 'raises an exception if there is no switch variable' do
|
||||||
|
expect { described_class.new(conditional1, conditional2) }
|
||||||
|
.to raise_error('Case missing switch variable')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an exception if a non-conditional is passed' do
|
||||||
|
expect { described_class.new(switch, conditional1, when2) }
|
||||||
|
.to raise_error(/is not a CaseConditional/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tests each conditional against the switch variable' do
|
||||||
|
node = described_class.new(switch, conditional1, conditional2)
|
||||||
|
expect(node.value(fruit: 'banana')).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an exception if the conditional is not matched' do
|
||||||
|
node = described_class.new(switch, conditional1, conditional2)
|
||||||
|
expect { node.value(fruit: 'orange') }
|
||||||
|
.to raise_error("No block matched the switch value 'orange'")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses the else value if provided and conditional is not matched' do
|
||||||
|
three = Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 3)
|
||||||
|
else_statement = Dentaku::AST::CaseElse.new(three)
|
||||||
|
node = described_class.new(
|
||||||
|
switch,
|
||||||
|
conditional1,
|
||||||
|
conditional2,
|
||||||
|
else_statement)
|
||||||
|
expect(node.value(fruit: 'orange')).to eq(3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#dependencies' do
|
||||||
|
let!(:tax) do
|
||||||
|
Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, :tax))
|
||||||
|
end
|
||||||
|
let!(:addition) { Dentaku::AST::Addition.new(two, tax) }
|
||||||
|
let!(:when2) { Dentaku::AST::CaseWhen.new(banana) }
|
||||||
|
let!(:then2) { Dentaku::AST::CaseThen.new(addition) }
|
||||||
|
let!(:conditional2) { Dentaku::AST::CaseConditional.new(when2, then2) }
|
||||||
|
|
||||||
|
it 'gathers dependencies from switch and conditionals' do
|
||||||
|
node = described_class.new(switch, conditional1, conditional2)
|
||||||
|
expect(node.dependencies).to eq([:fruit, :tax])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/ast/arithmetic'
|
||||||
|
|
||||||
|
require 'dentaku/token'
|
||||||
|
|
||||||
|
describe Dentaku::AST::Division do
|
||||||
|
let(:five) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 5) }
|
||||||
|
let(:six) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 6) }
|
||||||
|
|
||||||
|
let(:t) { Dentaku::AST::Numeric.new Dentaku::Token.new(:logical, true) }
|
||||||
|
|
||||||
|
it 'performs division' do
|
||||||
|
node = described_class.new(five, six)
|
||||||
|
expect(node.value.round(4)).to eq 0.8333
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'requires numeric operands' do
|
||||||
|
expect {
|
||||||
|
described_class.new(five, t)
|
||||||
|
}.to raise_error(Dentaku::ParseError, /requires numeric operands/)
|
||||||
|
|
||||||
|
expression = Dentaku::AST::Multiplication.new(five, five)
|
||||||
|
group = Dentaku::AST::Grouping.new(expression)
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.new(group, five)
|
||||||
|
}.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,21 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/ast/function'
|
||||||
|
|
||||||
|
describe Dentaku::AST::Function do
|
||||||
|
it 'maintains a function registry' do
|
||||||
|
expect(described_class).to respond_to(:get)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an exception when trying to access an undefined function' do
|
||||||
|
expect {
|
||||||
|
described_class.get("flarble")
|
||||||
|
}.to raise_error(Dentaku::ParseError, /undefined function/i)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'registers a custom function' do
|
||||||
|
described_class.register("flarble", :string, -> { "flarble" })
|
||||||
|
expect { described_class.get("flarble") }.not_to raise_error
|
||||||
|
function = described_class.get("flarble").new
|
||||||
|
expect(function.value).to eq "flarble"
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,40 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/ast/node'
|
||||||
|
require 'dentaku/tokenizer'
|
||||||
|
require 'dentaku/parser'
|
||||||
|
|
||||||
|
describe Dentaku::AST::Node do
|
||||||
|
it 'returns list of dependencies' do
|
||||||
|
node = make_node('x + 5')
|
||||||
|
expect(node.dependencies).to eq ['x']
|
||||||
|
|
||||||
|
node = make_node('5 < x')
|
||||||
|
expect(node.dependencies).to eq ['x']
|
||||||
|
|
||||||
|
node = make_node('5 < 7')
|
||||||
|
expect(node.dependencies).to eq []
|
||||||
|
|
||||||
|
node = make_node('(y * 7)')
|
||||||
|
expect(node.dependencies).to eq ['y']
|
||||||
|
|
||||||
|
node = make_node('if(x > 5, y, z)')
|
||||||
|
expect(node.dependencies).to eq ['x', 'y', 'z']
|
||||||
|
|
||||||
|
node = make_node('if(x > 5, y, z)')
|
||||||
|
expect(node.dependencies('x' => 7)).to eq ['y', 'z']
|
||||||
|
|
||||||
|
node = make_node('')
|
||||||
|
expect(node.dependencies).to eq []
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns unique list of dependencies' do
|
||||||
|
node = make_node('x + x')
|
||||||
|
expect(node.dependencies).to eq ['x']
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def make_node(expression)
|
||||||
|
Dentaku::Parser.new(Dentaku::Tokenizer.new.tokenize(expression)).parse
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,16 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/ast/numeric'
|
||||||
|
|
||||||
|
require 'dentaku/token'
|
||||||
|
|
||||||
|
describe Dentaku::AST::Numeric do
|
||||||
|
subject { described_class.new(Dentaku::Token.new(:numeric, 5)) }
|
||||||
|
|
||||||
|
it 'has numeric type' do
|
||||||
|
expect(subject.type).to eq :numeric
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has no dependencies' do
|
||||||
|
expect(subject.dependencies).to be_empty
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,135 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/ast/functions/string_functions'
|
||||||
|
|
||||||
|
describe Dentaku::AST::StringFunctions::Left do
|
||||||
|
let(:string) { identifier('string') }
|
||||||
|
let(:length) { identifier('length') }
|
||||||
|
|
||||||
|
subject { described_class.new(string, length) }
|
||||||
|
|
||||||
|
it 'returns the left N characters of the string' do
|
||||||
|
expect(subject.value('string' => 'ABCDEFG', 'length' => 4)).to eq 'ABCD'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'works correctly with literals' do
|
||||||
|
left = literal('ABCD')
|
||||||
|
len = literal(2)
|
||||||
|
fn = described_class.new(left, len)
|
||||||
|
expect(fn.value).to eq 'AB'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles an empty string correctly' do
|
||||||
|
expect(subject.value('string' => '', 'length' => 4)).to eq ''
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles size greater than input string length correctly' do
|
||||||
|
expect(subject.value('string' => 'abcdefg', 'length' => 40)).to eq 'abcdefg'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Dentaku::AST::StringFunctions::Right do
|
||||||
|
it 'returns the right N characters of the string' do
|
||||||
|
subject = described_class.new(literal('ABCDEFG'), literal(4))
|
||||||
|
expect(subject.value).to eq 'DEFG'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles an empty string correctly' do
|
||||||
|
subject = described_class.new(literal(''), literal(4))
|
||||||
|
expect(subject.value).to eq ''
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles size greater than input string length correctly' do
|
||||||
|
subject = described_class.new(literal('abcdefg'), literal(40))
|
||||||
|
expect(subject.value).to eq 'abcdefg'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Dentaku::AST::StringFunctions::Mid do
|
||||||
|
it 'returns a substring from the middle of the string' do
|
||||||
|
subject = described_class.new(literal('ABCDEFG'), literal(4), literal(2))
|
||||||
|
expect(subject.value).to eq 'DE'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles an empty string correctly' do
|
||||||
|
subject = described_class.new(literal(''), literal(4), literal(2))
|
||||||
|
expect(subject.value).to eq ''
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles offset greater than input string length correctly' do
|
||||||
|
subject = described_class.new(literal('abcdefg'), literal(40), literal(4))
|
||||||
|
expect(subject.value).to eq ''
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles size greater than input string length correctly' do
|
||||||
|
subject = described_class.new(literal('abcdefg'), literal(4), literal(40))
|
||||||
|
expect(subject.value).to eq 'defg'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Dentaku::AST::StringFunctions::Len do
|
||||||
|
it 'returns the length of a string' do
|
||||||
|
subject = described_class.new(literal('ABCDEFG'))
|
||||||
|
expect(subject.value).to eq 7
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles an empty string correctly' do
|
||||||
|
subject = described_class.new(literal(''))
|
||||||
|
expect(subject.value).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Dentaku::AST::StringFunctions::Find do
|
||||||
|
it 'returns the position of a substring within a string' do
|
||||||
|
subject = described_class.new(literal('DE'), literal('ABCDEFG'))
|
||||||
|
expect(subject.value).to eq 4
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles an empty substring correctly' do
|
||||||
|
subject = described_class.new(literal(''), literal('ABCDEFG'))
|
||||||
|
expect(subject.value).to eq 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles an empty string correctly' do
|
||||||
|
subject = described_class.new(literal('DE'), literal(''))
|
||||||
|
expect(subject.value).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Dentaku::AST::StringFunctions::Substitute do
|
||||||
|
it 'replaces a substring within a string' do
|
||||||
|
subject = described_class.new(literal('ABCDEFG'), literal('DE'), literal('xy'))
|
||||||
|
expect(subject.value).to eq 'ABCxyFG'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles an empty search string correctly' do
|
||||||
|
subject = described_class.new(literal('ABCDEFG'), literal(''), literal('xy'))
|
||||||
|
expect(subject.value).to eq 'xyABCDEFG'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles an empty replacement string correctly' do
|
||||||
|
subject = described_class.new(literal('ABCDEFG'), literal('DE'), literal(''))
|
||||||
|
expect(subject.value).to eq 'ABCFG'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Dentaku::AST::StringFunctions::Concat do
|
||||||
|
it 'concatenates two strings' do
|
||||||
|
subject = described_class.new(literal('ABC'), literal('DEF'))
|
||||||
|
expect(subject.value).to eq 'ABCDEF'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'concatenates a string onto an empty string' do
|
||||||
|
subject = described_class.new(literal(''), literal('ABC'))
|
||||||
|
expect(subject.value).to eq 'ABC'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'concatenates an empty string onto a string' do
|
||||||
|
subject = described_class.new(literal('ABC'), literal(''))
|
||||||
|
expect(subject.value).to eq 'ABC'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'concatenates two empty strings' do
|
||||||
|
subject = described_class.new(literal(''), literal(''))
|
||||||
|
expect(subject.value).to eq ''
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require 'dentaku'
|
||||||
|
require 'allocation_stats'
|
||||||
|
require 'benchmark'
|
||||||
|
|
||||||
|
puts "Dentaku version #{Dentaku::VERSION}"
|
||||||
|
puts "Ruby version #{RUBY_VERSION}"
|
||||||
|
|
||||||
|
with_duplicate_variables = [
|
||||||
|
"R1+R2+R3+R4+R5+R6",
|
||||||
|
{"R1"=>100000, "R2"=>0, "R3"=>200000, "R4"=>0, "R5"=>500000, "R6"=>0, "r1"=>100000, "r2"=>0, "r3"=>200000, "r4"=>0, "r5"=>500000, "r6"=>0}
|
||||||
|
]
|
||||||
|
|
||||||
|
without_duplicate_variables = [
|
||||||
|
"R1+R2+R3+R4+R5+R6",
|
||||||
|
{"R1"=>100000, "R2"=>0, "R3"=>200000, "R4"=>0, "R5"=>500000, "R6"=>0}
|
||||||
|
]
|
||||||
|
|
||||||
|
def test(args, custom_function: true)
|
||||||
|
calls = [ args ] * 100
|
||||||
|
|
||||||
|
10.times do |i|
|
||||||
|
|
||||||
|
stats = nil
|
||||||
|
bm = Benchmark.measure do
|
||||||
|
stats = AllocationStats.trace do
|
||||||
|
|
||||||
|
calls.each do |formula, bound|
|
||||||
|
|
||||||
|
calculator = Dentaku::Calculator.new
|
||||||
|
|
||||||
|
if custom_function
|
||||||
|
calculator.add_function(
|
||||||
|
:sum,
|
||||||
|
:numeric,
|
||||||
|
->(numbers) { numbers.inject(:+) }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
calculator.evaluate(formula, bound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
puts " run #{i}: #{bm.total}"
|
||||||
|
puts stats.allocations(alias_paths: true).group_by(:sourcefile, :class).to_text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
case ARGV[0]
|
||||||
|
when '1'
|
||||||
|
puts "with duplicate (downcased) variables, with a custom function:"
|
||||||
|
test(with_duplicate_variables, custom_function: true)
|
||||||
|
|
||||||
|
when '2'
|
||||||
|
puts "with duplicate (downcased) variables, without a custom function:"
|
||||||
|
test(with_duplicate_variables, custom_function: false)
|
||||||
|
|
||||||
|
when '3'
|
||||||
|
puts "without duplicate (downcased) variables, with a custom function:"
|
||||||
|
test(without_duplicate_variables, custom_function: true)
|
||||||
|
|
||||||
|
when '4'
|
||||||
|
puts "with duplicate (downcased) variables, without a custom function:"
|
||||||
|
test(without_duplicate_variables, custom_function: false)
|
||||||
|
|
||||||
|
else
|
||||||
|
puts "select a run option (1-4)"
|
||||||
|
end
|
|
@ -0,0 +1,77 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/bulk_expression_solver'
|
||||||
|
|
||||||
|
RSpec.describe Dentaku::BulkExpressionSolver do
|
||||||
|
let(:calculator) { Dentaku::Calculator.new }
|
||||||
|
|
||||||
|
describe "#solve!" do
|
||||||
|
it "evaluates properly with variables, even if some in memory" do
|
||||||
|
expressions = {
|
||||||
|
weekly_fruit_budget: "weekly_apple_budget + pear * 4",
|
||||||
|
weekly_apple_budget: "apples * 7",
|
||||||
|
pear: "1"
|
||||||
|
}
|
||||||
|
solver = described_class.new(expressions, calculator.store(apples: 3))
|
||||||
|
expect(solver.solve!)
|
||||||
|
.to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "lets you know if a variable is unbound" do
|
||||||
|
expressions = {more_apples: "apples + 1"}
|
||||||
|
expect {
|
||||||
|
described_class.new(expressions, calculator).solve!
|
||||||
|
}.to raise_error(Dentaku::UnboundVariableError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "lets you know if the result is a div/0 error" do
|
||||||
|
expressions = {more_apples: "1/0"}
|
||||||
|
expect {
|
||||||
|
described_class.new(expressions, calculator).solve!
|
||||||
|
}.to raise_error(Dentaku::ZeroDivisionError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not require keys to be parseable" do
|
||||||
|
expressions = { "the value of x, incremented" => "x + 1" }
|
||||||
|
solver = described_class.new(expressions, calculator.store("x" => 3))
|
||||||
|
expect(solver.solve!).to eq({ "the value of x, incremented" => 4 })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#solve" do
|
||||||
|
it "returns :undefined when variables are unbound" do
|
||||||
|
expressions = {more_apples: "apples + 1"}
|
||||||
|
expect(described_class.new(expressions, calculator).solve)
|
||||||
|
.to eq(more_apples: :undefined)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows passing in a custom value to an error handler when a variable is unbound" do
|
||||||
|
expressions = {more_apples: "apples + 1"}
|
||||||
|
expect(described_class.new(expressions, calculator).solve { :foo })
|
||||||
|
.to eq(more_apples: :foo)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows passing in a custom value to an error handler when there is a div/0 error" do
|
||||||
|
expressions = {more_apples: "1/0"}
|
||||||
|
expect(described_class.new(expressions, calculator).solve { :foo })
|
||||||
|
.to eq(more_apples: :foo)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores the recipient variable on the exception when there is a div/0 error' do
|
||||||
|
expressions = {more_apples: "1/0"}
|
||||||
|
exception = nil
|
||||||
|
described_class.new(expressions, calculator).solve do |ex|
|
||||||
|
exception = ex
|
||||||
|
end
|
||||||
|
expect(exception.recipient_variable).to eq('more_apples')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores the recipient variable on the exception when there is an unbound variable' do
|
||||||
|
expressions = {more_apples: "apples + 1"}
|
||||||
|
exception = nil
|
||||||
|
described_class.new(expressions, calculator).solve do |ex|
|
||||||
|
exception = ex
|
||||||
|
end
|
||||||
|
expect(exception.recipient_variable).to eq('more_apples')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,450 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/calculator'
|
||||||
|
|
||||||
|
describe Dentaku::Calculator do
|
||||||
|
let(:calculator) { described_class.new }
|
||||||
|
let(:with_memory) { described_class.new.store(apples: 3) }
|
||||||
|
|
||||||
|
it 'evaluates an expression' do
|
||||||
|
expect(calculator.evaluate('7+3')).to eq(10)
|
||||||
|
expect(calculator.evaluate('2 -1')).to eq(1)
|
||||||
|
expect(calculator.evaluate('-1 + 2')).to eq(1)
|
||||||
|
expect(calculator.evaluate('1 - 2')).to eq(-1)
|
||||||
|
expect(calculator.evaluate('1 - - 2')).to eq(3)
|
||||||
|
expect(calculator.evaluate('-1 - - 2')).to eq(1)
|
||||||
|
expect(calculator.evaluate('1 - - - 2')).to eq(-1)
|
||||||
|
expect(calculator.evaluate('(-1 + 2)')).to eq(1)
|
||||||
|
expect(calculator.evaluate('-(1 + 2)')).to eq(-3)
|
||||||
|
expect(calculator.evaluate('2 ^ - 1')).to eq(0.5)
|
||||||
|
expect(calculator.evaluate('2 ^ -(3 - 2)')).to eq(0.5)
|
||||||
|
expect(calculator.evaluate('(2 + 3) - 1')).to eq(4)
|
||||||
|
expect(calculator.evaluate('(-2 + 3) - 1')).to eq(0)
|
||||||
|
expect(calculator.evaluate('(-2 - 3) - 1')).to eq(-6)
|
||||||
|
expect(calculator.evaluate('1 + -(2 ^ 2)')).to eq(-3)
|
||||||
|
expect(calculator.evaluate('3 + -num', num: 2)).to eq(1)
|
||||||
|
expect(calculator.evaluate('-num + 3', num: 2)).to eq(1)
|
||||||
|
expect(calculator.evaluate('10 ^ 2')).to eq(100)
|
||||||
|
expect(calculator.evaluate('0 * 10 ^ -5')).to eq(0)
|
||||||
|
expect(calculator.evaluate('3 + 0 * -3')).to eq(3)
|
||||||
|
expect(calculator.evaluate('3 + 0 / -3')).to eq(3)
|
||||||
|
expect(calculator.evaluate('15 % 8')).to eq(7)
|
||||||
|
expect(calculator.evaluate('(((695759/735000)^(1/(1981-1991)))-1)*1000').round(4)).to eq(5.5018)
|
||||||
|
expect(calculator.evaluate('0.253/0.253')).to eq(1)
|
||||||
|
expect(calculator.evaluate('0.253/d', d: 0.253)).to eq(1)
|
||||||
|
expect(calculator.evaluate('10 + x', x: 'abc')).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'memory' do
|
||||||
|
it { expect(calculator).to be_empty }
|
||||||
|
it { expect(with_memory).not_to be_empty }
|
||||||
|
it { expect(with_memory.clear).to be_empty }
|
||||||
|
|
||||||
|
it 'discards local values' do
|
||||||
|
expect(calculator.evaluate('pears * 2', pears: 5)).to eq(10)
|
||||||
|
expect(calculator).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can store the value `false`' do
|
||||||
|
calculator.store('i_am_false', false)
|
||||||
|
expect(calculator.evaluate!('i_am_false')).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can store multiple values' do
|
||||||
|
calculator.store(first: 1, second: 2)
|
||||||
|
expect(calculator.evaluate!('first')).to eq 1
|
||||||
|
expect(calculator.evaluate!('second')).to eq 2
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores formulas' do
|
||||||
|
calculator.store_formula('area', 'length * width')
|
||||||
|
expect(calculator.evaluate!('area', length: 5, width: 5)).to eq 25
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'dependencies' do
|
||||||
|
it "finds dependencies in a generic statement" do
|
||||||
|
expect(calculator.dependencies("bob + dole / 3")).to eq(['bob', 'dole'])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't consider variables in memory as dependencies" do
|
||||||
|
expect(with_memory.dependencies("apples + oranges")).to eq(['oranges'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'solve!' do
|
||||||
|
it "evaluates properly with variables, even if some in memory" do
|
||||||
|
expect(with_memory.solve!(
|
||||||
|
weekly_fruit_budget: "weekly_apple_budget + pear * 4",
|
||||||
|
weekly_apple_budget: "apples * 7",
|
||||||
|
pear: "1"
|
||||||
|
)).to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "preserves hash keys" do
|
||||||
|
expect(calculator.solve!(
|
||||||
|
'meaning_of_life' => 'age + kids',
|
||||||
|
'age' => 40,
|
||||||
|
'kids' => 2
|
||||||
|
)).to eq('age' => 40, 'kids' => 2, 'meaning_of_life' => 42)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "lets you know about a cycle if one occurs" do
|
||||||
|
expect do
|
||||||
|
calculator.solve!(health: "happiness", happiness: "health")
|
||||||
|
end.to raise_error(TSort::Cyclic)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is case-insensitive' do
|
||||||
|
result = with_memory.solve!(total_fruit: "Apples + pears", pears: 10)
|
||||||
|
expect(result[:total_fruit]).to eq 13
|
||||||
|
end
|
||||||
|
|
||||||
|
it "lets you know if a variable is unbound" do
|
||||||
|
expect {
|
||||||
|
calculator.solve!(more_apples: "apples + 1")
|
||||||
|
}.to raise_error(Dentaku::UnboundVariableError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can reference stored formulas' do
|
||||||
|
calculator.store_formula("base_area", "length * width")
|
||||||
|
calculator.store_formula("volume", "base_area * height")
|
||||||
|
|
||||||
|
result = calculator.solve!(
|
||||||
|
weight: "volume * 5.432",
|
||||||
|
height: "3",
|
||||||
|
length: "2",
|
||||||
|
width: "length * 2",
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result[:weight]).to eq 130.368
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'solve' do
|
||||||
|
it "returns :undefined when variables are unbound" do
|
||||||
|
expressions = {more_apples: "apples + 1"}
|
||||||
|
expect(calculator.solve(expressions)).to eq(more_apples: :undefined)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows passing in a custom value to an error handler" do
|
||||||
|
expressions = {more_apples: "apples + 1"}
|
||||||
|
expect(calculator.solve(expressions) { :foo })
|
||||||
|
.to eq(more_apples: :foo)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "solves remainder of expressions with unbound variable" do
|
||||||
|
calculator.store(peaches: 1, oranges: 1)
|
||||||
|
expressions = { more_apples: "apples + 1", more_peaches: "peaches + 1" }
|
||||||
|
result = calculator.solve(expressions)
|
||||||
|
expect(calculator.memory).to eq("peaches" => 1, "oranges" => 1)
|
||||||
|
expect(result).to eq(
|
||||||
|
more_apples: :undefined,
|
||||||
|
more_peaches: 2
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "solves remainder of expressions when one cannot be evaluated" do
|
||||||
|
result = calculator.solve(
|
||||||
|
conditional: "IF(d != 0, ratio, 0)",
|
||||||
|
ratio: "10/d",
|
||||||
|
d: 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).to eq(
|
||||||
|
conditional: 0,
|
||||||
|
ratio: :undefined,
|
||||||
|
d: 0,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'evaluates a statement with no variables' do
|
||||||
|
expect(calculator.evaluate('5+3')).to eq(8)
|
||||||
|
expect(calculator.evaluate('(1+1+1)/3*100')).to eq(100)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails to evaluate unbound statements' do
|
||||||
|
unbound = 'foo * 1.5'
|
||||||
|
expect { calculator.evaluate!(unbound) }.to raise_error(Dentaku::UnboundVariableError)
|
||||||
|
expect { calculator.evaluate!(unbound) }.to raise_error do |error|
|
||||||
|
expect(error.unbound_variables).to eq ['foo']
|
||||||
|
end
|
||||||
|
expect(calculator.evaluate(unbound)).to be_nil
|
||||||
|
expect(calculator.evaluate(unbound) { :bar }).to eq :bar
|
||||||
|
expect(calculator.evaluate(unbound) { |e| e }).to eq unbound
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'evaluates unbound statements given a binding in memory' do
|
||||||
|
expect(calculator.evaluate('foo * 1.5', foo: 2)).to eq(3)
|
||||||
|
expect(calculator.bind(monkeys: 3).evaluate('monkeys < 7')).to be_truthy
|
||||||
|
expect(calculator.evaluate('monkeys / 1.5')).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rebinds for each evaluation' do
|
||||||
|
expect(calculator.evaluate('foo * 2', foo: 2)).to eq(4)
|
||||||
|
expect(calculator.evaluate('foo * 2', foo: 4)).to eq(8)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts strings or symbols for binding keys' do
|
||||||
|
expect(calculator.evaluate('foo * 2', foo: 2)).to eq(4)
|
||||||
|
expect(calculator.evaluate('foo * 2', 'foo' => 4)).to eq(8)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts digits in identifiers' do
|
||||||
|
expect(calculator.evaluate('foo1 * 2', foo1: 2)).to eq(4)
|
||||||
|
expect(calculator.evaluate('foo1 * 2', 'foo1' => 4)).to eq(8)
|
||||||
|
expect(calculator.evaluate('1foo * 2', '1foo' => 2)).to eq(4)
|
||||||
|
expect(calculator.evaluate('fo1o * 2', fo1o: 4)).to eq(8)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'compares string literals with string variables' do
|
||||||
|
expect(calculator.evaluate('fruit = "apple"', fruit: 'apple')).to be_truthy
|
||||||
|
expect(calculator.evaluate('fruit = "apple"', fruit: 'pear')).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'performs case-sensitive comparison' do
|
||||||
|
expect(calculator.evaluate('fruit = "Apple"', fruit: 'apple')).to be_falsey
|
||||||
|
expect(calculator.evaluate('fruit = "Apple"', fruit: 'Apple')).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows binding logical values' do
|
||||||
|
expect(calculator.evaluate('some_boolean AND 7 > 5', some_boolean: true)).to be_truthy
|
||||||
|
expect(calculator.evaluate('some_boolean AND 7 < 5', some_boolean: true)).to be_falsey
|
||||||
|
expect(calculator.evaluate('some_boolean AND 7 > 5', some_boolean: false)).to be_falsey
|
||||||
|
|
||||||
|
expect(calculator.evaluate('some_boolean OR 7 > 5', some_boolean: true)).to be_truthy
|
||||||
|
expect(calculator.evaluate('some_boolean OR 7 < 5', some_boolean: true)).to be_truthy
|
||||||
|
expect(calculator.evaluate('some_boolean OR 7 < 5', some_boolean: false)).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'functions' do
|
||||||
|
it 'include IF' do
|
||||||
|
expect(calculator.evaluate('if(foo < 8, 10, 20)', foo: 2)).to eq(10)
|
||||||
|
expect(calculator.evaluate('if(foo < 8, 10, 20)', foo: 9)).to eq(20)
|
||||||
|
expect(calculator.evaluate('if (foo < 8, 10, 20)', foo: 2)).to eq(10)
|
||||||
|
expect(calculator.evaluate('if (foo < 8, 10, 20)', foo: 9)).to eq(20)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'include ROUND' do
|
||||||
|
expect(calculator.evaluate('round(8.2)')).to eq(8)
|
||||||
|
expect(calculator.evaluate('round(8.8)')).to eq(9)
|
||||||
|
expect(calculator.evaluate('round(8.75, 1)')).to eq(BigDecimal.new('8.8'))
|
||||||
|
|
||||||
|
expect(calculator.evaluate('ROUND(apples * 0.93)', { apples: 10 })).to eq(9)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'include NOT' do
|
||||||
|
expect(calculator.evaluate('NOT(some_boolean)', some_boolean: true)).to be_falsey
|
||||||
|
expect(calculator.evaluate('NOT(some_boolean)', some_boolean: false)).to be_truthy
|
||||||
|
|
||||||
|
expect(calculator.evaluate('NOT(some_boolean) AND 7 > 5', some_boolean: true)).to be_falsey
|
||||||
|
expect(calculator.evaluate('NOT(some_boolean) OR 7 < 5', some_boolean: false)).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'evaluates functions with negative numbers' do
|
||||||
|
expect(calculator.evaluate('if (-1 < 5, -1, 5)')).to eq(-1)
|
||||||
|
expect(calculator.evaluate('if (-1 = -1, -1, 5)')).to eq(-1)
|
||||||
|
expect(calculator.evaluate('round(-1.23, 1)')).to eq(BigDecimal.new('-1.2'))
|
||||||
|
expect(calculator.evaluate('NOT(some_boolean) AND -1 > 3', some_boolean: true)).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'evaluates functions with stored variables' do
|
||||||
|
calculator.store("multi_color" => true, "number_of_sheets" => 5000, "sheets_per_minute_black" => 2000, "sheets_per_minute_color" => 1000)
|
||||||
|
result = calculator.evaluate('number_of_sheets / if(multi_color, sheets_per_minute_color, sheets_per_minute_black)')
|
||||||
|
expect(result).to eq(5)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'roundup' do
|
||||||
|
it 'should work with one argument' do
|
||||||
|
expect(calculator.evaluate('roundup(1.234)')).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should accept second precision argument like in Office formula' do
|
||||||
|
expect(calculator.evaluate('roundup(1.234, 2)')).to eq(1.24)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'rounddown' do
|
||||||
|
it 'should work with one argument' do
|
||||||
|
expect(calculator.evaluate('rounddown(1.234)')).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should accept second precision argument like in Office formula' do
|
||||||
|
expect(calculator.evaluate('rounddown(1.234, 2)')).to eq(1.23)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'explicit NULL' do
|
||||||
|
it 'can be used in IF statements' do
|
||||||
|
expect(calculator.evaluate('IF(null, 1, 2)')).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can be used in IF statements when passed in' do
|
||||||
|
expect(calculator.evaluate('IF(foo, 1, 2)', foo: nil)).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'nil values are carried across middle terms' do
|
||||||
|
results = calculator.solve!(
|
||||||
|
choice: 'IF(bar, 1, 2)',
|
||||||
|
bar: 'foo',
|
||||||
|
foo: nil)
|
||||||
|
expect(results).to eq(
|
||||||
|
choice: 2,
|
||||||
|
bar: nil,
|
||||||
|
foo: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises errors when used in arithmetic operation' do
|
||||||
|
expect {
|
||||||
|
calculator.solve!(more_apples: "apples + 1", apples: nil)
|
||||||
|
}.to raise_error(Dentaku::ArgumentError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'case statements' do
|
||||||
|
it 'handles complex then statements' do
|
||||||
|
formula = <<-FORMULA
|
||||||
|
CASE fruit
|
||||||
|
WHEN 'apple'
|
||||||
|
THEN (1 * quantity)
|
||||||
|
WHEN 'banana'
|
||||||
|
THEN (2 * quantity)
|
||||||
|
END
|
||||||
|
FORMULA
|
||||||
|
expect(calculator.evaluate(formula, quantity: 3, fruit: 'apple')).to eq(3)
|
||||||
|
expect(calculator.evaluate(formula, quantity: 3, fruit: 'banana')).to eq(6)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles complex when statements' do
|
||||||
|
formula = <<-FORMULA
|
||||||
|
CASE number
|
||||||
|
WHEN (2 * 2)
|
||||||
|
THEN 1
|
||||||
|
WHEN (2 * 3)
|
||||||
|
THEN 2
|
||||||
|
END
|
||||||
|
FORMULA
|
||||||
|
expect(calculator.evaluate(formula, number: 4)).to eq(1)
|
||||||
|
expect(calculator.evaluate(formula, number: 6)).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'throws an exception when no match and there is no default value' do
|
||||||
|
formula = <<-FORMULA
|
||||||
|
CASE number
|
||||||
|
WHEN 42
|
||||||
|
THEN 1
|
||||||
|
END
|
||||||
|
FORMULA
|
||||||
|
expect { calculator.evaluate(formula, number: 2) }
|
||||||
|
.to raise_error("No block matched the switch value '2'")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles a default else statement' do
|
||||||
|
formula = <<-FORMULA
|
||||||
|
CASE fruit
|
||||||
|
WHEN 'apple'
|
||||||
|
THEN 1 * quantity
|
||||||
|
WHEN 'banana'
|
||||||
|
THEN 2 * quantity
|
||||||
|
ELSE
|
||||||
|
3 * quantity
|
||||||
|
END
|
||||||
|
FORMULA
|
||||||
|
expect(calculator.evaluate(formula, quantity: 1, fruit: 'banana')).to eq(2)
|
||||||
|
expect(calculator.evaluate(formula, quantity: 1, fruit: 'orange')).to eq(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles nested case statements' do
|
||||||
|
formula = <<-FORMULA
|
||||||
|
CASE fruit
|
||||||
|
WHEN 'apple'
|
||||||
|
THEN 1 * quantity
|
||||||
|
WHEN 'banana'
|
||||||
|
THEN
|
||||||
|
CASE quantity
|
||||||
|
WHEN 1 THEN 2
|
||||||
|
WHEN 10 THEN
|
||||||
|
CASE type
|
||||||
|
WHEN 'organic' THEN 5
|
||||||
|
END
|
||||||
|
END
|
||||||
|
END
|
||||||
|
FORMULA
|
||||||
|
value = calculator.evaluate(
|
||||||
|
formula,
|
||||||
|
type: 'organic',
|
||||||
|
quantity: 10,
|
||||||
|
fruit: 'banana')
|
||||||
|
expect(value).to eq(5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'math functions' do
|
||||||
|
Math.methods(false).each do |method|
|
||||||
|
it method do
|
||||||
|
if Math.method(method).arity == 2
|
||||||
|
expect(calculator.evaluate("#{method}(1,2)")).to eq Math.send(method, 1, 2)
|
||||||
|
else
|
||||||
|
expect(calculator.evaluate("#{method}(1)")).to eq Math.send(method, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'disable_cache' do
|
||||||
|
before do
|
||||||
|
allow(Dentaku).to receive(:cache_ast?) { true }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'disables the AST cache' do
|
||||||
|
expect(calculator.disable_cache{ |c| c.cache_ast? }).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calculates normally' do
|
||||||
|
expect(calculator.disable_cache{ |c| c.evaluate("2 + 2") }).to eq(4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'clear_cache' do
|
||||||
|
before do
|
||||||
|
allow(Dentaku).to receive(:cache_ast?) { true }
|
||||||
|
|
||||||
|
calculator.ast("1+1")
|
||||||
|
calculator.ast("pineapples * 5")
|
||||||
|
calculator.ast("pi * radius ^ 2")
|
||||||
|
|
||||||
|
def calculator.ast_cache
|
||||||
|
@ast_cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'clears all items from cache' do
|
||||||
|
expect(calculator.ast_cache.length).to eq 3
|
||||||
|
calculator.clear_cache
|
||||||
|
expect(calculator.ast_cache.keys).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'clears one item from cache' do
|
||||||
|
calculator.clear_cache("1+1")
|
||||||
|
expect(calculator.ast_cache.keys.sort).to eq([
|
||||||
|
'pi * radius ^ 2',
|
||||||
|
'pineapples * 5',
|
||||||
|
])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'clears items matching regex from cache' do
|
||||||
|
calculator.clear_cache(/^pi/)
|
||||||
|
expect(calculator.ast_cache.keys.sort).to eq(['1+1'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'string functions' do
|
||||||
|
it 'concatenates two strings' do
|
||||||
|
expect(
|
||||||
|
calculator.evaluate('CONCAT(s1, s2)', 's1' => 'abc', 's2' => 'def')
|
||||||
|
).to eq 'abcdef'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,22 @@
|
||||||
|
require 'dentaku'
|
||||||
|
|
||||||
|
describe Dentaku do
|
||||||
|
it 'evaulates an expression' do
|
||||||
|
expect(Dentaku('5+3')).to eql(8)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'binds values to variables' do
|
||||||
|
expect(Dentaku('oranges > 7', oranges: 10)).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'evaulates a nested function' do
|
||||||
|
expect(Dentaku('roundup(roundup(3 * cherries) + raspberries)', cherries: 1.5, raspberries: 0.9)).to eql(6)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'treats variables as case-insensitive' do
|
||||||
|
expect(Dentaku('40 + N', 'n' => 2)).to eql(42)
|
||||||
|
expect(Dentaku('40 + N', 'N' => 2)).to eql(42)
|
||||||
|
expect(Dentaku('40 + n', 'N' => 2)).to eql(42)
|
||||||
|
expect(Dentaku('40 + n', 'n' => 2)).to eql(42)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/exceptions'
|
||||||
|
|
||||||
|
describe Dentaku::UnboundVariableError do
|
||||||
|
it 'includes variable name(s) in message' do
|
||||||
|
exception = described_class.new(['length'])
|
||||||
|
expect(exception.message).to match /length/
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,56 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/calculator'
|
||||||
|
|
||||||
|
describe Dentaku::Calculator do
|
||||||
|
describe 'functions' do
|
||||||
|
describe 'external functions' do
|
||||||
|
|
||||||
|
let(:with_external_funcs) do
|
||||||
|
c = described_class.new
|
||||||
|
|
||||||
|
c.add_function(:now, :string, -> { Time.now.to_s })
|
||||||
|
|
||||||
|
fns = [
|
||||||
|
[:pow, :numeric, ->(mantissa, exponent) { mantissa ** exponent }],
|
||||||
|
[:biggest, :numeric, ->(*args) { args.max }],
|
||||||
|
[:smallest, :numeric, ->(*args) { args.min }],
|
||||||
|
]
|
||||||
|
|
||||||
|
c.add_functions(fns)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes NOW' do
|
||||||
|
now = with_external_funcs.evaluate('NOW()')
|
||||||
|
expect(now).not_to be_nil
|
||||||
|
expect(now).not_to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes POW' do
|
||||||
|
expect(with_external_funcs.evaluate('POW(2,3)')).to eq(8)
|
||||||
|
expect(with_external_funcs.evaluate('POW(3,2)')).to eq(9)
|
||||||
|
expect(with_external_funcs.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes BIGGEST' do
|
||||||
|
expect(with_external_funcs.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes SMALLEST' do
|
||||||
|
expect(with_external_funcs.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'supports array parameters' do
|
||||||
|
calculator = described_class.new
|
||||||
|
calculator.add_function(
|
||||||
|
:includes,
|
||||||
|
:logical,
|
||||||
|
->(haystack, needle) {
|
||||||
|
haystack.include?(needle)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(calculator.evaluate("INCLUDES(list, 2)", list: [1,2,3])).to eq(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,150 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/token'
|
||||||
|
require 'dentaku/parser'
|
||||||
|
|
||||||
|
describe Dentaku::Parser do
|
||||||
|
it 'is constructed from a token' do
|
||||||
|
token = Dentaku::Token.new(:numeric, 5)
|
||||||
|
node = described_class.new([token]).parse
|
||||||
|
expect(node.value).to eq 5
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'performs simple addition' do
|
||||||
|
five = Dentaku::Token.new(:numeric, 5)
|
||||||
|
plus = Dentaku::Token.new(:operator, :add)
|
||||||
|
four = Dentaku::Token.new(:numeric, 4)
|
||||||
|
|
||||||
|
node = described_class.new([five, plus, four]).parse
|
||||||
|
expect(node.value).to eq 9
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'compares two numbers' do
|
||||||
|
five = Dentaku::Token.new(:numeric, 5)
|
||||||
|
lt = Dentaku::Token.new(:comparator, :lt)
|
||||||
|
four = Dentaku::Token.new(:numeric, 4)
|
||||||
|
|
||||||
|
node = described_class.new([five, lt, four]).parse
|
||||||
|
expect(node.value).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calculates unary percentage' do
|
||||||
|
five = Dentaku::Token.new(:numeric, 5)
|
||||||
|
mod = Dentaku::Token.new(:operator, :mod)
|
||||||
|
|
||||||
|
node = described_class.new([five, mod]).parse
|
||||||
|
expect(node.value).to eq 0.05
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'performs multiple operations in one stream' do
|
||||||
|
five = Dentaku::Token.new(:numeric, 5)
|
||||||
|
plus = Dentaku::Token.new(:operator, :add)
|
||||||
|
four = Dentaku::Token.new(:numeric, 4)
|
||||||
|
times = Dentaku::Token.new(:operator, :multiply)
|
||||||
|
three = Dentaku::Token.new(:numeric, 3)
|
||||||
|
|
||||||
|
node = described_class.new([five, plus, four, times, three]).parse
|
||||||
|
expect(node.value).to eq 17
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'respects order of operations' do
|
||||||
|
five = Dentaku::Token.new(:numeric, 5)
|
||||||
|
times = Dentaku::Token.new(:operator, :multiply)
|
||||||
|
four = Dentaku::Token.new(:numeric, 4)
|
||||||
|
plus = Dentaku::Token.new(:operator, :add)
|
||||||
|
three = Dentaku::Token.new(:numeric, 3)
|
||||||
|
|
||||||
|
node = described_class.new([five, times, four, plus, three]).parse
|
||||||
|
expect(node.value).to eq 23
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'respects grouping by parenthesis' do
|
||||||
|
lpar = Dentaku::Token.new(:grouping, :open)
|
||||||
|
five = Dentaku::Token.new(:numeric, 5)
|
||||||
|
plus = Dentaku::Token.new(:operator, :add)
|
||||||
|
four = Dentaku::Token.new(:numeric, 4)
|
||||||
|
rpar = Dentaku::Token.new(:grouping, :close)
|
||||||
|
times = Dentaku::Token.new(:operator, :multiply)
|
||||||
|
three = Dentaku::Token.new(:numeric, 3)
|
||||||
|
|
||||||
|
node = described_class.new([lpar, five, plus, four, rpar, times, three]).parse
|
||||||
|
expect(node.value).to eq 27
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'evaluates functions' do
|
||||||
|
fn = Dentaku::Token.new(:function, :if)
|
||||||
|
fopen = Dentaku::Token.new(:grouping, :open)
|
||||||
|
five = Dentaku::Token.new(:numeric, 5)
|
||||||
|
lt = Dentaku::Token.new(:comparator, :lt)
|
||||||
|
four = Dentaku::Token.new(:numeric, 4)
|
||||||
|
comma = Dentaku::Token.new(:grouping, :comma)
|
||||||
|
three = Dentaku::Token.new(:numeric, 3)
|
||||||
|
two = Dentaku::Token.new(:numeric, 2)
|
||||||
|
rpar = Dentaku::Token.new(:grouping, :close)
|
||||||
|
|
||||||
|
node = described_class.new([fn, fopen, five, lt, four, comma, three, comma, two, rpar]).parse
|
||||||
|
expect(node.value).to eq 2
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'represents formulas with variables' do
|
||||||
|
five = Dentaku::Token.new(:numeric, 5)
|
||||||
|
times = Dentaku::Token.new(:operator, :multiply)
|
||||||
|
x = Dentaku::Token.new(:identifier, :x)
|
||||||
|
|
||||||
|
node = described_class.new([five, times, x]).parse
|
||||||
|
expect { node.value }.to raise_error(Dentaku::UnboundVariableError)
|
||||||
|
expect(node.value(x: 3)).to eq 15
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'evaluates boolean expressions' do
|
||||||
|
d_true = Dentaku::Token.new(:logical, true)
|
||||||
|
d_and = Dentaku::Token.new(:combinator, :and)
|
||||||
|
d_false = Dentaku::Token.new(:logical, false)
|
||||||
|
|
||||||
|
node = described_class.new([d_true, d_and, d_false]).parse
|
||||||
|
expect(node.value).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'evaluates a case statement' do
|
||||||
|
case_start = Dentaku::Token.new(:case, :open)
|
||||||
|
x = Dentaku::Token.new(:identifier, :x)
|
||||||
|
case_when1 = Dentaku::Token.new(:case, :when)
|
||||||
|
one = Dentaku::Token.new(:numeric, 1)
|
||||||
|
case_then1 = Dentaku::Token.new(:case, :then)
|
||||||
|
two = Dentaku::Token.new(:numeric, 2)
|
||||||
|
case_when2 = Dentaku::Token.new(:case, :when)
|
||||||
|
three = Dentaku::Token.new(:numeric, 3)
|
||||||
|
case_then2 = Dentaku::Token.new(:case, :then)
|
||||||
|
four = Dentaku::Token.new(:numeric, 4)
|
||||||
|
case_close = Dentaku::Token.new(:case, :close)
|
||||||
|
|
||||||
|
node = described_class.new(
|
||||||
|
[case_start,
|
||||||
|
x,
|
||||||
|
case_when1,
|
||||||
|
one,
|
||||||
|
case_then1,
|
||||||
|
two,
|
||||||
|
case_when2,
|
||||||
|
three,
|
||||||
|
case_then2,
|
||||||
|
four,
|
||||||
|
case_close]).parse
|
||||||
|
expect(node.value(x: 3)).to eq(4)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an error on parse failure' do
|
||||||
|
five = Dentaku::Token.new(:numeric, 5)
|
||||||
|
times = Dentaku::Token.new(:operator, :multiply)
|
||||||
|
minus = Dentaku::Token.new(:operator, :subtract)
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.new([five, times, minus]).parse
|
||||||
|
}.to raise_error(Dentaku::ParseError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "evaluates explicit 'NULL' as a Nil" do
|
||||||
|
null = Dentaku::Token.new(:null, nil)
|
||||||
|
node = described_class.new([null]).parse
|
||||||
|
expect(node.value).to eq(nil)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,41 @@
|
||||||
|
require 'pry'
|
||||||
|
|
||||||
|
# automatically create a token stream from bare values
|
||||||
|
def token_stream(*args)
|
||||||
|
args.map do |value|
|
||||||
|
type = type_for(value)
|
||||||
|
Dentaku::Token.new(type, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# make a (hopefully intelligent) guess about type
|
||||||
|
def type_for(value)
|
||||||
|
case value
|
||||||
|
when Numeric
|
||||||
|
:numeric
|
||||||
|
when String
|
||||||
|
:string
|
||||||
|
when true, false
|
||||||
|
:logical
|
||||||
|
when :add, :subtract, :multiply, :divide, :mod, :pow
|
||||||
|
:operator
|
||||||
|
when :open, :close, :comma
|
||||||
|
:grouping
|
||||||
|
when :le, :ge, :ne, :ne, :lt, :gt, :eq
|
||||||
|
:comparator
|
||||||
|
when :and, :or
|
||||||
|
:combinator
|
||||||
|
when :if, :round, :roundup, :rounddown, :not
|
||||||
|
:function
|
||||||
|
else
|
||||||
|
:identifier
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def identifier(name)
|
||||||
|
Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, name))
|
||||||
|
end
|
||||||
|
|
||||||
|
def literal(value)
|
||||||
|
Dentaku::AST::Literal.new(Dentaku::Token.new(type_for(value), value))
|
||||||
|
end
|
|
@ -0,0 +1,135 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'dentaku/token_matcher'
|
||||||
|
|
||||||
|
describe Dentaku::TokenMatcher do
|
||||||
|
it 'with single category matches token category' do
|
||||||
|
matcher = described_class.new(:numeric)
|
||||||
|
token = Dentaku::Token.new(:numeric, 5)
|
||||||
|
|
||||||
|
expect(matcher).to eq(token)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'with multiple categories matches any included token category' do
|
||||||
|
matcher = described_class.new([:comparator, :operator])
|
||||||
|
numeric = Dentaku::Token.new(:numeric, 5)
|
||||||
|
comparator = Dentaku::Token.new(:comparator, :lt)
|
||||||
|
operator = Dentaku::Token.new(:operator, :add)
|
||||||
|
|
||||||
|
expect(matcher).to eq(comparator)
|
||||||
|
expect(matcher).to eq(operator)
|
||||||
|
expect(matcher).not_to eq(numeric)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'with single category and value matches token category and value' do
|
||||||
|
matcher = described_class.new(:operator, :add)
|
||||||
|
addition = Dentaku::Token.new(:operator, :add)
|
||||||
|
subtraction = Dentaku::Token.new(:operator, :subtract)
|
||||||
|
|
||||||
|
expect(matcher).to eq(addition)
|
||||||
|
expect(matcher).not_to eq(subtraction)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'with multiple values matches any included token value' do
|
||||||
|
matcher = described_class.new(:operator, [:add, :subtract])
|
||||||
|
add = Dentaku::Token.new(:operator, :add)
|
||||||
|
sub = Dentaku::Token.new(:operator, :subtract)
|
||||||
|
mul = Dentaku::Token.new(:operator, :multiply)
|
||||||
|
div = Dentaku::Token.new(:operator, :divide)
|
||||||
|
|
||||||
|
expect(matcher).to eq(add)
|
||||||
|
expect(matcher).to eq(sub)
|
||||||
|
expect(matcher).not_to eq(mul)
|
||||||
|
expect(matcher).not_to eq(div)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is invertible' do
|
||||||
|
matcher = described_class.new(:operator, [:add, :subtract]).invert
|
||||||
|
add = Dentaku::Token.new(:operator, :add)
|
||||||
|
mul = Dentaku::Token.new(:operator, :multiply)
|
||||||
|
cmp = Dentaku::Token.new(:comparator, :lt)
|
||||||
|
|
||||||
|
expect(matcher).not_to eq(add)
|
||||||
|
expect(matcher).to eq(mul)
|
||||||
|
expect(matcher).to eq(cmp)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'combining multiple tokens' do
|
||||||
|
let(:numeric) { described_class.new(:numeric) }
|
||||||
|
let(:string) { described_class.new(:string) }
|
||||||
|
|
||||||
|
it 'matches either' do
|
||||||
|
either = numeric | string
|
||||||
|
expect(either).to eq(Dentaku::Token.new(:numeric, 5))
|
||||||
|
expect(either).to eq(Dentaku::Token.new(:string, 'rhubarb'))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches any value' do
|
||||||
|
value = described_class.value
|
||||||
|
expect(value).to eq(Dentaku::Token.new(:numeric, 8))
|
||||||
|
expect(value).to eq(Dentaku::Token.new(:string, 'apricot'))
|
||||||
|
expect(value).to eq(Dentaku::Token.new(:logical, false))
|
||||||
|
expect(value).not_to eq(Dentaku::Token.new(:function, :round))
|
||||||
|
expect(value).not_to eq(Dentaku::Token.new(:identifier, :hello))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'stream matching' do
|
||||||
|
let(:stream) { token_stream(5, 11, 9, 24, :hello, 8) }
|
||||||
|
|
||||||
|
describe 'standard' do
|
||||||
|
let(:standard) { described_class.new(:numeric) }
|
||||||
|
|
||||||
|
it 'matches zero or more occurrences in a token stream' do
|
||||||
|
matched, substream = standard.match(stream)
|
||||||
|
expect(matched).to be_truthy
|
||||||
|
expect(substream.length).to eq 1
|
||||||
|
expect(substream.map(&:value)).to eq [5]
|
||||||
|
|
||||||
|
matched, substream = standard.match(stream, 4)
|
||||||
|
expect(substream).to be_empty
|
||||||
|
expect(matched).not_to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'star' do
|
||||||
|
let(:star) { described_class.new(:numeric).star }
|
||||||
|
|
||||||
|
it 'matches zero or more occurrences in a token stream' do
|
||||||
|
matched, substream = star.match(stream)
|
||||||
|
expect(matched).to be_truthy
|
||||||
|
expect(substream.length).to eq 4
|
||||||
|
expect(substream.map(&:value)).to eq [5, 11, 9, 24]
|
||||||
|
|
||||||
|
matched, substream = star.match(stream, 4)
|
||||||
|
expect(substream).to be_empty
|
||||||
|
expect(matched).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'plus' do
|
||||||
|
let(:plus) { described_class.new(:numeric).plus }
|
||||||
|
|
||||||
|
it 'matches one or more occurrences in a token stream' do
|
||||||
|
matched, substream = plus.match(stream)
|
||||||
|
expect(matched).to be_truthy
|
||||||
|
expect(substream.length).to eq 4
|
||||||
|
expect(substream.map(&:value)).to eq [5, 11, 9, 24]
|
||||||
|
|
||||||
|
matched, substream = plus.match(stream, 4)
|
||||||
|
expect(substream).to be_empty
|
||||||
|
expect(matched).not_to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'arguments' do
|
||||||
|
it 'matches comma-separated values' do
|
||||||
|
stream = token_stream(1, :comma, 2, :comma, true, :comma, 'olive', :comma, :'(')
|
||||||
|
matched, substream = described_class.arguments.match(stream)
|
||||||
|
expect(matched).to be_truthy
|
||||||
|
expect(substream.length).to eq 8
|
||||||
|
expect(substream.map(&:value)).to eq [1, :comma, 2, :comma, true, :comma, 'olive', :comma]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
require 'dentaku/token_scanner'
|
||||||
|
|
||||||
|
describe Dentaku::TokenScanner do
|
||||||
|
let(:whitespace) { described_class.new(:whitespace, '\s') }
|
||||||
|
let(:numeric) { described_class.new(:numeric, '(\d+(\.\d+)?|\.\d+)',
|
||||||
|
->(raw) { raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i })
|
||||||
|
}
|
||||||
|
let(:custom) { described_class.new(:identifier, '#\w+\b',
|
||||||
|
->(raw) { raw.gsub('#', '').to_sym })
|
||||||
|
}
|
||||||
|
|
||||||
|
after { described_class.register_default_scanners }
|
||||||
|
|
||||||
|
it 'returns a token for a matching string' do
|
||||||
|
token = whitespace.scan(' ').first
|
||||||
|
expect(token.category).to eq(:whitespace)
|
||||||
|
expect(token.value).to eq(' ')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns falsy for a non-matching string' do
|
||||||
|
expect(whitespace.scan('A')).not_to be
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'performs raw value conversion' do
|
||||||
|
token = numeric.scan('5').first
|
||||||
|
expect(token.category).to eq(:numeric)
|
||||||
|
expect(token.value).to eq(5)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a list of all configured scanners' do
|
||||||
|
expect(described_class.scanners.length).to eq 14
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows customizing available scanners' do
|
||||||
|
described_class.scanners = [:whitespace, :numeric]
|
||||||
|
expect(described_class.scanners.length).to eq 2
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid scanners' do
|
||||||
|
described_class.scanners = [:whitespace, :numeric, :fake]
|
||||||
|
expect(described_class.scanners.length).to eq 2
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses a custom scanner' do
|
||||||
|
described_class.scanners = [:whitespace, :numeric]
|
||||||
|
described_class.register_scanner(:custom, custom)
|
||||||
|
expect(described_class.scanners.length).to eq 3
|
||||||
|
|
||||||
|
token = custom.scan('#apple + #pear').first
|
||||||
|
expect(token.category).to eq(:identifier)
|
||||||
|
expect(token.value).to eq(:apple)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,10 @@
|
||||||
|
require 'dentaku/token'
|
||||||
|
|
||||||
|
describe Dentaku::Token do
|
||||||
|
it 'has a category and a value' do
|
||||||
|
token = Dentaku::Token.new(:numeric, 5)
|
||||||
|
expect(token.category).to eq(:numeric)
|
||||||
|
expect(token.value).to eq(5)
|
||||||
|
expect(token.is?(:numeric)).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,212 @@
|
||||||
|
require 'dentaku/tokenizer'
|
||||||
|
|
||||||
|
describe Dentaku::Tokenizer do
|
||||||
|
let(:tokenizer) { described_class.new }
|
||||||
|
|
||||||
|
it 'handles an empty expression' do
|
||||||
|
expect(tokenizer.tokenize('')).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tokenizes addition' do
|
||||||
|
tokens = tokenizer.tokenize('1+1')
|
||||||
|
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq([1, :add, 1])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tokenizes unary minus' do
|
||||||
|
tokens = tokenizer.tokenize('-5')
|
||||||
|
expect(tokens.map(&:category)).to eq([:operator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq([:negate, 5])
|
||||||
|
|
||||||
|
tokens = tokenizer.tokenize('(-5)')
|
||||||
|
expect(tokens.map(&:category)).to eq([:grouping, :operator, :numeric, :grouping])
|
||||||
|
expect(tokens.map(&:value)).to eq([:open, :negate, 5, :close])
|
||||||
|
|
||||||
|
tokens = tokenizer.tokenize('if(-5 > x, -7, -8) - 9')
|
||||||
|
expect(tokens.map(&:category)).to eq([
|
||||||
|
:function, :grouping, # if(
|
||||||
|
:operator, :numeric, :comparator, :identifier, :grouping, # -5 > x,
|
||||||
|
:operator, :numeric, :grouping, # -7,
|
||||||
|
:operator, :numeric, :grouping, # -8)
|
||||||
|
:operator, :numeric # - 9
|
||||||
|
])
|
||||||
|
expect(tokens.map(&:value)).to eq([
|
||||||
|
:if, :open, # if(
|
||||||
|
:negate, 5, :gt, 'x', :comma, # -5 > x,
|
||||||
|
:negate, 7, :comma, # -7,
|
||||||
|
:negate, 8, :close, # -8)
|
||||||
|
:subtract, 9 # - 9
|
||||||
|
])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tokenizes comparison with =' do
|
||||||
|
tokens = tokenizer.tokenize('number = 5')
|
||||||
|
expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq(['number', :eq, 5])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tokenizes comparison with =' do
|
||||||
|
tokens = tokenizer.tokenize('number = 5')
|
||||||
|
expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq(['number', :eq, 5])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tokenizes comparison with alternate ==' do
|
||||||
|
tokens = tokenizer.tokenize('number == 5')
|
||||||
|
expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq(['number', :eq, 5])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores whitespace' do
|
||||||
|
tokens = tokenizer.tokenize('1 / 1 ')
|
||||||
|
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq([1, :divide, 1])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tokenizes power operations' do
|
||||||
|
tokens = tokenizer.tokenize('10 ^ 2')
|
||||||
|
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq([10, :pow, 2])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tokenizes power operations' do
|
||||||
|
tokens = tokenizer.tokenize('0 * 10 ^ -5')
|
||||||
|
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric, :operator, :operator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq([0, :multiply, 10, :pow, :negate, 5])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles floating point' do
|
||||||
|
tokens = tokenizer.tokenize('1.5 * 3.7')
|
||||||
|
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq([1.5, :multiply, 3.7])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not require leading zero' do
|
||||||
|
tokens = tokenizer.tokenize('.5 * 3.7')
|
||||||
|
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq([0.5, :multiply, 3.7])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts arbitrary identifiers' do
|
||||||
|
tokens = tokenizer.tokenize('sea_monkeys > 1500')
|
||||||
|
expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq(['sea_monkeys', :gt, 1500])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'recognizes double-quoted strings' do
|
||||||
|
tokens = tokenizer.tokenize('animal = "giraffe"')
|
||||||
|
expect(tokens.map(&:category)).to eq([:identifier, :comparator, :string])
|
||||||
|
expect(tokens.map(&:value)).to eq(['animal', :eq, 'giraffe'])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'recognizes single-quoted strings' do
|
||||||
|
tokens = tokenizer.tokenize("animal = 'giraffe'")
|
||||||
|
expect(tokens.map(&:category)).to eq([:identifier, :comparator, :string])
|
||||||
|
expect(tokens.map(&:value)).to eq(['animal', :eq, 'giraffe'])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'recognizes binary minus operator' do
|
||||||
|
tokens = tokenizer.tokenize('2 - 3')
|
||||||
|
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq([2, :subtract, 3])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'recognizes unary minus operator' do
|
||||||
|
tokens = tokenizer.tokenize('-2 + 3')
|
||||||
|
expect(tokens.map(&:category)).to eq([:operator, :numeric, :operator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq([:negate, 2, :add, 3])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'recognizes unary minus operator' do
|
||||||
|
tokens = tokenizer.tokenize('2 - -3')
|
||||||
|
expect(tokens.map(&:category)).to eq([:numeric, :operator, :operator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq([2, :subtract, :negate, 3])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches "<=" before "<"' do
|
||||||
|
tokens = tokenizer.tokenize('perimeter <= 7500')
|
||||||
|
expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq(['perimeter', :le, 7500])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches "and" for logical expressions' do
|
||||||
|
tokens = tokenizer.tokenize('octopi <= 7500 AND sharks > 1500')
|
||||||
|
expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric, :combinator, :identifier, :comparator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq(['octopi', :le, 7500, :and, 'sharks', :gt, 1500])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches "or" for logical expressions' do
|
||||||
|
tokens = tokenizer.tokenize('size < 3 or admin = 1')
|
||||||
|
expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric, :combinator, :identifier, :comparator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq(['size', :lt, 3, :or, 'admin', :eq, 1])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'detects unbalanced parentheses' do
|
||||||
|
expect { tokenizer.tokenize('(5+3') }.to raise_error(Dentaku::TokenizerError, /too many opening parentheses/)
|
||||||
|
expect { tokenizer.tokenize(')') }.to raise_error(Dentaku::TokenizerError, /too many closing parentheses/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'recognizes identifiers that share initial substrings with combinators' do
|
||||||
|
tokens = tokenizer.tokenize('andover < 10')
|
||||||
|
expect(tokens.length).to eq(3)
|
||||||
|
expect(tokens.map(&:category)).to eq([:identifier, :comparator, :numeric])
|
||||||
|
expect(tokens.map(&:value)).to eq(['andover', :lt, 10])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tokenizes TRUE and FALSE literals' do
|
||||||
|
tokens = tokenizer.tokenize('true and false')
|
||||||
|
expect(tokens.length).to eq(3)
|
||||||
|
expect(tokens.map(&:category)).to eq([:logical, :combinator, :logical])
|
||||||
|
expect(tokens.map(&:value)).to eq([true, :and, false])
|
||||||
|
|
||||||
|
tokens = tokenizer.tokenize('true_lies and falsehoods')
|
||||||
|
expect(tokens.length).to eq(3)
|
||||||
|
expect(tokens.map(&:category)).to eq([:identifier, :combinator, :identifier])
|
||||||
|
expect(tokens.map(&:value)).to eq(['true_lies', :and, 'falsehoods'])
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'functions' do
|
||||||
|
it 'include IF' do
|
||||||
|
tokens = tokenizer.tokenize('if(x < 10, y, z)')
|
||||||
|
expect(tokens.length).to eq(10)
|
||||||
|
expect(tokens.map(&:category)).to eq([:function, :grouping, :identifier, :comparator, :numeric, :grouping, :identifier, :grouping, :identifier, :grouping])
|
||||||
|
expect(tokens.map(&:value)).to eq([:if, :open, 'x', :lt, 10, :comma, 'y', :comma, 'z', :close])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'include ROUND/UP/DOWN' do
|
||||||
|
tokens = tokenizer.tokenize('round(8.2)')
|
||||||
|
expect(tokens.length).to eq(4)
|
||||||
|
expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :grouping])
|
||||||
|
expect(tokens.map(&:value)).to eq([:round, :open, BigDecimal.new('8.2'), :close])
|
||||||
|
|
||||||
|
tokens = tokenizer.tokenize('round(8.75, 1)')
|
||||||
|
expect(tokens.length).to eq(6)
|
||||||
|
expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :grouping, :numeric, :grouping])
|
||||||
|
expect(tokens.map(&:value)).to eq([:round, :open, BigDecimal.new('8.75'), :comma, 1, :close])
|
||||||
|
|
||||||
|
tokens = tokenizer.tokenize('ROUNDUP(8.2)')
|
||||||
|
expect(tokens.length).to eq(4)
|
||||||
|
expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :grouping])
|
||||||
|
expect(tokens.map(&:value)).to eq([:roundup, :open, BigDecimal.new('8.2'), :close])
|
||||||
|
|
||||||
|
tokens = tokenizer.tokenize('RoundDown(8.2)')
|
||||||
|
expect(tokens.length).to eq(4)
|
||||||
|
expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :grouping])
|
||||||
|
expect(tokens.map(&:value)).to eq([:rounddown, :open, BigDecimal.new('8.2'), :close])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'include NOT' do
|
||||||
|
tokens = tokenizer.tokenize('not(8 < 5)')
|
||||||
|
expect(tokens.length).to eq(6)
|
||||||
|
expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :comparator, :numeric, :grouping])
|
||||||
|
expect(tokens.map(&:value)).to eq([:not, :open, 8, :lt, 5, :close])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles whitespace after function name' do
|
||||||
|
tokens = tokenizer.tokenize('not (8 < 5)')
|
||||||
|
expect(tokens.length).to eq(6)
|
||||||
|
expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :comparator, :numeric, :grouping])
|
||||||
|
expect(tokens.map(&:value)).to eq([:not, :open, 8, :lt, 5, :close])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
23
deployment_scripts/puppet/files/embedded/lib/ruby/gems/2.3.0/gems/influxdb-0.3.13/.gitignore
vendored
Normal file
23
deployment_scripts/puppet/files/embedded/lib/ruby/gems/2.3.0/gems/influxdb-0.3.13/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
*.gem
|
||||||
|
*.rbc
|
||||||
|
.bundle
|
||||||
|
.config
|
||||||
|
coverage
|
||||||
|
InstalledFiles
|
||||||
|
lib/bundler/man
|
||||||
|
pkg
|
||||||
|
rdoc
|
||||||
|
spec/reports
|
||||||
|
test/tmp
|
||||||
|
test/version_tmp
|
||||||
|
tmp
|
||||||
|
Gemfile.lock
|
||||||
|
.rvmrc
|
||||||
|
.ruby-version
|
||||||
|
.ruby-gemset
|
||||||
|
|
||||||
|
# YARD artifacts
|
||||||
|
.yardoc
|
||||||
|
_yardoc
|
||||||
|
doc/
|
||||||
|
*.local
|
|
@ -0,0 +1,35 @@
|
||||||
|
AllCops:
|
||||||
|
Include:
|
||||||
|
- 'Rakefile'
|
||||||
|
- '*.gemspec'
|
||||||
|
- 'lib/**/*.rb'
|
||||||
|
- 'spec/**/*.rb'
|
||||||
|
Exclude:
|
||||||
|
- 'bin/**/*'
|
||||||
|
- 'smoke/**/*'
|
||||||
|
DisplayCopNames: true
|
||||||
|
StyleGuideCopsOnly: false
|
||||||
|
|
||||||
|
Rails:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/FrozenStringLiteralComment:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/NumericPredicate:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/StringLiterals:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/RescueModifier:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Metrics/LineLength:
|
||||||
|
Max: 100
|
||||||
|
Exclude:
|
||||||
|
- 'spec/**/*.rb'
|
||||||
|
|
||||||
|
Metrics/ModuleLength:
|
||||||
|
CountComments: false # count full line comments?
|
||||||
|
Max: 120
|
|
@ -0,0 +1,52 @@
|
||||||
|
sudo: required
|
||||||
|
dist: trusty
|
||||||
|
language: ruby
|
||||||
|
before_install:
|
||||||
|
- gem install bundler
|
||||||
|
- gem update bundler
|
||||||
|
- smoke/provision.sh
|
||||||
|
rvm:
|
||||||
|
- 1.9.3
|
||||||
|
- 2.0.0
|
||||||
|
- 2.1.10
|
||||||
|
- 2.2.4
|
||||||
|
- 2.3.1
|
||||||
|
- ruby-head
|
||||||
|
env:
|
||||||
|
- TEST_TASK=spec
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
allow_failures:
|
||||||
|
- rvm: jruby-head
|
||||||
|
- rvm: ruby-head
|
||||||
|
- rvm: jruby-9.0.5.0
|
||||||
|
- rvm: 2.3.1
|
||||||
|
env: TEST_TASK=smoke influx_version=nightly channel=nightlies
|
||||||
|
include:
|
||||||
|
- rvm: 2.3.1
|
||||||
|
env: TEST_TASK=rubocop
|
||||||
|
- rvm: jruby-9.0.5.0
|
||||||
|
env: JRUBY_OPTS='--client -J-XX:+TieredCompilation -J-XX:TieredStopAtLevel=1 -J-Xss2m -J-Xmx256M'
|
||||||
|
- rvm: jruby-head
|
||||||
|
env: JRUBY_OPTS='--client -J-XX:+TieredCompilation -J-XX:TieredStopAtLevel=1 -J-Xss2m -J-Xmx256M'
|
||||||
|
- rvm: 2.3.1
|
||||||
|
env: TEST_TASK=smoke influx_version=0.10.3-1 pkghash=96244557d9bb7485ddc9d084ff7ce783
|
||||||
|
- rvm: 2.3.1
|
||||||
|
env: TEST_TASK=smoke influx_version=0.11.1-1 pkghash=f4cf8363125038dff038ced6b16bcafd
|
||||||
|
- rvm: 2.3.1
|
||||||
|
env: TEST_TASK=smoke influx_version=0.12.2-1 pkghash=f28bb1c57d52dc1593dca45b86be5913
|
||||||
|
- rvm: 2.3.1
|
||||||
|
env: TEST_TASK=smoke influx_version=0.13.0 pkghash=4f0aa76fee22cf4c18e2a0779ba4f462
|
||||||
|
- rvm: 2.3.1
|
||||||
|
env: TEST_TASK=smoke influx_version=1.0.2 pkghash=3e4c349cb57507913d9abda1459bdbed
|
||||||
|
- rvm: 2.3.1
|
||||||
|
env: TEST_TASK=smoke influx_version=1.1.0 pkghash=682904c350ecfc2a60ec9c6c08453ef2
|
||||||
|
- rvm: 2.3.1
|
||||||
|
env: TEST_TASK=smoke influx_version=nightly channel=nightlies
|
||||||
|
fail_fast: true
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
- haveged
|
||||||
|
- libgmp-dev
|
||||||
|
script: bundle exec rake $TEST_TASK
|
|
@ -0,0 +1,125 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
For the full commit log, [see here](https://github.com/influxdata/influxdb-ruby/commits/master).
|
||||||
|
|
||||||
|
## Unreleased changes
|
||||||
|
|
||||||
|
- None.
|
||||||
|
|
||||||
|
## v0.3.13, released 2016-11-23
|
||||||
|
|
||||||
|
- You can now `InfluxDB::Client#query`, `#write_points`, `#write_point` and
|
||||||
|
`#write` now accept an additional parameter to override the database on
|
||||||
|
invokation time (#173, #176, @jfragoulis).
|
||||||
|
|
||||||
|
|
||||||
|
## v0.3.12, released 2016-11-15
|
||||||
|
|
||||||
|
- Bugfix for broken Unicode support (regression introduced in #169).
|
||||||
|
Please note, this is only properly tested on Ruby 2.1+ (#171).
|
||||||
|
|
||||||
|
## v0.3.11, released 2016-10-12
|
||||||
|
|
||||||
|
- Bugfix/Enhancement in `PointValue#escape`. Input strings are now scrubbed
|
||||||
|
of invalid UTF byte sequences (#169, @ton31337).
|
||||||
|
|
||||||
|
## v0.3.10, released 2016-10-03
|
||||||
|
|
||||||
|
- Bugfix in `Query::Builder#quote` (#168, @cthulhu666).
|
||||||
|
|
||||||
|
## v0.3.9, released 2016-09-20
|
||||||
|
|
||||||
|
- Changed retry behaviour slightly. When the server responds with an incomplete
|
||||||
|
response, we now assume a major server-side problem (insufficient resources,
|
||||||
|
e.g. out-of-memory) and cancel any retry attempts (#165, #166).
|
||||||
|
|
||||||
|
## v0.3.8, released 2016-08-31
|
||||||
|
|
||||||
|
- Added support for named and positional query parameters (#160, @retorquere).
|
||||||
|
|
||||||
|
## v0.3.7, released 2016-08-14
|
||||||
|
|
||||||
|
- Fixed `prefix` handling for `#ping` and `#version` (#157, @dimiii).
|
||||||
|
|
||||||
|
## v0.3.6, released 2016-07-24
|
||||||
|
|
||||||
|
- Added feature for JSON streaming response, via `"chunk_size"` parameter
|
||||||
|
(#155, @mhodson-qxbranch).
|
||||||
|
|
||||||
|
## v0.3.5, released 2016-06-09
|
||||||
|
|
||||||
|
- Reintroduced full dependency on "cause" (for Ruby 1.9 compat).
|
||||||
|
- Extended `Client#create_database` and `#delete_database` to fallback on `config.database` (#153, #154, @anthonator).
|
||||||
|
|
||||||
|
## v0.3.4, released 2016-06-07
|
||||||
|
|
||||||
|
- Added resample options to `Client#create_continuous_query` (#149).
|
||||||
|
- Fixed resample options to be Ruby 1.9 compatible (#150, @SebastianCoetzee).
|
||||||
|
- Mentioned in README, that 0.3.x series is the last one to support Ruby 1.9.
|
||||||
|
|
||||||
|
## v0.3.3, released 2016-06-06 (yanked)
|
||||||
|
|
||||||
|
- Added resample options to `Client#create_continuous_query` (#149).
|
||||||
|
|
||||||
|
## v0.3.2, released 2016-06-02
|
||||||
|
|
||||||
|
- Added config option to authenticate without credentials (#146, @pmenglund).
|
||||||
|
|
||||||
|
## v0.3.1, released 2016-05-26
|
||||||
|
|
||||||
|
- Fixed #130 (again). Integer values are now really written as Integers to InfluxDB.
|
||||||
|
|
||||||
|
## v0.3.0, released 2016-04-24
|
||||||
|
|
||||||
|
- Write queries are now checked against 204 No Content responses, in accordance with the official documentation (#128).
|
||||||
|
- Async options are now configurabe (#107).
|
||||||
|
|
||||||
|
## v0.2.6, released 2016-04-14
|
||||||
|
|
||||||
|
- Empty tag keys/values are now omitted (#124).
|
||||||
|
|
||||||
|
## v0.2.5, released 2016-04-14
|
||||||
|
|
||||||
|
- Async writer now behaves when stopping the client (#73).
|
||||||
|
- Update development dependencies and started enforcing Rubocop styles.
|
||||||
|
|
||||||
|
## v0.2.4, released 2016-04-12
|
||||||
|
|
||||||
|
- Added `InfluxDB::Client#version`, returning the server version (#117).
|
||||||
|
- Fixed escaping issues (#119, #121, #135).
|
||||||
|
- Integer values are now written as Integer, not as Float value (#131).
|
||||||
|
- Return all result series when querying multiple selects (#134).
|
||||||
|
- Made host cycling thread safe (#136).
|
||||||
|
|
||||||
|
## v0.2.3, released 2015-10-27
|
||||||
|
|
||||||
|
- Added `epoch` option to client constructor and write methods (#104).
|
||||||
|
- Added `#list_user_grants` (#111), `#grant_user_admin_privileges` (#112) and `#alter_retention_policy` (#114) methods.
|
||||||
|
|
||||||
|
## v0.2.2, released 2015-07-29
|
||||||
|
|
||||||
|
- Fixed issues with Async client (#101)
|
||||||
|
- Avoid usage of `gsub!` (#102)
|
||||||
|
|
||||||
|
## v0.2.1, released 2015-07-25
|
||||||
|
|
||||||
|
- Fix double quote tags escaping (#98)
|
||||||
|
|
||||||
|
## v0.2.0, released 2015-07-20
|
||||||
|
|
||||||
|
- Large library refactoring (#88, #90)
|
||||||
|
- Extract config from client
|
||||||
|
- Extract HTTP functionality to separate module
|
||||||
|
- Extract InfluxDB management functions to separate modules
|
||||||
|
- Add writer concept
|
||||||
|
- Refactor specs (add cases)
|
||||||
|
- Add 'denormalize' option to config
|
||||||
|
- Recognize SeriesNotFound error
|
||||||
|
- Update README
|
||||||
|
- Add Rubocop config
|
||||||
|
- Break support for Ruby < 2
|
||||||
|
- Added support for InfluxDB 0.9+ (#92)
|
||||||
|
|
||||||
|
## v0.1.9, released 2015-07-04
|
||||||
|
|
||||||
|
- last version to support InfluxDB 0.8.x
|
|
@ -0,0 +1,14 @@
|
||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
if RUBY_ENGINE != "jruby" && RUBY_VERSION < "2.0"
|
||||||
|
gem "json", "~> 1.8.3"
|
||||||
|
gem "public_suffix", "< 1.5"
|
||||||
|
end
|
||||||
|
|
||||||
|
gemspec
|
||||||
|
|
||||||
|
local_gemfile = 'Gemfile.local'
|
||||||
|
|
||||||
|
if File.exist?(local_gemfile)
|
||||||
|
eval(File.read(local_gemfile)) # rubocop:disable Lint/Eval
|
||||||
|
end
|
|
@ -0,0 +1,22 @@
|
||||||
|
Copyright (c) 2013 Todd Persen
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,687 @@
|
||||||
|
# influxdb-ruby
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/influxdata/influxdb-ruby.svg?branch=master)](https://travis-ci.org/influxdata/influxdb-ruby)
|
||||||
|
|
||||||
|
The official Ruby client library for [InfluxDB](https://influxdata.com/time-series-platform/influxdb/).
|
||||||
|
Maintained by [@toddboom](https://github.com/toddboom) and [@dmke](https://github.com/dmke).
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Platform support](#platform-support)
|
||||||
|
- [Ruby support](#ruby-support)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [Creating a client](#creating-a-client)
|
||||||
|
- [Administrative tasks](#administrative-tasks)
|
||||||
|
- [Continuous queries](#continuous-queries)
|
||||||
|
- [Retention policies](#retention-policies)
|
||||||
|
- [Writing data](#writing-data)
|
||||||
|
- [Reading data](#reading-data)
|
||||||
|
- [Querying](#querying)
|
||||||
|
- [De-normalization](#de--normalization)
|
||||||
|
- [Streaming response](#streaming-response)
|
||||||
|
- [Retry](#retry)
|
||||||
|
- [Testing](#testing)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
|
||||||
|
## Platform support
|
||||||
|
|
||||||
|
> **Support for InfluxDB v0.8.x is now deprecated**. The final version of this
|
||||||
|
> library that will support the older InfluxDB interface is `v0.1.9`, which is
|
||||||
|
> available as a gem and tagged on this repository.
|
||||||
|
>
|
||||||
|
> If you're reading this message, then you should only expect support for
|
||||||
|
> InfluxDB v0.9.1 and higher.
|
||||||
|
|
||||||
|
## Ruby support
|
||||||
|
|
||||||
|
This gem should work with Ruby 1.9+, but starting with v0.4, we'll likely drop
|
||||||
|
Ruby 1.9 support.
|
||||||
|
|
||||||
|
Please note that for Ruby 1.9, you'll need to install the JSON gem in version
|
||||||
|
1.8.x yourself, for example by pinning the version in your `Gemfile` (i.e.
|
||||||
|
`gem "json", "~> 1.8.3"`).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
$ [sudo] gem install influxdb
|
||||||
|
```
|
||||||
|
|
||||||
|
Or add it to your `Gemfile`, and run `bundle install`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creating a client
|
||||||
|
|
||||||
|
Connecting to a single host:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
require 'influxdb'
|
||||||
|
|
||||||
|
influxdb = InfluxDB::Client.new host: "influxdb.domain.com"
|
||||||
|
# or
|
||||||
|
influxdb = InfluxDB::Client.new # no host given defaults connecting to localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
Connecting to multiple hosts (with built-in load balancing and failover):
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
require 'influxdb'
|
||||||
|
|
||||||
|
influxdb = InfluxDB::Client.new hosts: ["influxdb1.domain.com", "influxdb2.domain.com"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Administrative tasks
|
||||||
|
|
||||||
|
Create a database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
database = 'site_development'
|
||||||
|
|
||||||
|
influxdb.create_database(database)
|
||||||
|
```
|
||||||
|
|
||||||
|
Delete a database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
database = 'site_development'
|
||||||
|
|
||||||
|
influxdb.delete_database(database)
|
||||||
|
```
|
||||||
|
|
||||||
|
List databases:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
influxdb.list_databases
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a user for a database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
database = 'site_development'
|
||||||
|
new_username = 'foo'
|
||||||
|
new_password = 'bar'
|
||||||
|
permission = :write
|
||||||
|
|
||||||
|
# with all permissions
|
||||||
|
influxdb.create_database_user(database, new_username, new_password)
|
||||||
|
|
||||||
|
# with specified permission - options are: :read, :write, :all
|
||||||
|
influxdb.create_database_user(database, new_username, new_password, permissions: permission)
|
||||||
|
```
|
||||||
|
|
||||||
|
Update a user password:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
username = 'foo'
|
||||||
|
new_password = 'bar'
|
||||||
|
|
||||||
|
influxdb.update_user_password(username, new_password)
|
||||||
|
```
|
||||||
|
|
||||||
|
Grant user privileges on database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
username = 'foobar'
|
||||||
|
database = 'foo'
|
||||||
|
permission = :read # options are :read, :write, :all
|
||||||
|
|
||||||
|
influxdb.grant_user_privileges(username, database, permission)
|
||||||
|
```
|
||||||
|
|
||||||
|
Revoke user privileges from database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
username = 'foobar'
|
||||||
|
database = 'foo'
|
||||||
|
permission = :write # options are :read, :write, :all
|
||||||
|
|
||||||
|
influxdb.revoke_user_privileges(username, database, permission)
|
||||||
|
```
|
||||||
|
Delete a user:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
username = 'foobar'
|
||||||
|
|
||||||
|
influxdb.delete_user(username)
|
||||||
|
```
|
||||||
|
|
||||||
|
List users:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
influxdb.list_users
|
||||||
|
```
|
||||||
|
|
||||||
|
Create cluster admin:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
username = 'foobar'
|
||||||
|
password = 'pwd'
|
||||||
|
|
||||||
|
influxdb.create_cluster_admin(username, password)
|
||||||
|
```
|
||||||
|
|
||||||
|
List cluster admins:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
influxdb.list_cluster_admins
|
||||||
|
```
|
||||||
|
|
||||||
|
Revoke cluster admin privileges from user:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
username = 'foobar'
|
||||||
|
|
||||||
|
influxdb.revoke_cluster_admin_privileges(username)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Continuous Queries
|
||||||
|
|
||||||
|
List continuous queries of a database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
database = 'foo'
|
||||||
|
|
||||||
|
influxdb.list_continuous_queries(database)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a continuous query for a database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
database = 'foo'
|
||||||
|
name = 'clicks_count'
|
||||||
|
query = 'SELECT COUNT(name) INTO clicksCount_1h FROM clicks GROUP BY time(1h)'
|
||||||
|
|
||||||
|
influxdb.create_continuous_query(name, database, query)
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, you can specify the resample interval and the time range over
|
||||||
|
which the CQ runs:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
influxdb.create_continuous_query(name, database, query, resample_every: "10m", resample_for: "65m")
|
||||||
|
```
|
||||||
|
|
||||||
|
Delete a continuous query from a database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
database = 'foo'
|
||||||
|
name = 'clicks_count'
|
||||||
|
|
||||||
|
influxdb.delete_continuous_query(name, database)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retention Policies
|
||||||
|
|
||||||
|
List retention policies of a database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
database = 'foo'
|
||||||
|
|
||||||
|
influxdb.list_retention_policies(database)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a retention policy for a database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
database = 'foo'
|
||||||
|
name = '1h.cpu'
|
||||||
|
duration = '10m'
|
||||||
|
replication = 2
|
||||||
|
|
||||||
|
influxdb.create_retention_policy(name, database, duration, replication)
|
||||||
|
```
|
||||||
|
|
||||||
|
Delete a retention policy from a database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
database = 'foo'
|
||||||
|
name = '1h.cpu'
|
||||||
|
|
||||||
|
influxdb.delete_retention_policy(name, database)
|
||||||
|
```
|
||||||
|
|
||||||
|
Alter a retention policy for a database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
database = 'foo'
|
||||||
|
name = '1h.cpu'
|
||||||
|
duration = '10m'
|
||||||
|
replication = 2
|
||||||
|
|
||||||
|
influxdb.alter_retention_policy(name, database, duration, replication)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing data
|
||||||
|
|
||||||
|
Write some data:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
username = 'foo'
|
||||||
|
password = 'bar'
|
||||||
|
database = 'site_development'
|
||||||
|
name = 'foobar'
|
||||||
|
|
||||||
|
influxdb = InfluxDB::Client.new database, username: username, password: password
|
||||||
|
|
||||||
|
# Enumerator that emits a sine wave
|
||||||
|
Value = (0..360).to_a.map {|i| Math.send(:sin, i / 10.0) * 10 }.each
|
||||||
|
|
||||||
|
loop do
|
||||||
|
data = {
|
||||||
|
values: { value: Value.next },
|
||||||
|
tags: { wave: 'sine' } # tags are optional
|
||||||
|
}
|
||||||
|
|
||||||
|
influxdb.write_point(name, data)
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Write data with time precision (precision can be set in 2 ways):
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
require 'influxdb'
|
||||||
|
|
||||||
|
username = 'foo'
|
||||||
|
password = 'bar'
|
||||||
|
database = 'site_development'
|
||||||
|
name = 'foobar'
|
||||||
|
time_precision = 's'
|
||||||
|
|
||||||
|
# either in the client initialization:
|
||||||
|
influxdb = InfluxDB::Client.new database,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
time_precision: time_precision
|
||||||
|
|
||||||
|
data = {
|
||||||
|
values: { value: 0 },
|
||||||
|
timestamp: Time.now.to_i # timestamp is optional, if not provided point will be saved with current time
|
||||||
|
}
|
||||||
|
|
||||||
|
influxdb.write_point(name, data)
|
||||||
|
|
||||||
|
# or in a method call:
|
||||||
|
influxdb.write_point(name, data, time_precision)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Write data with a specific retention policy:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
require 'influxdb'
|
||||||
|
|
||||||
|
username = 'foo'
|
||||||
|
password = 'bar'
|
||||||
|
database = 'site_development'
|
||||||
|
name = 'foobar'
|
||||||
|
precision = 's'
|
||||||
|
retention = '1h.cpu'
|
||||||
|
|
||||||
|
influxdb = InfluxDB::Client.new database,
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
|
||||||
|
data = {
|
||||||
|
values: { value: 0 },
|
||||||
|
tags: { foo: 'bar', bar: 'baz' }
|
||||||
|
timestamp: Time.now.to_i
|
||||||
|
}
|
||||||
|
|
||||||
|
influxdb.write_point(name, data, precision, retention)
|
||||||
|
```
|
||||||
|
|
||||||
|
Write data while choosing the database:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
require 'influxdb'
|
||||||
|
|
||||||
|
username = 'foo'
|
||||||
|
password = 'bar'
|
||||||
|
database = 'site_development'
|
||||||
|
name = 'foobar'
|
||||||
|
precision = 's'
|
||||||
|
retention = '1h.cpu'
|
||||||
|
|
||||||
|
influxdb = InfluxDB::Client.new {
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
values: { value: 0 },
|
||||||
|
tags: { foo: 'bar', bar: 'baz' }
|
||||||
|
timestamp: Time.now.to_i
|
||||||
|
}
|
||||||
|
|
||||||
|
influxdb.write_point(name, data, precision, retention, database)
|
||||||
|
```
|
||||||
|
|
||||||
|
Write multiple points in a batch (performance boost):
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
series: 'cpu',
|
||||||
|
tags: { host: 'server_1', region: 'us' },
|
||||||
|
values: { internal: 5, external: 0.453345 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
series: 'gpu',
|
||||||
|
values: { value: 0.9999 },
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
influxdb.write_points(data)
|
||||||
|
|
||||||
|
# you can also specify precision in method call
|
||||||
|
|
||||||
|
precision = 'm'
|
||||||
|
influxdb.write_points(data, precision)
|
||||||
|
```
|
||||||
|
|
||||||
|
Write multiple points in a batch with a specific retention policy:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
series: 'cpu',
|
||||||
|
tags: { host: 'server_1', region: 'us' },
|
||||||
|
values: { internal: 5, external: 0.453345 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
series: 'gpu',
|
||||||
|
values: { value: 0.9999 },
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
precision = 'm'
|
||||||
|
retention = '1h.cpu'
|
||||||
|
influxdb.write_points(data, precision, retention)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Write asynchronously (note that a retention policy cannot be specified for asynchronous writes):
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
require 'influxdb'
|
||||||
|
|
||||||
|
username = 'foo'
|
||||||
|
password = 'bar'
|
||||||
|
database = 'site_development'
|
||||||
|
name = 'foobar'
|
||||||
|
|
||||||
|
influxdb = InfluxDB::Client.new database,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
async: true
|
||||||
|
|
||||||
|
data = {
|
||||||
|
values: { value: 0 },
|
||||||
|
tags: { foo: 'bar', bar: 'baz' },
|
||||||
|
timestamp: Time.now.to_i
|
||||||
|
}
|
||||||
|
|
||||||
|
influxdb.write_point(name, data)
|
||||||
|
```
|
||||||
|
|
||||||
|
Using `async: true` is a shortcut for the following:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
async_options = {
|
||||||
|
# number of points to write to the server at once
|
||||||
|
max_post_points: 1000,
|
||||||
|
# queue capacity
|
||||||
|
max_queue_size: 10_000,
|
||||||
|
# number of threads
|
||||||
|
num_worker_threads: 3,
|
||||||
|
# max. time (in seconds) a thread sleeps before
|
||||||
|
# checking if there are new jobs in the queue
|
||||||
|
sleep_interval: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
influxdb = InfluxDB::Client.new database,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
async: async_options
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Write data via UDP (note that a retention policy cannot be specified for UDP writes):
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
require 'influxdb'
|
||||||
|
host = '127.0.0.1'
|
||||||
|
port = 4444
|
||||||
|
|
||||||
|
influxdb = InfluxDB::Client.new udp: { host: host, port: port }
|
||||||
|
|
||||||
|
name = 'hitchhiker'
|
||||||
|
|
||||||
|
data = {
|
||||||
|
values: { value: 666 },
|
||||||
|
tags: { foo: 'bar', bar: 'baz' }
|
||||||
|
}
|
||||||
|
|
||||||
|
influxdb.write_point(name, data)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reading data
|
||||||
|
|
||||||
|
#### Querying
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
username = 'foo'
|
||||||
|
password = 'bar'
|
||||||
|
database = 'site_development'
|
||||||
|
|
||||||
|
influxdb = InfluxDB::Client.new database,
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
|
||||||
|
# without a block:
|
||||||
|
influxdb.query 'select * from time_series_1 group by region'
|
||||||
|
|
||||||
|
# results are grouped by name, but also their tags:
|
||||||
|
#
|
||||||
|
# [
|
||||||
|
# {
|
||||||
|
# "name"=>"time_series_1",
|
||||||
|
# "tags"=>{"region"=>"uk"},
|
||||||
|
# "values"=>[
|
||||||
|
# {"time"=>"2015-07-09T09:03:31Z", "count"=>32, "value"=>0.9673},
|
||||||
|
# {"time"=>"2015-07-09T09:03:49Z", "count"=>122, "value"=>0.4444}
|
||||||
|
# ]
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "name"=>"time_series_1",
|
||||||
|
# "tags"=>{"region"=>"us"},
|
||||||
|
# "values"=>[
|
||||||
|
# {"time"=>"2015-07-09T09:02:54Z", "count"=>55, "value"=>0.4343}
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# with a block:
|
||||||
|
influxdb.query 'select * from time_series_1 group by region' do |name, tags, points|
|
||||||
|
puts "#{name} [ #{tags.inspect} ]"
|
||||||
|
points.each do |pt|
|
||||||
|
puts " -> #{pt.inspect}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# result:
|
||||||
|
# time_series_1 [ {"region"=>"uk"} ]
|
||||||
|
# -> {"time"=>"2015-07-09T09:03:31Z", "count"=>32, "value"=>0.9673}
|
||||||
|
# -> {"time"=>"2015-07-09T09:03:49Z", "count"=>122, "value"=>0.4444}]
|
||||||
|
# time_series_1 [ {"region"=>"us"} ]
|
||||||
|
# -> {"time"=>"2015-07-09T09:02:54Z", "count"=>55, "value"=>0.4343}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you would rather receive points with integer timestamp, it's possible to set
|
||||||
|
`epoch` parameter:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
# globally, on client initialization:
|
||||||
|
influxdb = InfluxDB::Client.new database, epoch: 's'
|
||||||
|
|
||||||
|
influxdb.query 'select * from time_series group by region'
|
||||||
|
# [
|
||||||
|
# {
|
||||||
|
# "name"=>"time_series",
|
||||||
|
# "tags"=>{"region"=>"uk"},
|
||||||
|
# "values"=>[
|
||||||
|
# {"time"=>1438411376, "count"=>32, "value"=>0.9673}
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# or for a specific query call:
|
||||||
|
influxdb.query 'select * from time_series group by region', epoch: 'ms'
|
||||||
|
# [
|
||||||
|
# {
|
||||||
|
# "name"=>"time_series",
|
||||||
|
# "tags"=>{"region"=>"uk"},
|
||||||
|
# "values"=>[
|
||||||
|
# {"time"=>1438411376000, "count"=>32, "value"=>0.9673}
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Working with parameterized query strings works as expected:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
influxdb = InfluxDB::Client.new database
|
||||||
|
|
||||||
|
named_parameter_query = "select * from time_series_0 where time > %{min_time}"
|
||||||
|
influxdb.query named_parameter_query, params: { min_time: 0 }
|
||||||
|
# compiles to:
|
||||||
|
# select * from time_series_0 where time > 0
|
||||||
|
|
||||||
|
positional_params_query = "select * from time_series_0 where f = %{1} and i < %{2}"
|
||||||
|
influxdb.query positional_params_query, params: ["foobar", 42]
|
||||||
|
# compiles to (note the automatic escaping):
|
||||||
|
# select * from time_series_0 where f = 'foobar' and i < 42
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### (De-) Normalization
|
||||||
|
|
||||||
|
By default, InfluxDB::Client will denormalize points (received from InfluxDB as
|
||||||
|
columns and rows). If you want to get *raw* data add `denormalize: false` to
|
||||||
|
the initialization options or to query itself:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
influxdb.query 'select * from time_series_1 group by region', denormalize: false
|
||||||
|
|
||||||
|
# [
|
||||||
|
# {
|
||||||
|
# "name"=>"time_series_1",
|
||||||
|
# "tags"=>{"region"=>"uk"},
|
||||||
|
# "columns"=>["time", "count", "value"],
|
||||||
|
# "values"=>[
|
||||||
|
# ["2015-07-09T09:03:31Z", 32, 0.9673],
|
||||||
|
# ["2015-07-09T09:03:49Z", 122, 0.4444]
|
||||||
|
# ]
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "name"=>"time_series_1",
|
||||||
|
# "tags"=>{"region"=>"us"},
|
||||||
|
# "columns"=>["time", "count", "value"],
|
||||||
|
# "values"=>[
|
||||||
|
# ["2015-07-09T09:02:54Z", 55, 0.4343]
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|
||||||
|
influxdb.query 'select * from time_series_1 group by region', denormalize: false do |name, tags, points|
|
||||||
|
puts "#{name} [ #{tags.inspect} ]"
|
||||||
|
points.each do |key, values|
|
||||||
|
puts " #{key.inspect} -> #{values.inspect}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# time_series_1 [ {"region"=>"uk"} ]
|
||||||
|
# columns -> ["time", "count", "value"]
|
||||||
|
# values -> [["2015-07-09T09:03:31Z", 32, 0.9673], ["2015-07-09T09:03:49Z", 122, 0.4444]]}
|
||||||
|
# time_series_1 [ {"region"=>"us"} ]
|
||||||
|
# columns -> ["time", "count", "value"]
|
||||||
|
# values -> [["2015-07-09T09:02:54Z", 55, 0.4343]]}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also pick the database to query from:
|
||||||
|
|
||||||
|
```
|
||||||
|
influxdb.query 'select * from time_series_1', database: 'database'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Streaming response
|
||||||
|
|
||||||
|
If you expect large quantities of data in a response, you may want to enable
|
||||||
|
JSON streaming by setting a `chunk_size`:
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
influxdb = InfluxDB::Client.new database,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
chunk_size: 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [official documentation](http://docs.influxdata.com/influxdb/v0.13/guides/querying_data/#chunking)
|
||||||
|
for more details.
|
||||||
|
|
||||||
|
|
||||||
|
#### Retry
|
||||||
|
|
||||||
|
By default, InfluxDB::Client will keep trying (with exponential fall-off) to
|
||||||
|
connect to the database until it gets a connection. If you want to retry only
|
||||||
|
a finite number of times (or disable retries altogether), you can pass the
|
||||||
|
`:retry` option.
|
||||||
|
|
||||||
|
`:retry` can be either `true`, `false` or an `Integer` to retry infinite times,
|
||||||
|
disable retries or retry a finite number of times, respectively. Passing `0` is
|
||||||
|
equivalent to `false` and `-1` is equivalent to `true`.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ irb -r influxdb
|
||||||
|
> influxdb = InfluxDB::Client.new 'database', retry: 8
|
||||||
|
=> #<InfluxDB::Client:0x00000002bb5ce0 ...>
|
||||||
|
|
||||||
|
> influxdb.query 'select * from serie limit 1'
|
||||||
|
E, [2016-08-31T23:55:18.287947 #23476] ERROR -- InfluxDB: Failed to contact host localhost: #<Errno::ECONNREFUSED: Failed to open TCP connection to localhost:8086 (Connection refused - connect(2) for "localhost" port 8086)> - retrying in 0.01s.
|
||||||
|
E, [2016-08-31T23:55:18.298455 #23476] ERROR -- InfluxDB: Failed to contact host localhost: #<Errno::ECONNREFUSED: Failed to open TCP connection to localhost:8086 (Connection refused - connect(2) for "localhost" port 8086)> - retrying in 0.02s.
|
||||||
|
E, [2016-08-31T23:55:18.319122 #23476] ERROR -- InfluxDB: Failed to contact host localhost: #<Errno::ECONNREFUSED: Failed to open TCP connection to localhost:8086 (Connection refused - connect(2) for "localhost" port 8086)> - retrying in 0.04s.
|
||||||
|
E, [2016-08-31T23:55:18.359785 #23476] ERROR -- InfluxDB: Failed to contact host localhost: #<Errno::ECONNREFUSED: Failed to open TCP connection to localhost:8086 (Connection refused - connect(2) for "localhost" port 8086)> - retrying in 0.08s.
|
||||||
|
E, [2016-08-31T23:55:18.440422 #23476] ERROR -- InfluxDB: Failed to contact host localhost: #<Errno::ECONNREFUSED: Failed to open TCP connection to localhost:8086 (Connection refused - connect(2) for "localhost" port 8086)> - retrying in 0.16s.
|
||||||
|
E, [2016-08-31T23:55:18.600936 #23476] ERROR -- InfluxDB: Failed to contact host localhost: #<Errno::ECONNREFUSED: Failed to open TCP connection to localhost:8086 (Connection refused - connect(2) for "localhost" port 8086)> - retrying in 0.32s.
|
||||||
|
E, [2016-08-31T23:55:18.921740 #23476] ERROR -- InfluxDB: Failed to contact host localhost: #<Errno::ECONNREFUSED: Failed to open TCP connection to localhost:8086 (Connection refused - connect(2) for "localhost" port 8086)> - retrying in 0.64s.
|
||||||
|
E, [2016-08-31T23:55:19.562428 #23476] ERROR -- InfluxDB: Failed to contact host localhost: #<Errno::ECONNREFUSED: Failed to open TCP connection to localhost:8086 (Connection refused - connect(2) for "localhost" port 8086)> - retrying in 1.28s.
|
||||||
|
InfluxDB::ConnectionError: Tried 8 times to reconnect but failed.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone git@github.com:influxdata/influxdb-ruby.git
|
||||||
|
cd influxdb-ruby
|
||||||
|
bundle
|
||||||
|
bundle exec rake
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
- Fork this repository on GitHub.
|
||||||
|
- Make your changes.
|
||||||
|
- Add tests.
|
||||||
|
- Add an entry in the `CHANGELOG.md` in the "unreleased" section on top.
|
||||||
|
- Run the tests: `bundle exec rake`.
|
||||||
|
- Send a pull request.
|
||||||
|
- Please rebase against the master branch.
|
||||||
|
- If your changes look good, we'll merge them.
|
|
@ -0,0 +1,51 @@
|
||||||
|
require "rake/testtask"
|
||||||
|
require "bundler/gem_tasks"
|
||||||
|
require "rubocop/rake_task"
|
||||||
|
|
||||||
|
RuboCop::RakeTask.new
|
||||||
|
|
||||||
|
targeted_files = ARGV.drop(1)
|
||||||
|
file_pattern = targeted_files.empty? ? "spec/**/*_spec.rb" : targeted_files
|
||||||
|
|
||||||
|
require "rspec/core"
|
||||||
|
require "rspec/core/rake_task"
|
||||||
|
|
||||||
|
RSpec::Core::RakeTask.new(:spec) do |t|
|
||||||
|
t.pattern = FileList[file_pattern]
|
||||||
|
end
|
||||||
|
|
||||||
|
Rake::TestTask.new(:smoke) do |t|
|
||||||
|
t.test_files = FileList["smoke/*.rb"]
|
||||||
|
end
|
||||||
|
|
||||||
|
task default: [:spec, :rubocop]
|
||||||
|
|
||||||
|
task :console do
|
||||||
|
lib = File.expand_path("../lib", __FILE__)
|
||||||
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||||
|
require "influxdb"
|
||||||
|
|
||||||
|
begin
|
||||||
|
require "pry-byebug"
|
||||||
|
Pry.start
|
||||||
|
rescue LoadError
|
||||||
|
puts <<-TEXT.gsub(/^\s{6}([^ ])/, "\1"), ""
|
||||||
|
Could not load pry-byebug. Create a file Gemfile.local with
|
||||||
|
the following line, if you want to get rid of this message:
|
||||||
|
|
||||||
|
\tgem "pry-byebug"
|
||||||
|
|
||||||
|
(don't forget to run bundle afterwards). Falling back to IRB.
|
||||||
|
TEXT
|
||||||
|
|
||||||
|
require "irb"
|
||||||
|
ARGV.clear
|
||||||
|
IRB.start
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if !ENV.key?("influx_version") || ENV["influx_version"] == ""
|
||||||
|
task default: :spec
|
||||||
|
elsif ENV["TRAVIS"] == "true"
|
||||||
|
task default: :smoke
|
||||||
|
end
|
|
@ -0,0 +1,30 @@
|
||||||
|
# coding: utf-8
|
||||||
|
lib = File.expand_path('../lib', __FILE__)
|
||||||
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||||
|
require 'influxdb/version'
|
||||||
|
|
||||||
|
# rubocop:disable Style/SpecialGlobalVars
|
||||||
|
Gem::Specification.new do |spec|
|
||||||
|
spec.name = "influxdb"
|
||||||
|
spec.version = InfluxDB::VERSION
|
||||||
|
spec.authors = ["Todd Persen"]
|
||||||
|
spec.email = ["influxdb@googlegroups.com"]
|
||||||
|
spec.description = "This is the official Ruby library for InfluxDB."
|
||||||
|
spec.summary = "Ruby library for InfluxDB."
|
||||||
|
spec.homepage = "http://influxdb.org"
|
||||||
|
spec.license = "MIT"
|
||||||
|
|
||||||
|
spec.files = `git ls-files`.split($/)
|
||||||
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
||||||
|
spec.test_files = spec.files.grep(%r{^(test|spec|features|smoke)/})
|
||||||
|
spec.require_paths = ["lib"]
|
||||||
|
|
||||||
|
spec.add_runtime_dependency "json"
|
||||||
|
spec.add_runtime_dependency "cause"
|
||||||
|
|
||||||
|
spec.add_development_dependency "rake"
|
||||||
|
spec.add_development_dependency "bundler", "~> 1.3"
|
||||||
|
spec.add_development_dependency "rspec", "~> 3.5.0"
|
||||||
|
spec.add_development_dependency "webmock", "~> 2.1.0"
|
||||||
|
spec.add_development_dependency "rubocop", "~> 0.41.2"
|
||||||
|
end
|
|
@ -0,0 +1,19 @@
|
||||||
|
require "influxdb/version"
|
||||||
|
require "influxdb/errors"
|
||||||
|
require "influxdb/logging"
|
||||||
|
require "influxdb/max_queue"
|
||||||
|
require "influxdb/point_value"
|
||||||
|
require "influxdb/config"
|
||||||
|
|
||||||
|
require "influxdb/writer/async"
|
||||||
|
require "influxdb/writer/udp"
|
||||||
|
|
||||||
|
require "influxdb/query/core"
|
||||||
|
require "influxdb/query/cluster"
|
||||||
|
require "influxdb/query/database"
|
||||||
|
require "influxdb/query/user"
|
||||||
|
require "influxdb/query/continuous_query"
|
||||||
|
require "influxdb/query/retention_policy"
|
||||||
|
|
||||||
|
require "influxdb/client/http"
|
||||||
|
require "influxdb/client"
|
|
@ -0,0 +1,82 @@
|
||||||
|
require 'json'
|
||||||
|
require 'cause' unless Exception.instance_methods.include?(:cause)
|
||||||
|
require 'thread'
|
||||||
|
|
||||||
|
module InfluxDB
|
||||||
|
# InfluxDB client class
|
||||||
|
class Client
|
||||||
|
attr_reader :config, :writer
|
||||||
|
|
||||||
|
include InfluxDB::Logging
|
||||||
|
include InfluxDB::HTTP
|
||||||
|
include InfluxDB::Query::Core
|
||||||
|
include InfluxDB::Query::Cluster
|
||||||
|
include InfluxDB::Query::Database
|
||||||
|
include InfluxDB::Query::User
|
||||||
|
include InfluxDB::Query::ContinuousQuery
|
||||||
|
include InfluxDB::Query::RetentionPolicy
|
||||||
|
|
||||||
|
# Initializes a new InfluxDB client
|
||||||
|
#
|
||||||
|
# === Examples:
|
||||||
|
#
|
||||||
|
# # connect to localhost using root/root
|
||||||
|
# # as the credentials and doesn't connect to a db
|
||||||
|
#
|
||||||
|
# InfluxDB::Client.new
|
||||||
|
#
|
||||||
|
# # connect to localhost using root/root
|
||||||
|
# # as the credentials and 'db' as the db name
|
||||||
|
#
|
||||||
|
# InfluxDB::Client.new 'db'
|
||||||
|
#
|
||||||
|
# # override username, other defaults remain unchanged
|
||||||
|
#
|
||||||
|
# InfluxDB::Client.new username: 'username'
|
||||||
|
#
|
||||||
|
# # override username, use 'db' as the db name
|
||||||
|
# Influxdb::Client.new 'db', username: 'username'
|
||||||
|
#
|
||||||
|
# === Valid options in hash
|
||||||
|
#
|
||||||
|
# +:host+:: the hostname to connect to
|
||||||
|
# +:port+:: the port to connect to
|
||||||
|
# +:prefix+:: the specified path prefix when building the url e.g.: /prefix/db/dbname...
|
||||||
|
# +:username+:: the username to use when executing commands
|
||||||
|
# +:password+:: the password associated with the username
|
||||||
|
# +:use_ssl+:: use ssl to connect
|
||||||
|
# +:verify_ssl+:: verify ssl server certificate?
|
||||||
|
# +:ssl_ca_cert+:: ssl CA certificate, chainfile or CA path.
|
||||||
|
# The system CA path is automatically included
|
||||||
|
def initialize(*args)
|
||||||
|
opts = args.last.is_a?(Hash) ? args.last : {}
|
||||||
|
opts[:database] = args.first if args.first.is_a? String
|
||||||
|
@config = InfluxDB::Config.new(opts)
|
||||||
|
@stopped = false
|
||||||
|
@writer = find_writer
|
||||||
|
|
||||||
|
at_exit { stop! } if config.retry > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop!
|
||||||
|
writer.worker.stop! if config.async?
|
||||||
|
@stopped = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def stopped?
|
||||||
|
@stopped
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_writer
|
||||||
|
if config.async?
|
||||||
|
InfluxDB::Writer::Async.new(self, config.async)
|
||||||
|
elsif config.udp?
|
||||||
|
InfluxDB::Writer::UDP.new(self, config.udp)
|
||||||
|
else
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,134 @@
|
||||||
|
require 'uri'
|
||||||
|
require 'cgi'
|
||||||
|
require 'net/http'
|
||||||
|
require 'net/https'
|
||||||
|
|
||||||
|
module InfluxDB
|
||||||
|
# rubocop:disable Metrics/MethodLength
|
||||||
|
# rubocop:disable Metrics/AbcSize
|
||||||
|
module HTTP # :nodoc:
|
||||||
|
def get(url, options = {})
|
||||||
|
connect_with_retry do |http|
|
||||||
|
response = do_request http, Net::HTTP::Get.new(url)
|
||||||
|
case response
|
||||||
|
when Net::HTTPSuccess
|
||||||
|
handle_successful_response(response, options)
|
||||||
|
when Net::HTTPUnauthorized
|
||||||
|
raise InfluxDB::AuthenticationError, response.body
|
||||||
|
else
|
||||||
|
resolve_error(response.body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def post(url, data)
|
||||||
|
headers = { "Content-Type" => "application/octet-stream" }
|
||||||
|
connect_with_retry do |http|
|
||||||
|
response = do_request http, Net::HTTP::Post.new(url, headers), data
|
||||||
|
|
||||||
|
case response
|
||||||
|
when Net::HTTPNoContent
|
||||||
|
return response
|
||||||
|
when Net::HTTPUnauthorized
|
||||||
|
raise InfluxDB::AuthenticationError, response.body
|
||||||
|
else
|
||||||
|
resolve_error(response.body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def connect_with_retry
|
||||||
|
host = config.next_host
|
||||||
|
delay = config.initial_delay
|
||||||
|
retry_count = 0
|
||||||
|
|
||||||
|
begin
|
||||||
|
http = Net::HTTP.new(host, config.port)
|
||||||
|
http.open_timeout = config.open_timeout
|
||||||
|
http.read_timeout = config.read_timeout
|
||||||
|
|
||||||
|
http = setup_ssl(http)
|
||||||
|
yield http
|
||||||
|
|
||||||
|
rescue *InfluxDB::NON_RECOVERABLE_EXCEPTIONS => e
|
||||||
|
raise InfluxDB::ConnectionError, InfluxDB::NON_RECOVERABLE_MESSAGE
|
||||||
|
rescue Timeout::Error, *InfluxDB::RECOVERABLE_EXCEPTIONS => e
|
||||||
|
retry_count += 1
|
||||||
|
unless (config.retry == -1 || retry_count <= config.retry) && !stopped?
|
||||||
|
raise InfluxDB::ConnectionError, "Tried #{retry_count - 1} times to reconnect but failed."
|
||||||
|
end
|
||||||
|
log :error, "Failed to contact host #{host}: #{e.inspect} - retrying in #{delay}s."
|
||||||
|
sleep delay
|
||||||
|
delay = [config.max_delay, delay * 2].min
|
||||||
|
retry
|
||||||
|
ensure
|
||||||
|
http.finish if http.started?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def do_request(http, req, data = nil)
|
||||||
|
req.basic_auth config.username, config.password if basic_auth?
|
||||||
|
req.body = data if data
|
||||||
|
http.request(req)
|
||||||
|
end
|
||||||
|
|
||||||
|
def basic_auth?
|
||||||
|
config.auth_method == 'basic_auth'
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_error(response)
|
||||||
|
if response =~ /Couldn\'t find series/
|
||||||
|
raise InfluxDB::SeriesNotFound, response
|
||||||
|
end
|
||||||
|
raise InfluxDB::Error, response
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_successful_response(response, options)
|
||||||
|
if options.fetch(:json_streaming, false)
|
||||||
|
parsed_response = response.body.each_line.with_object({}) do |line, parsed|
|
||||||
|
parsed.merge!(JSON.parse(line)) { |_key, oldval, newval| oldval + newval }
|
||||||
|
end
|
||||||
|
elsif response.body
|
||||||
|
parsed_response = JSON.parse(response.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
errors = errors_from_response(parsed_response)
|
||||||
|
|
||||||
|
raise InfluxDB::QueryError, errors if errors
|
||||||
|
options.fetch(:parse, false) ? parsed_response : response
|
||||||
|
end
|
||||||
|
|
||||||
|
def errors_from_response(parsed_resp)
|
||||||
|
return unless parsed_resp.is_a?(Hash)
|
||||||
|
parsed_resp
|
||||||
|
.fetch('results', [])
|
||||||
|
.fetch(0, {})
|
||||||
|
.fetch('error', nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_ssl(http)
|
||||||
|
http.use_ssl = config.use_ssl
|
||||||
|
http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless config.verify_ssl
|
||||||
|
|
||||||
|
return http unless config.use_ssl
|
||||||
|
|
||||||
|
http.cert_store = generate_cert_store
|
||||||
|
http
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_cert_store
|
||||||
|
store = OpenSSL::X509::Store.new
|
||||||
|
store.set_default_paths
|
||||||
|
if config.ssl_ca_cert
|
||||||
|
if File.directory?(config.ssl_ca_cert)
|
||||||
|
store.add_path(config.ssl_ca_cert)
|
||||||
|
else
|
||||||
|
store.add_file(config.ssl_ca_cert)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
store
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,120 @@
|
||||||
|
require 'thread'
|
||||||
|
|
||||||
|
module InfluxDB
|
||||||
|
# InfluxDB client configuration
|
||||||
|
class Config
|
||||||
|
AUTH_METHODS = ["params".freeze, "basic_auth".freeze, "none".freeze].freeze
|
||||||
|
|
||||||
|
attr_accessor :port,
|
||||||
|
:username,
|
||||||
|
:password,
|
||||||
|
:database,
|
||||||
|
:time_precision,
|
||||||
|
:use_ssl,
|
||||||
|
:verify_ssl,
|
||||||
|
:ssl_ca_cert,
|
||||||
|
:auth_method,
|
||||||
|
:initial_delay,
|
||||||
|
:max_delay,
|
||||||
|
:open_timeout,
|
||||||
|
:read_timeout,
|
||||||
|
:retry,
|
||||||
|
:prefix,
|
||||||
|
:chunk_size,
|
||||||
|
:denormalize,
|
||||||
|
:epoch
|
||||||
|
|
||||||
|
attr_reader :async, :udp
|
||||||
|
|
||||||
|
def initialize(opts = {})
|
||||||
|
extract_http_options!(opts)
|
||||||
|
extract_ssl_options!(opts)
|
||||||
|
extract_database_options!(opts)
|
||||||
|
extract_writer_options!(opts)
|
||||||
|
extract_query_options!(opts)
|
||||||
|
|
||||||
|
configure_retry! opts.fetch(:retry, nil)
|
||||||
|
configure_hosts! opts[:hosts] || opts[:host] || "localhost".freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def udp?
|
||||||
|
udp != false
|
||||||
|
end
|
||||||
|
|
||||||
|
def async?
|
||||||
|
async != false
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_host
|
||||||
|
host = @hosts_queue.pop
|
||||||
|
@hosts_queue.push(host)
|
||||||
|
host
|
||||||
|
end
|
||||||
|
|
||||||
|
def hosts
|
||||||
|
Array.new(@hosts_queue.length) do
|
||||||
|
host = @hosts_queue.pop
|
||||||
|
@hosts_queue.push(host)
|
||||||
|
host
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/AbcSize
|
||||||
|
def extract_http_options!(opts)
|
||||||
|
@port = opts.fetch :port, 8086
|
||||||
|
@prefix = opts.fetch :prefix, "".freeze
|
||||||
|
@username = opts.fetch :username, "root".freeze
|
||||||
|
@password = opts.fetch :password, "root".freeze
|
||||||
|
@open_timeout = opts.fetch :write_timeout, 5
|
||||||
|
@read_timeout = opts.fetch :read_timeout, 300
|
||||||
|
@max_delay = opts.fetch :max_delay, 30
|
||||||
|
@initial_delay = opts.fetch :initial_delay, 0.01
|
||||||
|
auth = opts[:auth_method]
|
||||||
|
@auth_method = AUTH_METHODS.include?(auth) ? auth : "params".freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_ssl_options!(opts)
|
||||||
|
@use_ssl = opts.fetch :use_ssl, false
|
||||||
|
@verify_ssl = opts.fetch :verify_ssl, true
|
||||||
|
@ssl_ca_cert = opts.fetch :ssl_ca_cert, false
|
||||||
|
end
|
||||||
|
|
||||||
|
# normalize retry option
|
||||||
|
def configure_retry!(value)
|
||||||
|
case value
|
||||||
|
when Integer
|
||||||
|
@retry = value
|
||||||
|
when true, nil
|
||||||
|
@retry = -1
|
||||||
|
when false
|
||||||
|
@retry = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# load the hosts into a Queue for thread safety
|
||||||
|
def configure_hosts!(hosts)
|
||||||
|
@hosts_queue = Queue.new
|
||||||
|
Array(hosts).each do |host|
|
||||||
|
@hosts_queue.push(host)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_database_options!(opts)
|
||||||
|
@database = opts[:database]
|
||||||
|
@time_precision = opts.fetch :time_precision, "s".freeze
|
||||||
|
@denormalize = opts.fetch :denormalize, true
|
||||||
|
@epoch = opts.fetch :epoch, false
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_writer_options!(opts)
|
||||||
|
@async = opts.fetch :async, false
|
||||||
|
@udp = opts.fetch :udp, false
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_query_options!(opts)
|
||||||
|
@chunk_size = opts.fetch :chunk_size, nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,52 @@
|
||||||
|
require "net/http"
|
||||||
|
require "zlib"
|
||||||
|
|
||||||
|
module InfluxDB # :nodoc:
|
||||||
|
class Error < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
|
class AuthenticationError < Error
|
||||||
|
end
|
||||||
|
|
||||||
|
class ConnectionError < Error
|
||||||
|
end
|
||||||
|
|
||||||
|
class SeriesNotFound < Error
|
||||||
|
end
|
||||||
|
|
||||||
|
class JSONParserError < Error
|
||||||
|
end
|
||||||
|
|
||||||
|
class QueryError < Error
|
||||||
|
end
|
||||||
|
|
||||||
|
# When executing queries via HTTP, some errors can more or less safely
|
||||||
|
# be ignored and we can retry the query again. This following
|
||||||
|
# exception classes shall be deemed as "safe".
|
||||||
|
#
|
||||||
|
# Taken from: https://github.com/lostisland/faraday/blob/master/lib/faraday/adapter/net_http.rb
|
||||||
|
RECOVERABLE_EXCEPTIONS = [
|
||||||
|
Errno::ECONNABORTED,
|
||||||
|
Errno::ECONNREFUSED,
|
||||||
|
Errno::ECONNRESET,
|
||||||
|
Errno::EHOSTUNREACH,
|
||||||
|
Errno::EINVAL,
|
||||||
|
Errno::ENETUNREACH,
|
||||||
|
Net::HTTPBadResponse,
|
||||||
|
Net::HTTPHeaderSyntaxError,
|
||||||
|
Net::ProtocolError,
|
||||||
|
SocketError,
|
||||||
|
(OpenSSL::SSL::SSLError if defined?(OpenSSL))
|
||||||
|
].compact.freeze
|
||||||
|
|
||||||
|
# Exception classes which hint to a larger problem on the server side,
|
||||||
|
# like insuffient resources. If we encouter on of the following, wo
|
||||||
|
# _don't_ retry a query but escalate it upwards.
|
||||||
|
NON_RECOVERABLE_EXCEPTIONS = [
|
||||||
|
EOFError,
|
||||||
|
Zlib::Error
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
NON_RECOVERABLE_MESSAGE = "The server has sent incomplete data" \
|
||||||
|
" (insufficient resources are a possible cause).".freeze
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue