expressionist 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f849377088fcff72b0ccd7d6b8c82e4f4465b2ad142e5fce4bd11080389bdb09
4
- data.tar.gz: c7cc2728aa2d5b0e5dbcff4350f819a9cebf3f19785a86b6b0196e491828264b
3
+ metadata.gz: 1da1a2ceee72ee773a38c0473c4b204274369bcaeb9ee5b80de93b6d8fcc84e2
4
+ data.tar.gz: c657865ed1042f95856f2acfb1beec2adb0667469e3c3eb23cc93b672296bfd4
5
5
  SHA512:
6
- metadata.gz: e6d1de63704129958bf171aa089a8139ad0cfeac68ae736ee40f6104b4bc3a5c6796614c0f702e644ba63dba3796c80ef2bdb8d279d8feb11eae5329637ff1e8
7
- data.tar.gz: cb512ff6dff6b079bd59fdaa1cce4291b7797c631c65848fdb2fa00501c2146714e3a0c519596be79256371e5c78af13703518ad3c799e00343595efc3e4d60e
6
+ metadata.gz: adcea62b1ca67c192f7ad89e1b3a1516b5074d8e42215934dfa33885767465fe2a36d5ea52b704babaf63a2685a75afbd15e8ab8db76a5392f315994976e7c7d
7
+ data.tar.gz: 97788da9e6736afa85d186ecb835083f4be7de33168a1fea4d364ac52c5320edda58cf0f60dc7b495fe7b9fa2ef8f954ba584ab22906d854e368898ee5724e9b
data/.gitignore CHANGED
@@ -7,4 +7,5 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
  /.idea
