michaelmior /
NoSE
| 1 | # frozen_string_literal: true |
||
| 2 | |||
| 3 | 1 | require 'parslet' |
|
| 4 | |||
| 5 | # rubocop:disable Style/ClassAndModuleChildren |
||
| 6 | |||
| 7 | # Parslet DSL extension for capturing the input source |
||
| 8 | 1 | class CaptureSource < Parslet::Atoms::Capture |
|
| 9 | # Ugly hack to capture the source string that was parsed |
||
| 10 | 1 | def apply(source, context, consume_all) |
|
| 11 | 33 | before = source.instance_variable_get(:@str).rest |
|
| 12 | 33 | success, value = result = super(source, context, consume_all) |
|
| 13 | 33 | if success |
|
| 14 | # Save the portion of the source string |
||
| 15 | 33 | after = source.instance_variable_get(:@str).rest |
|
| 16 | 33 | source_str = before[0..(before.length - after.length - 1)] |
|
| 17 | 33 | value[(name.to_s + '_source').to_sym] = source_str |
|
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
| 18 | end |
||
| 19 | |||
| 20 | 33 | result |
|
| 21 | end |
||
| 22 | end |
||
| 23 | |||
| 24 | # Modify named captures to allow arrays |
||
| 25 | 1 | class Parslet::Atoms::Named < Parslet::Atoms::Base |
|
| 26 | 1 | def initialize(parslet, name, array = false) |
|
|
0 ignored issues
–
show
|
|||
| 27 | 3879 | super() |
|
| 28 | 3879 | @parslet = parslet |
|
| 29 | 3879 | @name = name |
|
| 30 | 3879 | @array = array |
|
| 31 | end |
||
| 32 | |||
| 33 | 1 | private |
|
| 34 | |||
| 35 | # Optionally wrap the produced single value in an array |
||
| 36 | 1 | def produce_return_value(val) |
|
| 37 | 4792 | flatval = flatten(val, true) |
|
| 38 | 4792 | flatval = [flatval] if @array && val.last == [:repetition] |
|
| 39 | 4792 | { name => flatval } |
|
| 40 | end |
||
| 41 | end |
||
| 42 | |||
| 43 | # Extend the DSL to with some additional ways to capture the output |
||
| 44 | 1 | module Parslet::Atoms::DSL |
|
| 45 | # Like #as, but ensures that the result is always an array |
||
| 46 | # @return [Array<Parslet::Atoms::Named>] |
||
| 47 | 1 | def as_array(name) |
|
| 48 | 1039 | Parslet::Atoms::Named.new(self, name, true) |
|
| 49 | end |
||
| 50 | |||
| 51 | # Capture some output along with the source string |
||
| 52 | # @return [CaptureSource] |
||
| 53 | 1 | def capture_source(name) |
|
| 54 | 33 | CaptureSource.new(self, name) |
|
| 55 | end |
||
| 56 | end |
||
| 57 | |||
| 58 | # rubocop:enable Style/ClassAndModuleChildren |
||
| 59 | |||
| 60 | 1 | module NoSE |
|
| 61 | # rubocop:disable Style/BlockEndNewline, Style/BlockDelimiters |
||
| 62 | # rubocop:disable Style/MultilineOperationIndentation |
||
| 63 | |||
| 64 | # Literals used in queries and updates |
||
| 65 | 1 | module Literals |
|
| 66 | 1 | include Parslet |
|
| 67 | |||
| 68 | 247 | rule(:integer) { match('[0-9]').repeat(1).as(:int) } |
|
| 69 | 239 | rule(:quote) { str('"') } |
|
| 70 | 28 | rule(:nonquote) { quote.absent? >> any } |
|
| 71 | 239 | rule(:string) { quote >> nonquote.repeat(1).as(:str) >> quote } |
|
| 72 | 247 | rule(:literal) { integer | string | str('?').as(:unknown) } |
|
| 73 | end |
||
| 74 | |||
| 75 | # Predicates used in queries and updates |
||
| 76 | 1 | module Predicates |
|
| 77 | 1 | include Parslet |
|
| 78 | |||
| 79 | 1 | rule(:operator) { |
|
| 80 | 216 | str('=') | str('!=') | str('<=') | str('>=') | str('<') | str('>') } |
|
| 81 | 1 | rule(:condition) { |
|
| 82 | 216 | field.as(:field) >> space? >> operator.as(:op) >> space? >> |
|
| 83 | literal.as(:value) } |
||
| 84 | 1 | rule(:expression) { |
|
| 85 | 216 | condition >> (space >> str('AND') >> space >> expression).repeat } |
|
| 86 | 1 | rule(:where) { |
|
| 87 | 216 | space >> str('WHERE') >> space >> expression.as_array(:expression) } |
|
| 88 | end |
||
| 89 | |||
| 90 | # Identifiers and combinations of them used in queries and updates |
||
| 91 | 1 | module Identifiers |
|
| 92 | 1 | include Parslet |
|
| 93 | |||
| 94 | 247 | rule(:identifier) { match('[A-z]').repeat(1).as(:identifier) } |
|
| 95 | 217 | rule(:field) { identifier >> (str('.') >> identifier).repeat(1) } |
|
| 96 | 42 | rule(:fields) { field >> (comma >> field).repeat } |
|
| 97 | 1 | rule(:select_field) { |
|
| 98 | 183 | field.as_array(:field) | (identifier >> str('.') >> |
|
| 99 | str('*').repeat(1, 2).as(:identifier2)) } |
||
| 100 | 184 | rule(:select_fields) { select_field >> (comma >> select_field).repeat } |
|
| 101 | 200 | rule(:path) { identifier >> (str('.') >> identifier).repeat } |
|
| 102 | end |
||
| 103 | |||
| 104 | # Field settings for update and insert statements |
||
| 105 | 1 | module UpdateSettings |
|
| 106 | 1 | include Parslet |
|
| 107 | |||
| 108 | 1 | rule(:setting) { |
|
| 109 | 41 | (identifier | str('**')).as(:field) >> space? >> str('=') >> space? >> |
|
| 110 | literal.as(:value) |
||
| 111 | } |
||
| 112 | 1 | rule(:settings) { |
|
| 113 | 41 | setting >> (space? >> str(',') >> space? >> setting).repeat |
|
| 114 | } |
||
| 115 | end |
||
| 116 | |||
| 117 | # Parser for a simple CQL-like grammar |
||
| 118 | 1 | class CQLP < Parslet::Parser |
|
| 119 | 1 | include Literals |
|
| 120 | 1 | include Identifiers |
|
| 121 | 1 | include Predicates |
|
| 122 | 1 | include UpdateSettings |
|
| 123 | |||
| 124 | 247 | rule(:space) { match('\s').repeat(1) } |
|
| 125 | 247 | rule(:space?) { space.maybe } |
|
| 126 | 184 | rule(:comma) { str(',') >> space? } |
|
| 127 | |||
| 128 | 184 | rule(:limit) { space >> str('LIMIT') >> space >> integer.as(:limit) } |
|
| 129 | 1 | rule(:order) { |
|
| 130 | 183 | space >> str('ORDER BY') >> space >> fields.as_array(:fields) } |
|
| 131 | |||
| 132 | 234 | rule(:comment) { str(' -- ') >> match('.').repeat } |
|
| 133 | |||
| 134 | 1 | rule(:query) { |
|
| 135 | 183 | str('SELECT') >> space >> select_fields.as_array(:select) >> |
|
| 136 | space >> str('FROM') >> space >> path.as_array(:path) >> |
||
| 137 | where.maybe.as(:where) >> order.maybe.as(:order) >> |
||
| 138 | limit.maybe.capture(:limit) >> comment.maybe.as(:comment) } |
||
| 139 | |||
| 140 | 1 | rule(:update) { |
|
| 141 | 24 | str('UPDATE') >> space >> identifier.as(:entity) >> space >> |
|
| 142 | 24 | (str('FROM') >> space >> path.as_array(:path) >> space).maybe >> |
|
| 143 | str('SET') >> space >> settings.as_array(:settings) >> |
||
| 144 | where.maybe.as(:where).capture_source(:where) >> |
||
| 145 | comment.maybe.as(:comment) |
||
| 146 | } |
||
| 147 | |||
| 148 | 1 | rule(:connect_item) { |
|
| 149 | 30 | identifier.as(:target) >> space? >> str('(') >> space? >> |
|
| 150 | literal.as(:target_pk) >> space? >> str(')') |
||
| 151 | } |
||
| 152 | |||
| 153 | 1 | rule(:connect_list) { |
|
| 154 | 17 | connect_item >> (space? >> str(',') >> space? >> connect_item).repeat |
|
| 155 | } |
||
| 156 | |||
| 157 | 1 | rule(:insert) { |
|
| 158 | 17 | str('INSERT INTO') >> space >> identifier.as(:entity) >> space >> |
|
| 159 | str('SET') >> space >> settings.as_array(:settings) >> |
||
| 160 | (space >> str('AND') >> space >> str('CONNECT') >> space >> |
||
| 161 | str('TO') >> space >> connect_list.as_array(:connections)).maybe >> |
||
| 162 | comment.maybe.as(:comment) |
||
| 163 | } |
||
| 164 | |||
| 165 | 1 | rule(:delete) { |
|
| 166 | 9 | str('DELETE') >> space >> identifier.as(:entity) >> |
|
| 167 | 9 | (space >> str('FROM') >> space >> path.as_array(:path)).maybe >> |
|
| 168 | where.maybe.as(:where).capture_source(:where) >> |
||
| 169 | comment.maybe.as(:comment) |
||
| 170 | } |
||
| 171 | |||
| 172 | 1 | rule(:connect) { |
|
| 173 | 13 | (str('CONNECT') | str('DISCONNECT')).capture(:type) >> space >> |
|
| 174 | identifier.as(:entity) >> space? >> str('(') >> space? >> |
||
| 175 | literal.as(:source_pk) >> space? >> str(')') >> space >> |
||
| 176 | dynamic do |_, context| |
||
| 177 | 13 | context.captures[:type] == 'CONNECT' ? str('TO') : str('FROM') |
|
| 178 | end >> space >> connect_item |
||
| 179 | } |
||
| 180 | |||
| 181 | 1 | rule(:statement) { |
|
| 182 | query | update | insert | delete | connect |
||
| 183 | } |
||
| 184 | |||
| 185 | 1 | root :statement |
|
| 186 | end |
||
| 187 | |||
| 188 | # Simple transformations to clean up the CQL parse tree |
||
| 189 | 1 | class CQLT < Parslet::Transform |
|
| 190 | 1520 | rule(identifier: simple(:identifier)) { identifier } |
|
| 191 | 1 | rule(identifier: simple(:identifier), identifier2: simple(:identifier2)) { |
|
| 192 | 40 | [identifier.to_s, identifier2.to_s] } |
|
| 193 | 203 | rule(field: sequence(:id)) { id.map(&:to_s) } |
|
| 194 | 1 | rule(path: sequence(:id)) { id.map(&:to_s) } |
|
| 195 | 57 | rule(str: simple(:string)) { string.to_s } |
|
| 196 | 1 | rule(statement: subtree(:stmt)) { stmt.first.last } |
|
| 197 | 37 | rule(int: simple(:integer)) { integer } |
|
| 198 | 364 | rule(unknown: simple(:val)) { nil } |
|
| 199 | end |
||
| 200 | |||
| 201 | # rubocop:enable all |
||
| 202 | end |
||
| 203 |