Issues (393)

lib/nose/parser.rb (2 issues)

Severity
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
Prefer string interpolation to string concatenation.
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
Prefer keyword arguments for arguments with a boolean default value; use array: false instead of array = false.
Loading history...
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