logicality 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +8 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +32 -0
- data/LICENSE +7 -0
- data/README.md +93 -0
- data/lib/logicality/interpreter/interpreter.rb +9 -0
- data/lib/logicality/interpreter/node_visitor.rb +36 -0
- data/lib/logicality/interpreter/simple_interpreter.rb +64 -0
- data/lib/logicality/lexer/grammar.rb +19 -0
- data/lib/logicality/lexer/lexer.rb +10 -0
- data/lib/logicality/lexer/regexp_lexer.rb +97 -0
- data/lib/logicality/lexer/token.rb +37 -0
- data/lib/logicality/logic.rb +65 -0
- data/lib/logicality/logicality.rb +11 -0
- data/lib/logicality/parser/ast/ast.rb +11 -0
- data/lib/logicality/parser/ast/binary_operator_node.rb +26 -0
- data/lib/logicality/parser/ast/node.rb +27 -0
- data/lib/logicality/parser/ast/unary_operator_node.rb +25 -0
- data/lib/logicality/parser/ast/value_operand_node.rb +25 -0
- data/lib/logicality/parser/parser.rb +9 -0
- data/lib/logicality/parser/simple_parser.rb +97 -0
- data/lib/logicality/version.rb +10 -0
- data/logicality.gemspec +28 -0
- data/spec/logic_spec.rb +87 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0356f85f51e792d67aaadf12bb510c5c9e798870
|
4
|
+
data.tar.gz: 17d3463798a0c252bb4161853254280bbb2f9ccd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '044761792866c934fb3f2827b36a7f1c1d94351ea82dfc85cb79611e7f2f13fa25a906428c1d006ca96c7333bc02455e0e55573503fc8ccb28c0315c2eefd17d'
|
7
|
+
data.tar.gz: ab723be91f1b30bf0b1fe5c14e46dec6bdf907729fe8ae9dfbe55d75a24b733c0d2ea98a8aadb21c0ea9d0f4631217a79d20bb71f88ec5b7f4c0bfe42b7f9eed
|
data/.editorconfig
ADDED
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.DS_Store
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.7
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
logicality (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.3)
|
10
|
+
rspec (3.8.0)
|
11
|
+
rspec-core (~> 3.8.0)
|
12
|
+
rspec-expectations (~> 3.8.0)
|
13
|
+
rspec-mocks (~> 3.8.0)
|
14
|
+
rspec-core (3.8.0)
|
15
|
+
rspec-support (~> 3.8.0)
|
16
|
+
rspec-expectations (3.8.1)
|
17
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
18
|
+
rspec-support (~> 3.8.0)
|
19
|
+
rspec-mocks (3.8.0)
|
20
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
21
|
+
rspec-support (~> 3.8.0)
|
22
|
+
rspec-support (3.8.0)
|
23
|
+
|
24
|
+
PLATFORMS
|
25
|
+
ruby
|
26
|
+
|
27
|
+
DEPENDENCIES
|
28
|
+
logicality!
|
29
|
+
rspec
|
30
|
+
|
31
|
+
BUNDLED WITH
|
32
|
+
1.16.3
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright 2018 Blue Marble Payroll, LLC
|
2
|
+
|
3
|
+
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:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
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.
|
data/README.md
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
*Note: This is a ruby implementation of [Logicality](https://github.com/bluemarblepayroll/logicality).*
|
2
|
+
|
3
|
+
# Logicality
|
4
|
+
|
5
|
+
[![Build Status](https://travis-ci.org/bluemarblepayroll/logicality-rb.svg?branch=master)](https://travis-ci.org/bluemarblepayroll/logicality-rb)
|
6
|
+
|
7
|
+
A common problem that many frameworks have is the ability to give developers an expressive
|
8
|
+
intermediary scripting language or DSL. Logicality helps solve this problem by providing a simple
|
9
|
+
boolean expression evaluator. That way, your developers can create simple scripts for dynamically
|
10
|
+
resolving boolean values such as:
|
11
|
+
|
12
|
+
* a
|
13
|
+
* b
|
14
|
+
* a && b
|
15
|
+
* a || b
|
16
|
+
* a && b || c
|
17
|
+
* (a && b) || (c && d)
|
18
|
+
* (a && b) || (c && (d || e && f))
|
19
|
+
|
20
|
+
## Credit
|
21
|
+
|
22
|
+
Deep inspiration was taken from [this set of articles](https://ruslanspivak.com/lsbasi-part7/).
|
23
|
+
Here, the author gives details around the theory and practical implementation of creating a basic
|
24
|
+
language processor and compiler.
|
25
|
+
|
26
|
+
## Installation
|
27
|
+
|
28
|
+
To install through Rubygems:
|
29
|
+
|
30
|
+
````
|
31
|
+
gem install install logicality
|
32
|
+
````
|
33
|
+
|
34
|
+
You can also add this to your Gemfile:
|
35
|
+
|
36
|
+
````
|
37
|
+
bundle add logicality
|
38
|
+
````
|
39
|
+
|
40
|
+
## Examples
|
41
|
+
|
42
|
+
### A simple object-based example.
|
43
|
+
|
44
|
+
Consider a case where some content should be displayed if it is marked as visible or if the user is an administrator. You can express this as:
|
45
|
+
|
46
|
+
````
|
47
|
+
visible || admin
|
48
|
+
````
|
49
|
+
|
50
|
+
Now you can bind and evaluate this expression against passed in objects:
|
51
|
+
|
52
|
+
````
|
53
|
+
record = { 'visible' => false, 'admin' => true }
|
54
|
+
visible = Logicality::Logic.evaluate('visible || admin', record) # resolves to true.
|
55
|
+
````
|
56
|
+
|
57
|
+
### Plugging in a Custom Resolver
|
58
|
+
|
59
|
+
Notice that the above example uses the default value resolver, which either wants an object that has values as attributes, or an object with a brackets method (i.e. Hash). In the brackets method approach, keys will be accessed using strings (as opposed to symbols.). If we wanted to use a custom resolver, for example to use symbols as keys, we could do this:
|
60
|
+
|
61
|
+
````
|
62
|
+
resolver = lambda do |value, input|
|
63
|
+
symbolized_hash = (input || {}).map { |k,v| [ k.to_s.to_sym, v] }.to_h
|
64
|
+
!!symbolized_hash[value]
|
65
|
+
end
|
66
|
+
|
67
|
+
record = { visible: false, admin: true }
|
68
|
+
visible = Logicality::Logic.evaluate('visible || admin', record) # resolves to true.
|
69
|
+
````
|
70
|
+
|
71
|
+
## Contributing
|
72
|
+
|
73
|
+
### Development Environment Configuration
|
74
|
+
|
75
|
+
Basic steps to take to get this repository compiling:
|
76
|
+
|
77
|
+
1. Install [Ruby](https://www.ruby-lang.org/en/documentation/installation/) (check logicality.gemspec for versions supported)
|
78
|
+
2. Install bundler (gem install bundler)
|
79
|
+
3. Clone the repository (git clone git@github.com:bluemarblepayroll/logicality-rb.git)
|
80
|
+
4. Navigate to the root folder (cd logicality)
|
81
|
+
5. Install dependencies (bundle)
|
82
|
+
|
83
|
+
### Running Tests
|
84
|
+
|
85
|
+
To execute the test suite run:
|
86
|
+
|
87
|
+
````
|
88
|
+
rspec
|
89
|
+
````
|
90
|
+
|
91
|
+
## License
|
92
|
+
|
93
|
+
This project is MIT Licensed.
|
@@ -0,0 +1,9 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
require './lib/logicality/interpreter/node_visitor'
|
9
|
+
require './lib/logicality/interpreter/simple_interpreter'
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Logicality
|
9
|
+
module Interpreter
|
10
|
+
class NodeVisitor
|
11
|
+
|
12
|
+
def visit(node)
|
13
|
+
return nil unless node
|
14
|
+
|
15
|
+
visitor_name = method_name(node)
|
16
|
+
|
17
|
+
if respond_to?(visitor_name)
|
18
|
+
send(visitor_name, node)
|
19
|
+
else
|
20
|
+
generic_visit(node)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def generic_visit(node)
|
27
|
+
raise ArgumentError, "No visitor method: #{method_name(node)}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_name(node)
|
31
|
+
"visit_#{node.name}"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Logicality
|
9
|
+
module Interpreter
|
10
|
+
class SimpleInterpreter < NodeVisitor
|
11
|
+
|
12
|
+
attr_reader :resolver
|
13
|
+
|
14
|
+
def initialize(resolver)
|
15
|
+
raise ArgumentError, "Resolver is required" unless resolver
|
16
|
+
|
17
|
+
@resolver = resolver
|
18
|
+
end
|
19
|
+
|
20
|
+
def error(node)
|
21
|
+
raise ArgumentError, "Visitor cant process node token type: #{node.token.type}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def visit_binary_operator_node(node)
|
25
|
+
if node.token.type == Lexer::Token::Type::AND_OP
|
26
|
+
visit(node.left) && visit(node.right)
|
27
|
+
elsif node.token.type == Lexer::Token::Type::OR_OP
|
28
|
+
visit(node.left) || visit(node.right)
|
29
|
+
else
|
30
|
+
error(node)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def visit_unary_operator_node(node)
|
35
|
+
if node.token.type == Lexer::Token::Type::NOT_OP
|
36
|
+
!visit(node.child)
|
37
|
+
else
|
38
|
+
error(node)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def visit_value_operand_node(node)
|
43
|
+
if node.value == 'true'
|
44
|
+
true
|
45
|
+
elsif node.value == 'false' || node.value == 'null'
|
46
|
+
false
|
47
|
+
else
|
48
|
+
resolve_value(node.value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def resolve_value(value)
|
55
|
+
if resolver.nil?
|
56
|
+
raise ArgumentError, "No resolver function but trying to resolve: #{value}"
|
57
|
+
end
|
58
|
+
|
59
|
+
!!resolver.call(value)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Logicality
|
9
|
+
module Lexer
|
10
|
+
module Grammar
|
11
|
+
VALUE = /([a-zA-Z0-9_$@?\.]+)/
|
12
|
+
AND_OP = /(&&)/
|
13
|
+
OR_OP = /(\|\|)/
|
14
|
+
NOT_OP = /(\!)/
|
15
|
+
LEFT_PAREN = /(\()/
|
16
|
+
RIGHT_PAREN = /(\))/
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
require './lib/logicality/lexer/grammar'
|
9
|
+
require './lib/logicality/lexer/token'
|
10
|
+
require './lib/logicality/lexer/regexp_lexer'
|
@@ -0,0 +1,97 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Logicality
|
9
|
+
module Lexer
|
10
|
+
class RegexpLexer
|
11
|
+
include Grammar
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
def invalid_pattern
|
16
|
+
"#{pattern}|(\\s*)"
|
17
|
+
end
|
18
|
+
|
19
|
+
def invalid_regexp
|
20
|
+
Regexp.new(invalid_pattern)
|
21
|
+
end
|
22
|
+
|
23
|
+
def pattern
|
24
|
+
Grammar.constants.map { |c| Grammar.const_get(c).source }
|
25
|
+
.join('|')
|
26
|
+
end
|
27
|
+
|
28
|
+
def regexp
|
29
|
+
Regexp.new(pattern)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :expression
|
35
|
+
|
36
|
+
def initialize(expression)
|
37
|
+
raise ArgumentError, 'Expression is required' unless expression && expression.to_s.length > 0
|
38
|
+
|
39
|
+
@expression = expression.to_s
|
40
|
+
|
41
|
+
if invalid_matches.length > 0
|
42
|
+
raise ArgumentError, "Invalid syntax: #{invalid_matches}"
|
43
|
+
end
|
44
|
+
|
45
|
+
reset
|
46
|
+
end
|
47
|
+
|
48
|
+
def next_token
|
49
|
+
return nil if index > matches.length - 1
|
50
|
+
|
51
|
+
increment
|
52
|
+
|
53
|
+
scan_array = matches[index]
|
54
|
+
|
55
|
+
return nil unless scan_array
|
56
|
+
|
57
|
+
tokens = scan_array.map.with_index do |value, index|
|
58
|
+
const = Grammar.constants[index]
|
59
|
+
value ? Token.new(const, value) : nil
|
60
|
+
end.compact
|
61
|
+
|
62
|
+
if tokens.length > 1
|
63
|
+
raise ArgumentError, "Too many tokens found for: #{scan_array}"
|
64
|
+
elsif tokens.length == 0
|
65
|
+
raise ArgumentError, "Cannot tokenize: #{scan_array}"
|
66
|
+
end
|
67
|
+
|
68
|
+
tokens.first
|
69
|
+
end
|
70
|
+
|
71
|
+
def reset
|
72
|
+
@index = -1
|
73
|
+
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
attr_reader :index
|
80
|
+
|
81
|
+
def increment
|
82
|
+
@index += 1
|
83
|
+
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def invalid_matches
|
88
|
+
@invalid_matches ||= expression.gsub(self.class.invalid_regexp, '')
|
89
|
+
end
|
90
|
+
|
91
|
+
def matches
|
92
|
+
@matches ||= expression.scan(self.class.regexp)
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Logicality
|
9
|
+
module Lexer
|
10
|
+
class Token
|
11
|
+
|
12
|
+
module Type
|
13
|
+
VALUE = :value
|
14
|
+
AND_OP = :and_op
|
15
|
+
OR_OP = :or_op
|
16
|
+
NOT_OP = :not_op
|
17
|
+
LEFT_PAREN = :left_paren
|
18
|
+
RIGHT_PAREN = :right_paren
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :type, :value
|
22
|
+
|
23
|
+
def initialize(type, value)
|
24
|
+
raise ArgumentError, 'type is required' unless type && type.to_s.length > 0
|
25
|
+
raise ArgumentError, 'value is required' unless value && value.to_s.length > 0
|
26
|
+
|
27
|
+
@type = Type.const_get(type.to_s.upcase.to_sym)
|
28
|
+
@value = value.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
"#{type}::#{value}"
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Logicality
|
9
|
+
class Logic
|
10
|
+
class << self
|
11
|
+
|
12
|
+
def evaluate(expression, input, resolver = nil)
|
13
|
+
node = get(expression)
|
14
|
+
wrapper = resolver_wrapper(input, resolver)
|
15
|
+
interpreter = Interpreter::SimpleInterpreter.new(wrapper)
|
16
|
+
|
17
|
+
interpreter.visit(node)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def resolver_wrapper(input, resolver)
|
23
|
+
if resolver
|
24
|
+
lambda { |value| resolver.call(value, input) }
|
25
|
+
else
|
26
|
+
lambda { |value| object_resolver.call(value, input) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def object_resolver
|
31
|
+
lambda do |value, input|
|
32
|
+
return false unless input
|
33
|
+
|
34
|
+
if input.respond_to?(value)
|
35
|
+
!!input.send(value)
|
36
|
+
elsif input.respond_to?(:[])
|
37
|
+
!!input[value]
|
38
|
+
else
|
39
|
+
false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def cache
|
45
|
+
@cache || {}
|
46
|
+
end
|
47
|
+
|
48
|
+
def set(expression, node)
|
49
|
+
@cache = {} unless @cache
|
50
|
+
|
51
|
+
@cache[expression] = node
|
52
|
+
end
|
53
|
+
|
54
|
+
def get(expression)
|
55
|
+
return cache[expression] if cache[expression]
|
56
|
+
|
57
|
+
lexer = Lexer::RegexpLexer.new(expression)
|
58
|
+
parser = Parser::SimpleParser.new(lexer)
|
59
|
+
|
60
|
+
set(expression, parser.parse)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
require './lib/logicality/lexer/lexer'
|
9
|
+
require './lib/logicality/parser/parser'
|
10
|
+
require './lib/logicality/interpreter/interpreter'
|
11
|
+
require './lib/logicality/logic'
|
@@ -0,0 +1,11 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
require './lib/logicality/parser/ast/node'
|
9
|
+
require './lib/logicality/parser/ast/binary_operator_node'
|
10
|
+
require './lib/logicality/parser/ast/unary_operator_node'
|
11
|
+
require './lib/logicality/parser/ast/value_operand_node'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Logicality
|
9
|
+
module Parser
|
10
|
+
module Ast
|
11
|
+
class BinaryOperatorNode < Node
|
12
|
+
|
13
|
+
attr_reader :left, :right
|
14
|
+
|
15
|
+
def initialize(left, token, right)
|
16
|
+
super(token)
|
17
|
+
|
18
|
+
@name = 'binary_operator_node'
|
19
|
+
@left = left
|
20
|
+
@right = right
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Logicality
|
9
|
+
module Parser
|
10
|
+
module Ast
|
11
|
+
class Node
|
12
|
+
|
13
|
+
attr_reader :token, :name
|
14
|
+
|
15
|
+
def initialize(token)
|
16
|
+
@token = token
|
17
|
+
@name = ''
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
"AstNode: #{self.class.name}::#{token}"
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Logicality
|
9
|
+
module Parser
|
10
|
+
module Ast
|
11
|
+
class UnaryOperatorNode < Node
|
12
|
+
|
13
|
+
attr_reader :child
|
14
|
+
|
15
|
+
def initialize(child, token)
|
16
|
+
super(token)
|
17
|
+
|
18
|
+
@name = 'unary_operator_node'
|
19
|
+
@child = child
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Logicality
|
9
|
+
module Parser
|
10
|
+
module Ast
|
11
|
+
class ValueOperandNode < Node
|
12
|
+
|
13
|
+
attr_reader :value
|
14
|
+
|
15
|
+
def initialize(token)
|
16
|
+
super(token)
|
17
|
+
|
18
|
+
@name = 'value_operand_node'
|
19
|
+
@value = token.value
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
require './lib/logicality/parser/ast/ast'
|
9
|
+
require './lib/logicality/parser/simple_parser'
|
@@ -0,0 +1,97 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Logicality
|
9
|
+
module Parser
|
10
|
+
class SimpleParser
|
11
|
+
|
12
|
+
attr_reader :lexer
|
13
|
+
|
14
|
+
def initialize(lexer)
|
15
|
+
@lexer = lexer
|
16
|
+
|
17
|
+
@current_token = lexer.next_token
|
18
|
+
|
19
|
+
if @current_token.nil?
|
20
|
+
raise ArgumentError, 'Lexer must contain at least one token'
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse
|
26
|
+
expr
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
BINARY_TYPES = [
|
32
|
+
Lexer::Token::Type::AND_OP,
|
33
|
+
Lexer::Token::Type::OR_OP,
|
34
|
+
].freeze
|
35
|
+
|
36
|
+
attr_reader :current_token
|
37
|
+
|
38
|
+
def error
|
39
|
+
raise ArgumentError, 'Invalid parser syntax'
|
40
|
+
end
|
41
|
+
|
42
|
+
def eat(token_type)
|
43
|
+
if current_token.type == token_type
|
44
|
+
@current_token = lexer.next_token
|
45
|
+
else
|
46
|
+
error
|
47
|
+
end
|
48
|
+
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def factor
|
53
|
+
token = current_token
|
54
|
+
|
55
|
+
if current_token.type == Lexer::Token::Type::VALUE
|
56
|
+
eat(Lexer::Token::Type::VALUE)
|
57
|
+
|
58
|
+
Ast::ValueOperandNode.new(token)
|
59
|
+
elsif current_token.type == Lexer::Token::Type::LEFT_PAREN
|
60
|
+
eat(Lexer::Token::Type::LEFT_PAREN)
|
61
|
+
node = expr
|
62
|
+
eat(Lexer::Token::Type::RIGHT_PAREN)
|
63
|
+
|
64
|
+
node
|
65
|
+
elsif current_token.type == Lexer::Token::Type::NOT_OP
|
66
|
+
eat(Lexer::Token::Type::NOT_OP)
|
67
|
+
node = factor
|
68
|
+
|
69
|
+
Ast::UnaryOperatorNode.new(node, token)
|
70
|
+
else
|
71
|
+
raise ArgumentError, "Factor cannot determine what to do with: #{token}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def expr
|
76
|
+
node = factor
|
77
|
+
|
78
|
+
loop do
|
79
|
+
break unless current_token && BINARY_TYPES.include?(current_token.type)
|
80
|
+
|
81
|
+
token = current_token
|
82
|
+
|
83
|
+
if token.type == Lexer::Token::Type::AND_OP
|
84
|
+
eat(Lexer::Token::Type::AND_OP)
|
85
|
+
elsif token.type == Lexer::Token::Type::OR_OP
|
86
|
+
eat(Lexer::Token::Type::OR_OP)
|
87
|
+
end
|
88
|
+
|
89
|
+
node = Ast::BinaryOperatorNode.new(node, token, factor)
|
90
|
+
end
|
91
|
+
|
92
|
+
node
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/logicality.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require "./lib/logicality/version"
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
|
5
|
+
s.name = 'logicality'
|
6
|
+
s.version = Logicality::VERSION
|
7
|
+
s.summary = "String-based boolean expression evaluator"
|
8
|
+
|
9
|
+
s.description = <<-EOS
|
10
|
+
A common problem that many frameworks have is the ability to give developers
|
11
|
+
an expressive intermediary scripting language or DSL.
|
12
|
+
Logicality helps solve this problem by providing a simple boolean
|
13
|
+
expression evaluator.
|
14
|
+
EOS
|
15
|
+
|
16
|
+
s.authors = [ 'Matthew Ruggio' ]
|
17
|
+
s.email = [ 'mruggio@bluemarblepayroll.com' ]
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.homepage = 'https://github.com/bluemarblepayroll/logicality-rb'
|
22
|
+
s.license = 'MIT'
|
23
|
+
|
24
|
+
s.required_ruby_version = '>= 2.3.7'
|
25
|
+
|
26
|
+
s.add_development_dependency('rspec')
|
27
|
+
|
28
|
+
end
|
data/spec/logic_spec.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
3
|
+
#
|
4
|
+
# This source code is licensed under the MIT license found in the
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
6
|
+
#
|
7
|
+
|
8
|
+
require './lib/logicality/logicality'
|
9
|
+
|
10
|
+
def run(tests)
|
11
|
+
tests.each do |x|
|
12
|
+
input = x[1] ? x[1].map { |k,v| [ k.to_s, v] }.to_h : nil
|
13
|
+
|
14
|
+
result = Logicality::Logic.evaluate(x[0], input)
|
15
|
+
|
16
|
+
expect(result).to eq(x[2]), "Failed on #{x[0]} (input: #{input}): expected #{x[2]} but got: #{result}"
|
17
|
+
end
|
18
|
+
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
describe Logicality::Logic do
|
23
|
+
|
24
|
+
context 'when evaluating' do
|
25
|
+
|
26
|
+
it 'should evaluate boolean-only expressions' do
|
27
|
+
tests = [
|
28
|
+
[ 'true', nil, true ],
|
29
|
+
[ 'false', nil, false ],
|
30
|
+
[ 'true && false', nil, false ],
|
31
|
+
[ 'true && true', nil, true ]
|
32
|
+
]
|
33
|
+
|
34
|
+
run(tests)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should evaluate and expressions' do
|
38
|
+
tests = [
|
39
|
+
[ 'a && b', nil, false ],
|
40
|
+
[ 'a && b', {}, false ],
|
41
|
+
[ 'a && b', { a: true }, false ],
|
42
|
+
[ 'a && b', { a: true, b: false }, false ],
|
43
|
+
[ 'a && b', { a: false, b: false }, false ],
|
44
|
+
[ 'a && b', { a: true, b: true }, true ]
|
45
|
+
]
|
46
|
+
|
47
|
+
run(tests)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should evaluate and-or expressions' do
|
51
|
+
tests = [
|
52
|
+
[ 'a && b || c', { a: false, b: false, c: true }, true ],
|
53
|
+
[ '(a && b) || c', { a: false, b: false, c: true }, true ],
|
54
|
+
[ 'a || b && c', { a: false, b: false, c: true }, false ],
|
55
|
+
[ 'a || (b && c)', { a: false, b: false, c: true }, false ],
|
56
|
+
[ '(a || b) && c', { a: false, b: false, c: true }, false ]
|
57
|
+
]
|
58
|
+
|
59
|
+
run(tests)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should evaluate not expressions' do
|
63
|
+
tests = [
|
64
|
+
[ '!a', { a: false }, true ],
|
65
|
+
[ '!a && !b', { a: false, b: false }, true ],
|
66
|
+
[ '!a && b', { a: false, b: false }, false ],
|
67
|
+
[ 'a && !b', { a: false, b: false }, false ],
|
68
|
+
[ '!(a && b)', { a: false, b: false }, true ]
|
69
|
+
]
|
70
|
+
|
71
|
+
run(tests)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should treat question marks as a valid part of a value token' do
|
75
|
+
tests = [
|
76
|
+
[ 'a?', { 'a?': true }, true ],
|
77
|
+
[ '!a?', { 'a?': true }, false ],
|
78
|
+
[ 'a? && b?', { 'a?': true, 'b?': true }, true ],
|
79
|
+
[ 'a && b?', { a: true, 'b?': true }, true ]
|
80
|
+
]
|
81
|
+
|
82
|
+
run(tests)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: logicality
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthew Ruggio
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-08-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: |2
|
28
|
+
A common problem that many frameworks have is the ability to give developers
|
29
|
+
an expressive intermediary scripting language or DSL.
|
30
|
+
Logicality helps solve this problem by providing a simple boolean
|
31
|
+
expression evaluator.
|
32
|
+
email:
|
33
|
+
- mruggio@bluemarblepayroll.com
|
34
|
+
executables: []
|
35
|
+
extensions: []
|
36
|
+
extra_rdoc_files: []
|
37
|
+
files:
|
38
|
+
- ".editorconfig"
|
39
|
+
- ".gitignore"
|
40
|
+
- ".ruby-version"
|
41
|
+
- ".travis.yml"
|
42
|
+
- Gemfile
|
43
|
+
- Gemfile.lock
|
44
|
+
- LICENSE
|
45
|
+
- README.md
|
46
|
+
- lib/logicality/interpreter/interpreter.rb
|
47
|
+
- lib/logicality/interpreter/node_visitor.rb
|
48
|
+
- lib/logicality/interpreter/simple_interpreter.rb
|
49
|
+
- lib/logicality/lexer/grammar.rb
|
50
|
+
- lib/logicality/lexer/lexer.rb
|
51
|
+
- lib/logicality/lexer/regexp_lexer.rb
|
52
|
+
- lib/logicality/lexer/token.rb
|
53
|
+
- lib/logicality/logic.rb
|
54
|
+
- lib/logicality/logicality.rb
|
55
|
+
- lib/logicality/parser/ast/ast.rb
|
56
|
+
- lib/logicality/parser/ast/binary_operator_node.rb
|
57
|
+
- lib/logicality/parser/ast/node.rb
|
58
|
+
- lib/logicality/parser/ast/unary_operator_node.rb
|
59
|
+
- lib/logicality/parser/ast/value_operand_node.rb
|
60
|
+
- lib/logicality/parser/parser.rb
|
61
|
+
- lib/logicality/parser/simple_parser.rb
|
62
|
+
- lib/logicality/version.rb
|
63
|
+
- logicality.gemspec
|
64
|
+
- spec/logic_spec.rb
|
65
|
+
homepage: https://github.com/bluemarblepayroll/logicality-rb
|
66
|
+
licenses:
|
67
|
+
- MIT
|
68
|
+
metadata: {}
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 2.3.7
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 2.5.2.3
|
86
|
+
signing_key:
|
87
|
+
specification_version: 4
|
88
|
+
summary: String-based boolean expression evaluator
|
89
|
+
test_files:
|
90
|
+
- spec/logic_spec.rb
|