ruby-grafana-reporter 0.1.7 → 0.4.0

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +86 -245
  3. data/bin/ruby-grafana-reporter +3 -2
  4. data/lib/VERSION.rb +6 -3
  5. data/lib/grafana/abstract_datasource.rb +116 -0
  6. data/lib/grafana/dashboard.rb +75 -66
  7. data/lib/grafana/errors.rb +81 -61
  8. data/lib/grafana/grafana.rb +130 -131
  9. data/lib/grafana/grafana_alerts_datasource.rb +57 -0
  10. data/lib/grafana/grafana_annotations_datasource.rb +56 -0
  11. data/lib/grafana/grafana_property_datasource.rb +25 -0
  12. data/lib/grafana/graphite_datasource.rb +44 -0
  13. data/lib/grafana/image_rendering_datasource.rb +44 -0
  14. data/lib/grafana/panel.rb +47 -39
  15. data/lib/grafana/prometheus_datasource.rb +39 -0
  16. data/lib/grafana/sql_datasource.rb +65 -0
  17. data/lib/grafana/variable.rb +218 -259
  18. data/lib/grafana/webrequest.rb +71 -0
  19. data/lib/grafana_reporter/abstract_query.rb +401 -0
  20. data/lib/grafana_reporter/abstract_report.rb +163 -109
  21. data/lib/grafana_reporter/alerts_table_query.rb +44 -0
  22. data/lib/grafana_reporter/annotations_table_query.rb +43 -0
  23. data/lib/grafana_reporter/application/application.rb +162 -229
  24. data/lib/grafana_reporter/application/errors.rb +33 -30
  25. data/lib/grafana_reporter/application/webservice.rb +242 -0
  26. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +90 -0
  27. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +89 -0
  28. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +76 -0
  29. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +77 -0
  30. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +72 -0
  31. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +98 -0
  32. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +93 -0
  33. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +23 -0
  34. data/lib/grafana_reporter/asciidoctor/report.rb +172 -159
  35. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +46 -0
  36. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +35 -0
  37. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +92 -0
  38. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +88 -0
  39. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
  40. data/lib/grafana_reporter/configuration.rb +310 -326
  41. data/lib/grafana_reporter/console_configuration_wizard.rb +319 -0
  42. data/lib/grafana_reporter/demo_report_wizard.rb +87 -0
  43. data/lib/grafana_reporter/errors.rb +81 -38
  44. data/lib/grafana_reporter/help.rb +447 -0
  45. data/lib/grafana_reporter/logger/two_way_logger.rb +58 -52
  46. data/lib/grafana_reporter/panel_image_query.rb +29 -0
  47. data/lib/grafana_reporter/panel_property_query.rb +22 -0
  48. data/lib/grafana_reporter/query_value_query.rb +79 -0
  49. data/lib/grafana_reporter/report_webhook.rb +35 -0
  50. data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +29 -27
  51. metadata +48 -60
  52. data/lib/grafana/abstract_panel_query.rb +0 -20
  53. data/lib/grafana/abstract_query.rb +0 -127
  54. data/lib/grafana/abstract_sql_query.rb +0 -42
  55. data/lib/grafana/panel_image_query.rb +0 -49
  56. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -99
  57. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -96
  58. data/lib/grafana_reporter/asciidoctor/errors.rb +0 -37
  59. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -86
  60. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -86
  61. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -67
  62. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -65
  63. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -58
  64. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -75
  65. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -70
  66. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -18
  67. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -41
  68. data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -202
  69. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -67
  70. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -65
  71. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -57
  72. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -32
  73. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -23
  74. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -43
  75. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -36
  76. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -309
  77. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -34
  78. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +0 -32
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # This class standardizes all webcalls. Key functionality is to properly support HTTPS calls as a base functionality.
5
+ class WebRequest
6
+ attr_accessor :relative_url, :options
7
+
8
+ @ssl_cert = nil
9
+
10
+ class << self
11
+ attr_accessor :ssl_cert
12
+ end
13
+
14
+ # Initializes a specific HTTP request.
15
+ #
16
+ # Default (can be overridden, by specifying the options Hash):
17
+ # accept: 'application/json'
18
+ # request: Net::HTTP::Get
19
+ # content_type: 'application/json'
20
+ #
21
+ # @param base_url [String] URL which shall be queried
22
+ # @param options [Hash] options, which shall be merged to the request. Also allows `+logger+` option
23
+ def initialize(base_url, options = {})
24
+ @base_url = base_url
25
+ default_options = { accept: 'application/json', request: Net::HTTP::Get, content_type: 'application/json' }
26
+ @options = default_options.merge(options.reject { |k, _v| k == :logger && k == :relative_url })
27
+ @relative_url = options[:relative_url]
28
+ @logger = options[:logger] || Logger.new(nil)
29
+ end
30
+
31
+ # Executes the HTTP request
32
+ #
33
+ # @param timeout [Integer] number of seconds to wait, before the http request is cancelled, defaults to 60 seconds
34
+ # @return [Response] HTTP response object
35
+ def execute(timeout = nil)
36
+ timeout ||= 60
37
+
38
+ uri = URI.parse("#{@base_url}#{@relative_url}")
39
+ @http = Net::HTTP.new(uri.host, uri.port)
40
+ configure_ssl if @base_url =~ /^https/
41
+
42
+ @http.read_timeout = timeout.to_i
43
+
44
+ request = @options[:request].new(uri.request_uri)
45
+ request['Accept'] = @options[:accept] if @options[:accept]
46
+ request['Content-Type'] = @options[:content_type] if @options[:content_type]
47
+ request['Authorization'] = @options[:authorization] if @options[:authorization]
48
+ request.body = @options[:body]
49
+
50
+ @logger.debug("Requesting #{uri} with '#{@options[:body]}' and timeout '#{timeout}'")
51
+ response = @http.request(request)
52
+ @logger.debug("Received response #{response}")
53
+
54
+ response
55
+ end
56
+
57
+ private
58
+
59
+ def configure_ssl
60
+ @http.use_ssl = true
61
+ @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
62
+ if self.class.ssl_cert && !File.exist?(self.class.ssl_cert)
63
+ @logger.warn('SSL certificate file does not exist.')
64
+ elsif self.class.ssl_cert
65
+ @http.cert_store = OpenSSL::X509::Store.new
66
+ @http.cert_store.set_default_paths
67
+ @http.cert_store.add_file(self.class.ssl_cert)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,401 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ # @abstract Override {#pre_process}, {#post_process} and {#self.build_demo_entry} in subclass.
5
+ #
6
+ # Superclass containing everything for all queries towards grafana.
7
+ class AbstractQuery
8
+ attr_accessor :datasource, :timeout, :from, :to
9
+ attr_writer :raw_query
10
+ attr_reader :variables, :result, :panel
11
+
12
+ # @param grafana_or_panel [Object] {Grafana} or {Panel} object for which the query is executed
13
+ def initialize(grafana_or_panel)
14
+ if grafana_or_panel.is_a?(Grafana::Panel)
15
+ @panel = grafana_or_panel
16
+ @grafana = @panel.dashboard.grafana
17
+ else
18
+ @grafana = grafana_or_panel
19
+ end
20
+ @variables = {}
21
+ end
22
+
23
+ # @abstract
24
+ #
25
+ # Runs the whole process to receive values properly from this query:
26
+ # - calls {#pre_process}
27
+ # - executes this query against the {Grafana} instance
28
+ # - calls {#post_process}
29
+ #
30
+ # @return [Hash] result of the query in standardized format
31
+ def execute
32
+ return @result unless @result.nil?
33
+
34
+ pre_process
35
+ @result = @datasource.request(from: @from, to: @to, raw_query: raw_query, variables: grafana_variables,
36
+ prepared_request: @grafana.prepare_request, timeout: timeout)
37
+ post_process
38
+ @result
39
+ end
40
+
41
+ # Sets default configurations from the given {Dashboard} and store them as settings in the query.
42
+ #
43
+ # Following data is extracted:
44
+ # - +from+, by {Dashboard#from_time}
45
+ # - +to+, by {Dashboard#to_time}
46
+ # - and all variables as {Variable}, prefixed with +var-+, as grafana also does it
47
+ # @param dashboard [Dashboard] dashboard from which the defaults are captured
48
+ def set_defaults_from_dashboard(dashboard)
49
+ @from = dashboard.from_time
50
+ @to = dashboard.to_time
51
+ dashboard.variables.each { |item| merge_variables({ "var-#{item.name}": item }) }
52
+ end
53
+
54
+ # Overwrite this function to extract a proper raw query value from this object.
55
+ #
56
+ # If the property +@raw_query+ is not set manually by the calling object, this
57
+ # method may be overwritten to extract the raw query from this object instead.
58
+ def raw_query
59
+ @raw_query
60
+ end
61
+
62
+ # @abstract
63
+ #
64
+ # Overwrite this function to perform all necessary actions, before the query is actually executed.
65
+ # Here you can e.g. set values of variables or similar.
66
+ #
67
+ # Especially for direct queries, it is essential to set the +@datasource+ variable at latest here in the
68
+ # subclass.
69
+ def pre_process
70
+ raise NotImplementedError
71
+ end
72
+
73
+ # @abstract
74
+ #
75
+ # Use this function to format the raw result of the @result variable to conform to the expected return value.
76
+ def post_process
77
+ raise NotImplementedError
78
+ end
79
+
80
+ # Merges the given hashes to the current object by using the {#merge_variables} method.
81
+ # It respects the priorities of the hashes and the object and allows only valid variables to be passed.
82
+ # @param document_hash [Hash] variables from report template level
83
+ # @param item_hash [Hash] variables from item configuration level, i.e. specific call, which may override document
84
+ # @return [void]
85
+ # TODO: rename method
86
+ def merge_hash_variables(document_hash, item_hash)
87
+ sel_doc_items = document_hash.select do |k, _v|
88
+ k =~ /^var-/ || k == 'grafana-report-timestamp' || k =~ /grafana_default_(?:from|to)_timezone/
89
+ end
90
+ merge_variables(sel_doc_items.each_with_object({}) { |(k, v), h| h[k] = ::Grafana::Variable.new(v) })
91
+
92
+ sel_items = item_hash.select do |k, _v|
93
+ # TODO: specify accepted options in each class or check if simply all can be allowed with prefix +var-+
94
+ k =~ /^var-/ || k =~ /^render-/ || k =~ /filter_columns|format|replace_values_.*|transpose|column_divider|
95
+ row_divider|from_timezone|to_timezone|result_type|query/x
96
+ end
97
+ merge_variables(sel_items.each_with_object({}) { |(k, v), h| h[k] = ::Grafana::Variable.new(v) })
98
+
99
+ @timeout = item_hash['timeout'] || document_hash['grafana-default-timeout'] || @timeout
100
+ @from = item_hash['from'] || document_hash['from'] || @from
101
+ @to = item_hash['to'] || document_hash['to'] || @to
102
+ end
103
+
104
+ # Transposes the given result.
105
+ #
106
+ # NOTE: Only the +:content+ of the given result hash is transposed. The +:header+ is ignored.
107
+ #
108
+ # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#preformat_response})
109
+ # @param transpose_variable [Grafana::Variable] true, if the result hash shall be transposed
110
+ # @return [Hash] transposed query result
111
+ def transpose(result, transpose_variable)
112
+ return result unless transpose_variable
113
+ return result unless transpose_variable.raw_value == 'true'
114
+
115
+ result[:content] = result[:content].transpose
116
+
117
+ result
118
+ end
119
+
120
+ # Filters columns out of the query result.
121
+ #
122
+ # Multiple columns may be filtered. Therefore the column titles have to be named in the
123
+ # {Grafana::Variable#raw_value} and have to be separated by +,+ (comma).
124
+ # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#preformat_response})
125
+ # @param filter_columns_variable [Grafana::Variable] column names, which shall be removed in the query result
126
+ # @return [Hash] filtered query result
127
+ def filter_columns(result, filter_columns_variable)
128
+ return result unless filter_columns_variable
129
+
130
+ filter_columns = filter_columns_variable.raw_value
131
+ filter_columns.split(',').each do |filter_column|
132
+ pos = result[:header][0].index(filter_column)
133
+
134
+ unless pos.nil?
135
+ result[:header][0].delete_at(pos)
136
+ result[:content].each { |row| row.delete_at(pos) }
137
+ end
138
+ end
139
+
140
+ result
141
+ end
142
+
143
+ # Uses the Kernel#format method to format values in the query results.
144
+ #
145
+ # The formatting will be applied separately for every column. Therefore the column formats have to be named
146
+ # in the {Grafana::Variable#raw_value} and have to be separated by +,+ (comma). If no value is specified for
147
+ # a column, no change will happen.
148
+ # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#preformat_response})
149
+ # @param formats [Grafana::Variable] formats, which shall be applied to the columns in the query result
150
+ # @return [Hash] formatted query result
151
+ # TODO: make sure that caught errors are also visible in logger
152
+ def format_columns(result, formats)
153
+ return result unless formats
154
+
155
+ formats.text.split(',').each_index do |i|
156
+ format = formats.text.split(',')[i]
157
+ next if format.empty?
158
+
159
+ result[:content].map do |row|
160
+ next unless row.length > i
161
+
162
+ begin
163
+ row[i] = format % row[i] if row[i]
164
+ rescue StandardError => e
165
+ row[i] = e.message
166
+ end
167
+ end
168
+ end
169
+ result
170
+ end
171
+
172
+ # Used to replace values in a query result according given configurations.
173
+ #
174
+ # The given variables will be applied to an appropriate column, depending
175
+ # on the naming of the variable. The variable name ending specifies the column,
176
+ # e.g. a variable named +replace_values_2+ will be applied to the second column.
177
+ #
178
+ # The {Grafana::Variable#text} needs to contain the replace specification.
179
+ # Multiple replacements can be specified by separating them with +,+. If a
180
+ # literal comma is needed, it can be escaped with a backslash: +\\,+.
181
+ #
182
+ # The rule will be separated from the replacement text with a colon +:+.
183
+ # If a literal colon is wanted, it can be escaped with a backslash: +\\:+.
184
+ #
185
+ # Examples:
186
+ # - Basic string replacement
187
+ # MyTest:ThisValue
188
+ # will replace all occurences of the text 'MyTest' with 'ThisValue'.
189
+ # - Number comparison
190
+ # <=10:OK
191
+ # will replace all values smaller or equal to 10 with 'OK'.
192
+ # - Regular expression
193
+ # ^[^ ]\\+ (\d+)$:\1 is the answer
194
+ # will replace all values matching the pattern, e.g. 'answerToAllQuestions 42' to
195
+ # '42 is the answer'. Important to know: the regular expressions always have to start
196
+ # with +^+ and end with +$+, i.e. the expression itself always has to match
197
+ # the whole content in one field.
198
+ # @param result [Hash] preformatted query result (see {Grafana::AbstractDatasource#preformat_response}.
199
+ # @param configs [Array<Grafana::Variable>] one variable for replacing values in one column
200
+ # @return [Hash] query result with replaced values
201
+ # TODO: make sure that caught errors are also visible in logger
202
+ def replace_values(result, configs)
203
+ return result if configs.empty?
204
+
205
+ configs.each do |key, formats|
206
+ cols = key.split('_')[2..-1].map(&:to_i)
207
+
208
+ formats.text.split(/(?<!\\),/).each_index do |j|
209
+ format = formats.text.split(/(?<!\\),/)[j]
210
+
211
+ arr = format.split(/(?<!\\):/)
212
+ raise MalformedReplaceValuesStatementError, format if arr.length != 2
213
+
214
+ k = arr[0]
215
+ v = arr[1]
216
+ k.gsub!(/\\([:,])/, '\1')
217
+ v.gsub!(/\\([:,])/, '\1')
218
+ result[:content].map do |row|
219
+ (row.length - 1).downto 0 do |i|
220
+ if cols.include?(i + 1) || cols.empty?
221
+
222
+ # handle regular expressions
223
+ if k.start_with?('^') && k.end_with?('$')
224
+ begin
225
+ row[i] = row[i].to_s.gsub(/#{k}/, v) if row[i].to_s =~ /#{k}/
226
+ rescue StandardError => e
227
+ row[i] = e.message
228
+ end
229
+
230
+ # handle value comparisons
231
+ elsif (match = k.match(/^ *(?<operator>[<>]=?|<>|=) *(?<number>[+-]?\d+(?:\.\d+)?)$/))
232
+ skip = false
233
+ begin
234
+ val = Float(row[i])
235
+ rescue StandardError
236
+ # value cannot be converted to number, simply ignore it as the comparison does not fit here
237
+ skip = true
238
+ end
239
+
240
+ unless skip
241
+ begin
242
+ op = match[:operator].gsub(/^=$/, '==').gsub(/^<>$/, '!=')
243
+ if val.public_send(op.to_sym, Float(match[:number]))
244
+ row[i] = if v.include?('\\1')
245
+ v.gsub(/\\1/, row[i].to_s)
246
+ else
247
+ v
248
+ end
249
+ end
250
+ rescue StandardError => e
251
+ row[i] = e.message
252
+ end
253
+ end
254
+
255
+ # handle as normal comparison
256
+ elsif row[i].to_s == k
257
+ row[i] = v
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ result
266
+ end
267
+
268
+ # Used to translate the relative date strings used by grafana, e.g. +now-5d/w+ to the
269
+ # correct timestamp. Reason is that grafana does this in the frontend, which we have
270
+ # to emulate here for the reporter.
271
+ #
272
+ # Additionally providing this function the +report_time+ assures that all queries
273
+ # rendered within one report will use _exactly_ the same timestamp in those relative
274
+ # times, i.e. there shouldn't appear any time differences, no matter how long the
275
+ # report is running.
276
+ # @param orig_date [String] time string provided by grafana, usually +from+ or +to+.
277
+ # @param report_time [Grafana::Variable] report start time
278
+ # @param is_to_time [Boolean] true, if the time should be calculated for +to+, false if it shall be
279
+ # calculated for +from+
280
+ # @param timezone [Grafana::Variable] timezone to use, if not system timezone
281
+ # @return [String] translated date as timestamp string
282
+ def translate_date(orig_date, report_time, is_to_time, timezone = nil)
283
+ report_time ||= Variable.new(Time.now.to_s)
284
+ return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date
285
+ return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
286
+ return orig_date if orig_date =~ /^\d+$/
287
+
288
+ # check if a relative date is mentioned
289
+ date_spec = orig_date.clone
290
+
291
+ date_spec.slice!(/^now/)
292
+ raise TimeRangeUnknownError, orig_date unless date_spec
293
+
294
+ date = DateTime.parse(report_time.raw_value)
295
+ # TODO: allow from_translated or similar in ADOC template
296
+ date = date.new_offset(timezone.raw_value) if timezone
297
+
298
+ until date_spec.empty?
299
+ fit_match = date_spec.match(%r{^/(?<fit>[smhdwMy])})
300
+ if fit_match
301
+ date = fit_date(date, fit_match[:fit], is_to_time)
302
+ date_spec.slice!(%r{^/#{fit_match[:fit]}})
303
+ end
304
+
305
+ delta_match = date_spec.match(/^(?<op>(?:-|\+))(?<count>\d+)?(?<unit>[smhdwMy])/)
306
+ if delta_match
307
+ date = delta_date(date, "#{delta_match[:op]}#{delta_match[:count] || 1}".to_i, delta_match[:unit])
308
+ date_spec.slice!(/^#{delta_match[:op]}#{delta_match[:count]}#{delta_match[:unit]}/)
309
+ end
310
+
311
+ raise TimeRangeUnknownError, orig_date unless fit_match || delta_match
312
+ end
313
+
314
+ # step back one second, if this is the 'to' time
315
+ date = (date.to_time - 1).to_datetime if is_to_time
316
+
317
+ (Time.at(date.to_time.to_i).to_i * 1000).to_s
318
+ end
319
+
320
+ private
321
+
322
+ # @return [Hash<String, Variable>] all grafana variables stored in this query, i.e. the variable name
323
+ # is prefixed with +var-+
324
+ def grafana_variables
325
+ @variables.select { |k, _v| k =~ /^var-.+/ }
326
+ end
327
+
328
+ def delta_date(date, delta_count, time_letter)
329
+ # substract specified time
330
+ case time_letter
331
+ when 's'
332
+ (date.to_time + (delta_count * 1)).to_datetime
333
+ when 'm'
334
+ (date.to_time + (delta_count * 60)).to_datetime
335
+ when 'h'
336
+ (date.to_time + (delta_count * 60 * 60)).to_datetime
337
+ when 'd'
338
+ date.next_day(delta_count)
339
+ when 'w'
340
+ date.next_day(delta_count * 7)
341
+ when 'M'
342
+ date.next_month(delta_count)
343
+ when 'y'
344
+ date.next_year(delta_count)
345
+ end
346
+ end
347
+
348
+ def fit_date(date, fit_letter, is_to_time)
349
+ # fit to specified time frame
350
+ case fit_letter
351
+ when 's'
352
+ date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, date.sec, date.zone)
353
+ date = (date.to_time + 1).to_datetime if is_to_time
354
+ when 'm'
355
+ date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, 0, date.zone)
356
+ date = (date.to_time + 60).to_datetime if is_to_time
357
+ when 'h'
358
+ date = DateTime.new(date.year, date.month, date.day, date.hour, 0, 0, date.zone)
359
+ date = (date.to_time + 60 * 60).to_datetime if is_to_time
360
+ when 'd'
361
+ date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
362
+ date = date.next_day(1) if is_to_time
363
+ when 'w'
364
+ date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
365
+ date = if date.wday.zero?
366
+ date.prev_day(7)
367
+ else
368
+ date.prev_day(date.wday - 1)
369
+ end
370
+ date = date.next_day(7) if is_to_time
371
+ when 'M'
372
+ date = DateTime.new(date.year, date.month, 1, 0, 0, 0, date.zone)
373
+ date = date.next_month if is_to_time
374
+ when 'y'
375
+ date = DateTime.new(date.year, 1, 1, 0, 0, 0, date.zone)
376
+ date = date.next_year if is_to_time
377
+ end
378
+
379
+ date
380
+ end
381
+
382
+ # Merges the given Hash with the stored variables.
383
+ #
384
+ # Can be used to easily set many values at once in the local variables hash.
385
+ #
386
+ # Please note, that the values of the Hash need to be of type {Variable}.
387
+ #
388
+ # @param hash [Hash<String,Variable>] Hash containing variable name as key and {Variable} as value
389
+ # @return [AbstractQuery] this object
390
+ # TODO: test if this method can be removed, or make it private at least
391
+ def merge_variables(hash)
392
+ hash.each do |k, v|
393
+ if @variables[k.to_s].nil?
394
+ @variables[k.to_s] = v
395
+ else
396
+ @variables[k.to_s].raw_value = v.raw_value
397
+ end
398
+ end
399
+ end
400
+ end
401
+ end