1 | # frozen_string_literal: true |
||
2 | |||
3 | 1 | require 'date' |
|
4 | 1 | require 'formatador' |
|
5 | 1 | require 'parallel' |
|
6 | 1 | require 'pp' |
|
7 | 1 | require 'stringio' |
|
8 | |||
9 | # Reopen to add utility methods |
||
10 | 1 | module Enumerable |
|
11 | # Enumerate all non-empty prefixes of the enumerable |
||
12 | # @return [Enumerator] |
||
13 | 1 | def prefixes |
|
14 | 1207 | Enumerator.new do |enum| |
|
15 | 1207 | prefix = [] |
|
16 | 1207 | each do |elem| |
|
17 | 882 | prefix = prefix.dup << elem |
|
18 | 882 | enum.yield prefix |
|
19 | end |
||
20 | end |
||
21 | end |
||
22 | |||
23 | # Enumerate all partitionings of an enumerable |
||
24 | # @return [Enumerator] |
||
25 | 1 | def partitions(max_length = nil) |
|
26 | 2925 | max_length = length if max_length.nil? |
|
27 | 2925 | Enumerator.new do |enum| |
|
28 | 2925 | 1.upto(max_length).map do |length| |
|
29 | 31732 | enum.yield partition.with_index { |_, i| i < length } |
|
30 | end |
||
31 | end |
||
32 | end |
||
33 | |||
34 | # Take the sum of the result of calling the block on each item |
||
35 | # @return [Object] |
||
36 | 1 | def sum_by(initial = 0) |
|
37 | 35346 | reduce(initial) { |sum, item| sum + yield(item) } |
|
38 | end |
||
39 | |||
40 | # Take the product of the result of calling the block on each item |
||
41 | # @return [Object] |
||
42 | 1 | def product_by(initial = 1) |
|
43 | 16777 | reduce(initial) { |product, item| product * yield(item) } |
|
44 | end |
||
45 | end |
||
46 | |||
47 | # Extend with some convenience methods |
||
48 | 1 | class Array |
|
49 | # Find the longest common prefix of two arrays |
||
50 | # @return [Array<Object>] |
||
51 | 1 | def longest_common_prefix(other) |
|
52 | 289 | fail TypeError unless other.is_a? Array |
|
53 | 289 | (prefixes.to_a & other.prefixes.to_a).max_by(&:length) || [] |
|
54 | end |
||
55 | end |
||
56 | |||
57 | # Reopen to present as finite as with Float |
||
58 | 1 | class Integer |
|
59 | # Convenience methods to allow integers to be considered finite |
||
60 | # @return [Boolean] |
||
61 | 1 | def finite? |
|
62 | true |
||
63 | end |
||
64 | end |
||
65 | |||
66 | # Extend Object to print coloured output |
||
67 | 1 | class Object |
|
68 | 1 | def inspect |
|
69 | 734 | Formatador.parse(respond_to?(:to_color) ? to_color : to_s) |
|
70 | end |
||
71 | |||
72 | # Get a colored representation of the object |
||
73 | # @return [String] |
||
74 | 1 | def to_color |
|
75 | 611 | to_s |
|
76 | end |
||
77 | end |
||
78 | |||
79 | # Allow a supertype to look up a class given the |
||
80 | # name of a subtype inheriting from this class |
||
81 | 1 | module Supertype |
|
82 | # Add class methods when this module is included |
||
83 | # @return [void] |
||
84 | 1 | def self.included(base) |
|
85 | 7 | base.extend ClassMethods |
|
86 | end |
||
87 | |||
88 | # Add a single method to get a class given the subtype name |
||
89 | 1 | module ClassMethods |
|
90 | # Get the class given the name of a subtype |
||
91 | # @return [Class] the concrete class with the given subtype name |
||
92 | 1 | def subtype_class(name) |
|
93 | 13 | class_name = self.name.split('::')[0..-2] |
|
94 | 13 | class_name << name.split('_').map do |name_part| |
|
95 | 25 | name_part = name_part[0].upcase + name_part[1..-1] |
|
96 | 25 | name_part.sub 'Id', 'ID' |
|
97 | end.join |
||
98 | 13 | class_name[-1] = class_name[-1] + self.name.split('::').last |
|
99 | |||
100 | 13 | class_name.reduce(Object) do |mod, name_part| |
|
101 | 46 | mod.const_get(name_part) |
|
102 | end |
||
103 | end |
||
104 | end |
||
105 | end |
||
106 | |||
107 | # Allow subclasses to return a string representing of the |
||
108 | # class, minus a common suffix also used in the superclass |
||
109 | 1 | module Subtype |
|
110 | # Add instance and class methods when this module is included |
||
111 | # @return [void] |
||
112 | 1 | def self.included(base) |
|
113 | 56 | base.send :include, InstanceMethods |
|
114 | 56 | base.extend ClassMethods |
|
115 | end |
||
116 | |||
117 | # Mirror the subtype method on class instances |
||
118 | 1 | module InstanceMethods |
|
119 | # A mirror of {Subtype::ClassMethods#subtype_name} |
||
120 | # @return [String] |
||
121 | 1 | def subtype_name(**args) |
|
122 | 377 | self.class.subtype_name(**args) |
|
123 | end |
||
124 | end |
||
125 | |||
126 | # Add a single method to retrieve the subtype name |
||
127 | 1 | module ClassMethods |
|
128 | # Get a unique string identify this subclass amongst sibling classes |
||
129 | # @return [String] |
||
130 | 1 | def subtype_name(name_case: :snake) |
|
131 | 384 | super_name = name_array superclass |
|
132 | 384 | self_name = name_array self |
|
133 | 384 | self_name = self_name.reverse.drop_while do |part| |
|
134 | 1142 | super_name.include? part |
|
135 | end.reverse |
||
136 | |||
137 | 384 | if name_case == :snake |
|
138 | 383 | name = self_name.join('_').freeze |
|
139 | 1 | elsif name_case == :camel |
|
140 | 1 | name = self_name.map do |part| |
|
141 | 2 | part[0].upcase + part[1..-1] |
|
142 | end.join('').freeze |
||
143 | end |
||
144 | |||
145 | 384 | name |
|
146 | end |
||
147 | |||
148 | 1 | private |
|
149 | |||
150 | # Convert camel case class names to an array |
||
151 | # @return [Array<String>] |
||
152 | 1 | def name_array(cls) |
|
153 | 768 | frozen_name = cls.name.dup |
|
154 | 768 | frozen_name.gsub!(/^.*::/, '') |
|
155 | 768 | frozen_name.gsub!('ID', 'Id') |
|
156 | 768 | frozen_name.freeze |
|
157 | |||
158 | 768 | frozen_name.split(/(?=[A-Z]+)/).map(&:freeze) \ |
|
159 | .map! do |s| |
||
160 | 2262 | s.downcase.freeze |
|
161 | end |
||
162 | end |
||
163 | end |
||
164 | end |
||
165 | |||
166 | # Simple helper class to facilitate cardinality estimates |
||
167 | 1 | class Cardinality |
|
168 | # Update the cardinality based on filtering implicit to the index |
||
169 | # @return [Integer] |
||
170 | 1 | def self.filter(cardinality, eq_filter, range_filter) |
|
171 | 344 | filtered = (range_filter.nil? ? 1.0 : 0.1) * cardinality |
|
172 | 344 | filtered *= eq_filter.map do |field| |
|
173 | 20 | 1.0 / field.cardinality |
|
174 | end.inject(1.0, &:*) |
||
175 | |||
176 | 344 | filtered |
|
177 | end |
||
178 | end |
||
179 | |||
180 | # Add a simple function for pretty printing strings |
||
181 | 1 | module Kernel |
|
182 | 1 | private |
|
183 | |||
184 | # Pretty print to a string |
||
185 | # @return [String] |
||
186 | 1 | def pp_s(*objs) |
|
187 | s = StringIO.new |
||
188 | objs.each { |obj| PP.pp(obj, s) } |
||
189 | s.rewind |
||
190 | s.read |
||
191 | end |
||
192 | |||
193 | 1 | module_function :pp_s |
|
194 | end |
||
195 | |||
196 | # Add simple convenience methods |
||
197 | 1 | class Object |
|
198 | # Convert all the keys of a hash to symbols |
||
199 | # @return [Object] |
||
200 | 1 | def deep_symbolize_keys |
|
201 | return each_with_object({}) do |(k, v), memo| |
||
202 | memo[k.to_sym] = v.deep_symbolize_keys |
||
203 | memo |
||
204 | end if is_a? Hash |
||
205 | |||
206 | return each_with_object([]) do |v, memo| |
||
207 | memo << v.deep_symbolize_keys |
||
208 | memo |
||
209 | end if is_a? Array |
||
210 | |||
211 | self |
||
212 | end |
||
213 | end |
||
214 | |||
215 | # Extend the kernel to allow warning suppression |
||
216 | 1 | module Kernel |
|
217 | # Allow the suppression of warnings for a block of code |
||
218 | # @return [void] |
||
219 | 1 | def suppress_warnings |
|
220 | original_verbosity = $VERBOSE |
||
221 | $VERBOSE = nil |
||
222 | result = yield |
||
223 | $VERBOSE = original_verbosity |
||
224 | |||
225 | result |
||
226 | end |
||
227 | end |
||
228 | |||
229 | 1 | module NoSE |
|
230 | # Helper functions for building DSLs |
||
231 | 1 | module DSL |
|
232 | # Add methods to the class which can be used to access entities and fields |
||
233 | # @return [void] |
||
234 | 1 | def mixin_fields(entities, cls) |
|
235 | 20 | entities.each do |entity_name, entity| |
|
236 | # Add fake entity object for the DSL |
||
237 | 68 | fake = Object.new |
|
238 | |||
239 | # Add a method named by the entity to allow field creation |
||
240 | 68 | cls.send :define_method, entity_name.to_sym, (proc do |
|
241 | 640 | metaclass = class << fake; self; end |
|
242 | |||
243 | # Allow fields to be defined using [] access |
||
244 | 320 | metaclass.send :define_method, :[] do |field_name| |
|
245 | 37 | if field_name == '*' |
|
246 | 37 | entity.fields.values |
|
247 | else |
||
248 | entity.fields[field_name] || entity.foreign_keys[field_name] |
||
249 | end |
||
250 | end |
||
251 | |||
252 | # Define methods named for fields so things like 'user.id' work |
||
253 | 320 | entity.fields.merge(entity.foreign_keys).each do |field_name, field| |
|
254 | 1629 | metaclass.send :define_method, field_name.to_sym, -> { field } |
|
255 | end |
||
256 | |||
257 | 320 | fake |
|
258 | end) |
||
259 | end |
||
260 | end |
||
261 | |||
262 | 1 | module_function :mixin_fields |
|
263 | end |
||
264 | |||
265 | # Add loading of class instances from the filesystem |
||
266 | 1 | module Loader |
|
267 | 1 | attr_reader :source_code |
|
268 | |||
269 | 1 | def self.included(base) |
|
270 | 3 | base.extend ClassMethods |
|
271 | end |
||
272 | |||
273 | # Add a class method to load class instances from file |
||
274 | 1 | module ClassMethods |
|
275 | # Load a class with the given name from a directory specified |
||
276 | # by the LOAD_PATH class constant |
||
277 | # @return [Object] an instance of the class which included this module |
||
278 | 1 | def load(name) |
|
279 | 18 | if File.exist? name |
|
280 | filename = name |
||
281 | else |
||
282 | 18 | path = const_get(:LOAD_PATH) |
|
283 | 18 | filename = File.expand_path "../../../#{path}/#{name}.rb", __FILE__ |
|
284 | end |
||
285 | |||
286 | 18 | source_code = File.read(filename) |
|
287 | 18 | instance = binding.eval source_code, filename |
|
288 | 18 | instance.instance_variable_set :@source_code, source_code |
|
289 | 18 | instance |
|
290 | end |
||
291 | end |
||
292 | end |
||
293 | end |
||
294 | |||
295 | # Allow tracking of subclasses for plugin purposes |
||
296 | 1 | module Listing |
|
297 | 1 | def self.included(base) |
|
298 | 3 | base.extend ClassMethods |
|
299 | 3 | base.class_variable_set :@@registry, {} |
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Loading history...
|
|||
300 | end |
||
301 | |||
302 | # Add a class method to track new subclasses |
||
303 | 1 | module ClassMethods |
|
304 | # Track this new subclass for later |
||
305 | # @return [void] |
||
306 | 1 | def inherited(subclass) |
|
307 | 9 | class_variable_get(:@@registry)[subclass.name] = subclass |
|
308 | end |
||
309 | |||
310 | # List all of the encountered subclasses |
||
311 | # @return [Hash<String, Class>] |
||
312 | 1 | def subclasses |
|
313 | 4 | class_variable_get(:@@registry) |
|
314 | end |
||
315 | end |
||
316 | end |
||
317 | |||
318 | # Extend Time to allow conversion to DateTime instances |
||
319 | 1 | class Time |
|
320 | # Convert to a DateTime instance |
||
321 | # http://stackoverflow.com/a/279785/123695 |
||
322 | # @return [DateTime] |
||
323 | 1 | def to_datetime |
|
324 | # Convert seconds + microseconds into a fractional number of seconds |
||
325 | seconds = sec + Rational(usec, 10**6) |
||
326 | |||
327 | # Convert a UTC offset measured in minutes to one measured in a |
||
328 | # fraction of a day. |
||
329 | offset = Rational(utc_offset, 60 * 60 * 24) |
||
330 | DateTime.new(year, month, day, hour, min, seconds, offset) |
||
331 | end |
||
332 | end |
||
333 |