10
- *.iml
10
+ *.iml
11
+ /*.gem
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- expressionist (0.0.1)
4
+ expressionist (0.0.2)
5
5
  parslet (~> 1.8)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Expressionist
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/expressionist`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Simple query language for tree structured data
6
4
 
7
5
  ## Installation
8
6
 
@@ -22,18 +20,63 @@ Or install it yourself as:
22
20
 
23
21
  ## Usage
24
22
 
25
- TODO: Write usage instructions here
23
+ ```ruby
24
+ require 'expressionist'
25
+
26
+ context = Expressionist::Context.new
27
+
28
+ context['some.key'] = 'value'
29
+
30
+ expression = Expressionist.compile('some.key == "value" AND count(some.*) == 1')
31
+
32
+ expression.call(context) # => true
33
+
34
+ context['some.key2'] = 'value'
35
+
36
+ expression.call(context) # => false
37
+
38
+ context.export # => {"some"=>{"key"=>{"."=>"value"}, "key2"=>{"."=>"value"}}}
39
+
40
+ tmp = expression.executable # => Array
41
+
42
+ another = Expressionist.compile(tmp)
43
+
44
+ another.call(context) # => false
45
+ ```
46
+
47
+ ## Contexts
48
+
49
+ Context defines tree data structure that can be queried. Data can be importad
50
+ from an existing `Hash` and can be exported using `context.export`.
51
+
52
+ The `.` is used to separate the path. There can never be two `.` next to each
53
+ other and path can not start with a `.`. The `.` character can not be escaped.
54
+
55
+ Paths can only contain these `/[A-Za-z0-9.\-_]/` characters.
56
+
57
+ ## Query language (expressions)
58
+
59
+ ToDo: update the section, ATM check the `lib/expressionist/grammar.rb` and
60
+ `lib/expressionist/functions/*.rb` files.
26
61
 
27
62
  ## Development
28
63
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
64
+ After checking out the repo, run `bin/setup` to install dependencies. Then,
65
+ run `rake test` to run the tests. You can also run `bin/console` for an
66
+ interactive prompt that will allow you to experiment.
30
67
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
68
+ To install this gem onto your local machine, run `bundle exec rake install`.
69
+ To release a new version, update the version number in `version.rb`,
70
+ and then run `bundle exec rake release`, which will create a git tag for the
71
+ version, push git commits and tags, and push the `.gem` file to
72
+ [rubygems.org](https://rubygems.org).
32
73
 
33
74
  ## Contributing
34
75
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/expressionist.
76
+ Bug reports and pull requests are welcome on GitHub at
77
+ [marekjelen/expressionist](https://github.com/marekjelen/expressionist).
36
78
 
37
79
  ## License
38
80
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
81
+ The gem is available as open source under the terms of the
82
+ [MIT License](https://opensource.org/licenses/MIT).
@@ -11,8 +11,9 @@ Gem::Specification.new do |spec|
11
11
  spec.authors = ['Marek Jelen']
12
12
  spec.email = ['marek@jelen.biz']
13
13
 
14
- spec.summary = 'Simple query language for querying tree data structure'
15
- spec.description = 'Simple query language for querying tree data structure'
14
+ spec.summary = 'Simple query language for tree structured data'
15
+ spec.description = 'Safely make queries against tree data structure with ' \
16
+ 'ability to serialize queries for later usage'
16
17
  spec.homepage = 'https://www.github.com/marekjelen/expressionist'
17
18
  spec.license = 'MIT'
18
19
 
@@ -1,6 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'parslet'
4
+
3
5
  require 'expressionist/version'
6
+ require 'expressionist/context'
7
+ require 'expressionist/expression'
8
+ require 'expressionist/parser'
4
9
 
5
10
  module Expressionist
6
- end
11
+
12
+ def self.compile(expression)
13
+ Expression.new(Parser.new(expression))
14
+ end
15
+
16
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressionist
4
+
5
+ class Context
6
+
7
+ def initialize(data = {})
8
+ @data = data
9
+ end
10
+
11
+ def []=(key, value)
12
+ segments = key.split('.')
13
+ len = segments.length
14
+ data = @data
15
+ (0...len).each do |i|
16
+ key = segments[i]
17
+ data = (data[key] ||= {})
18
+ end
19
+ data['.'] = value
20
+ end
21
+
22
+ def delete(key)
23
+ segments = key.split('.')
24
+ len = segments.length
25
+ data = @data
26
+ (0...len).each do |i|
27
+ break unless data
28
+ data = data[segments[i]]
29
+ end
30
+ data.delete('.') if data
31
+ end
32
+
33
+ def [](key)
34
+ find(key.split('.'), @data)
35
+ end
36
+
37
+ def find(segments = [], data = nil)
38
+ data ||= @data
39
+ segment = segments[0]
40
+ subsegments = segments[1..-1]
41
+
42
+ case
43
+ when data == nil
44
+ [nil]
45
+ when segments.length == 0
46
+ [cast(data['.'])]
47
+ when segment == '?'
48
+ (data.keys - ['.']).map do |k|
49
+ find(subsegments, data[k])
50
+ end
51
+ when segment == '*' && subsegments.length > 0 && data[subsegments[0]]
52
+ find(subsegments[1..-1], data[subsegments[0]])
53
+ when segment == '*'
54
+ (subsegments.length == 0 ? [data['.']] : []) + (data.keys - ['.']).map do |k|
55
+ find(segments, data[k])
56
+ end
57
+ else
58
+ find(subsegments, data[segment])
59
+ end.flatten.compact
60
+ end
61
+
62
+ def export
63
+ @data
64
+ end
65
+
66
+ def cast(value)
67
+ case value
68
+ when 'true'
69
+ true
70
+ when 'false'
71
+ false
72
+ when /[0-9]+[.,][0-9]+/
73
+ Float(value)
74
+ when /[0-9]+/
75
+ Integer(value)
76
+ else
77
+ value
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'expressionist/function'
4
+ require 'expressionist/functions'
5
+
6
+ module Expressionist
7
+
8
+ class Expression
9
+
10
+ def initialize(value)
11
+ if value.kind_of?(Parser)
12
+ @parser = value
13
+ else
14
+ @executable = value
15
+ end
16
+ end
17
+
18
+ def executable
19
+ @executable ||= @parser.executable
20
+ end
21
+
22
+ def call(context = {}, executable = nil)
23
+ executable ||= self.executable
24
+
25
+ args = executable[1..-1].map do |exp|
26
+ if exp.kind_of?(Array)
27
+ call(context, exp)
28
+ else
29
+ exp
30
+ end
31
+ end
32
+
33
+ function = Functions.get(executable[0])
34
+ unless function
35
+ raise ArgumentError, "Function #{executable[0].inspect} is missing"
36
+ end
37
+
38
+ function.call(context, *args)
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressionist
4
+
5
+ class Function
6
+
7
+ attr_reader :name, :args
8
+
9
+ def initialize(name, *args)
10
+ if name.kind_of?(Array) && args.length == 0
11
+ @name = name[0]
12
+ @args = name[1..-1].map do |arg|
13
+ arg.kind_of?(Array) ? Function.new(arg) : arg
14
+ end
15
+ else
16
+ @name = name
17
+ @args = args
18
+ end
19
+ end
20
+
21
+ def to_a
22
+ [name] + args.map { |arg| arg.kind_of?(Function) ? arg.to_a : arg }
23
+ end
24
+
25
+ def to_s
26
+ "#{name}(#{args.map(&to_s).join(', ')})"
27
+ end
28
+
29
+ def ==(other)
30
+ self.class == other.class && other.name == self.name && other.args == self.args
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressionist
4
+
5
+ module Functions
6
+
7
+ class << self
8
+
9
+ def functions
10
+ @functions ||= {}
11
+ end
12
+
13
+ def add(name, &block)
14
+ functions[name] = block
15
+ end
16
+
17
+ def get(name)
18
+ functions[name]
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ require 'expressionist/functions/core'
28
+ require 'expressionist/functions/operators'
29
+ require 'expressionist/functions/boolean'
30
+ require 'expressionist/functions/math'
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressionist
4
+ module Functions
5
+
6
+ add('and') do |context, *items|
7
+ items.inject(true) {|c, i| c && i}
8
+ end
9
+
10
+ add('or') do |context, *items|
11
+ items.inject(false) {|c, i| c || i}
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressionist
4
+ module Functions
5
+
6
+ add('get') do |context, *segments|
7
+ context.find(segments).first
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressionist
4
+ module Functions
5
+
6
+ add('max') do |context, *segments|
7
+ context.find(segments).max
8
+ end
9
+
10
+ add('count') do |context, *segments|
11
+ context.find(segments).length
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressionist
4
+
5
+ module Functions
6
+
7
+ add('>') do |context, left, right|
8
+ left > right
9
+ end
10
+
11
+ add('>=') do |context, left, right|
12
+ left >= right
13
+ end
14
+
15
+ add('=') do |context, left, right|
16
+ left == right
17
+ end
18
+
19
+ add('==') do |context, left, right|
20
+ left == right
21
+ end
22
+
23
+ add('!=') do |context, left, right|
24
+ left != right
25
+ end
26
+
27
+ add('<=') do |context, left, right|
28
+ left <= right
29
+ end
30
+
31
+ add('<') do |context, left, right|
32
+ left < right
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressionist
4
+
5
+ class Grammar < Parslet::Parser
6
+
7
+ rule(:space) {match('\s').repeat(1)}
8
+ rule(:space?) {space.maybe}
9
+
10
+ rule(:lparen) {space? >> str('(') >> space?}
11
+ rule(:rparen) {space? >> str(')') >> space?}
12
+
13
+ rule(:comma) {space? >> str(',') >> space?}
14
+ rule(:dot) {space? >> str('.') >> space?}
15
+
16
+ rule(:lquote) {space? >> str('"')}
17
+ rule(:rquote) {str('"') >> space?}
18
+
19
+ rule(:ande) {space? >> str('AND').as(:boolean_operator) >> space?}
20
+ rule(:ore) {space? >> str('OR').as(:boolean_operator) >> space?}
21
+
22
+ rule(:anum) {match('[0-9a-zA-Z]')}
23
+ rule(:anums) {match('[.-_]') >> anum}
24
+ rule(:sanums) {match('[-_]') >> anum}
25
+
26
+ rule(:quoted) {(str('\"').as(:char) | (str('"').absent? >> any).as(:char)).repeat}
27
+ rule(:string) {lquote >> quoted.as(:string) >> rquote}
28
+ rule(:integer) {match('[0-9]').repeat(1).as(:integer)}
29
+ rule(:float) {(match('[0-9a-zA-Z]').repeat(1) >> str('.') >> match('[0-9a-zA-Z]').repeat(1)).as(:float)}
30
+ rule(:bool) {(str('true') | str('false')).as(:bool)}
31
+
32
+ rule(:keyword) {anum >> (anum | sanums).repeat}
33
+
34
+ rule(:segment) { str('*').as(:segment) | str('?').as(:segment) | ((anum >> (anum | sanums).repeat).as(:segment)) }
35
+ rule(:path) {(segment >> (dot >> segment).repeat).as(:path)}
36
+
37
+ rule(:func) {keyword.as(:function) >> lparen >> path >> rparen}
38
+
39
+ rule(:operator) {str('>=') | str('<=') | str('!=') | str('==') | str('=') | str('<') | str('>')}
40
+
41
+ rule(:value) {string | bool | float | integer}
42
+
43
+ rule(:expression) do
44
+ space? >>
45
+ (func.as(:func) | path) >>
46
+ space? >>
47
+ operator.as(:operator) >>
48
+ space? >>
49
+ value.as(:value) >>
50
+ space?
51
+ end
52
+
53
+ rule(:primary) { lparen >> or_expression >> rparen | expression }
54
+
55
+ rule(:and_expression) { (primary.as(:left) >> ande >> and_expression.as(:right)).as(:and) | primary }
56
+
57
+ rule(:or_expression) { (and_expression.as(:left) >> ore >> or_expression.as(:right)).as(:or) | and_expression }
58
+
59
+ rule(:base) { or_expression }
60
+ # https://github.com/kschiess/parslet/blob/master/example/boolean_algebra.rb
61
+
62
+ root(:base)
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'expressionist/grammar'
4
+ require 'expressionist/transformer'
5
+
6
+ module Expressionist
7
+
8
+ class Parser
9
+
10
+ def initialize(expression)
11
+ @parsed = nil
12
+ @compiled = nil
13
+ @executable = nil
14
+
15
+ case expression
16
+ when String
17
+ @expression = expression
18
+ when Function
19
+ @compiled = expression
20
+ when Array
21
+ @executable = expression
22
+ else
23
+ raise ArgumentError, "Unknown expression type: #{expression}"
24
+ end
25
+ end
26
+
27
+ def parsed
28
+ raise RuntimeError, 'Expression was not passed' unless @expression
29
+ @parsed ||= Grammar.new.parse(@expression)
30
+ rescue Parslet::ParseFailed => e
31
+ # puts expression
32
+ # puts e.parse_failure_cause.ascii_tree
33
+ raise ArgumentError, "Invalid expression: #{@expression}"
34
+ end
35
+
36
+ def compiled
37
+ raise RuntimeError, 'Raw nor compiled form was passed' unless @compiled || parsed
38
+ @compiled ||= Transformer.new.apply(parsed)
39
+ end
40
+
41
+ def executable
42
+ raise RuntimeError, 'Compiled nor executable form was passed' unless @executable || compiled
43
+ @executable ||= compiled.to_a
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressionist
4
+
5
+ class Transformer < Parslet::Transform
6
+
7
+ rule(integer: simple(:value)) { Integer(value) }
8
+ rule(float: simple(:value)) { Float(value) }
9
+ rule(integer: simple(:value)) { Integer(value) }
10
+ rule(bool: simple(:value)) { ['false'].include?(value) ? false : true }
11
+
12
+ rule(char: simple(:value)) { value == '\\"' ? '"' : value }
13
+ rule(string: sequence(:value)) { value.join }
14
+
15
+ rule(segment: simple(:value)) { value.str }
16
+
17
+ rule(path: sequence(:value)) { value }
18
+ rule(path: simple(:value)) { value }
19
+
20
+ rule(function: simple(:name), path: simple(:path)) do
21
+ Function.new(name.str, path)
22
+ end
23
+
24
+ rule(function: simple(:name), path: sequence(:path)) do
25
+ Function.new(name.str, *path)
26
+ end
27
+
28
+ rule(operator: simple(:op), value: simple(:value), func: simple(:func)) do
29
+ Function.new(op.str, func, value)
30
+ end
31
+
32
+ rule(operator: simple(:op), value: simple(:value), path: simple(:path)) do
33
+ Function.new(op.str, Function.new('get', path), value)
34
+ end
35
+
36
+ rule(operator: simple(:op), value: simple(:value), path: sequence(:path)) do
37
+ Function.new(op.str, Function.new('get', *path), value)
38
+ end
39
+
40
+ rule(expression: simple(:value)) { value }
41
+
42
+ rule(boolean_operator: simple(:value), left: simple(:left), right: simple(:right)) do
43
+ Function.new(value.str.strip.downcase, left, right)
44
+ end
45
+
46
+ rule(and: simple(:value)) { value }
47
+ rule(or: simple(:value)) { value }
48
+
49
+ end
50
+
51
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  #
3
3
  module Expressionist
4
- VERSION = '0.0.1'
4
+ VERSION = '0.0.2'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: expressionist
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marek Jelen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-07-24 00:00:00.000000000 Z
11
+ date: 2018-07-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parslet
@@ -80,7 +80,8 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '10.0'
83
- description: Simple query language for querying tree data structure
83
+ description: Safely make queries against tree data structure with ability to serialize
84
+ queries for later usage
84
85
  email:
85
86
  - marek@jelen.biz
86
87
  executables: []
@@ -98,6 +99,17 @@ files:
98
99
  - bin/setup
99
100
  - expressionist.gemspec
100
101
  - lib/expressionist.rb
102
+ - lib/expressionist/context.rb
103
+ - lib/expressionist/expression.rb
104
+ - lib/expressionist/function.rb
105
+ - lib/expressionist/functions.rb
106
+ - lib/expressionist/functions/boolean.rb
107
+ - lib/expressionist/functions/core.rb
108
+ - lib/expressionist/functions/math.rb
109
+ - lib/expressionist/functions/operators.rb
110
+ - lib/expressionist/grammar.rb
111
+ - lib/expressionist/parser.rb
112
+ - lib/expressionist/transformer.rb
101
113
  - lib/expressionist/version.rb
102
114
  homepage: https://www.github.com/marekjelen/expressionist
103
115
  licenses:
@@ -122,5 +134,5 @@ rubyforge_project:
122
134
  rubygems_version: 2.7.6
123
135
  signing_key:
124
136
  specification_version: 4
125
- summary: Simple query language for querying tree data structure
137
+ summary: Simple query language for tree structured data
126
138
  test_files: []