expressionist 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []