fastapi 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 68312e62ae3797dbc5cd672cc5cbc10766390ca3
4
+ data.tar.gz: 021eab09bc923f07bcf54da04d012d7102c608a7
5
+ SHA512:
6
+ metadata.gz: 0901d7eed24c1d75e5fa1be3c339a065c4e7b6c5a3d0bc8b1a51a508055e7b7e99ceef0a1508db682a4702c33d5cf2db06ee5617261a016c8e2fd1fc5ebff521
7
+ data.tar.gz: 4c62611aa52b91a266e800bc3ffdeb803c28dca90dea3210f9b70b4db905a845216fba7f4094d85316f6c2e68197b7cb2cb05c23403c89e9c71459288fc686b1
data/lib/fastapi.rb ADDED
@@ -0,0 +1,698 @@
1
+ require 'oj'
2
+ require 'fastapi/active_record_extension.rb'
3
+
4
+ class FastAPI
5
+
6
+ @@result_types = {
7
+ single: 0,
8
+ multiple: 1,
9
+ }
10
+
11
+ @@api_comparator_list = [
12
+ 'is',
13
+ 'not',
14
+ 'gt',
15
+ 'gte',
16
+ 'lt',
17
+ 'lte',
18
+ 'in',
19
+ 'not_in',
20
+ 'contains',
21
+ 'icontains',
22
+ 'is_null',
23
+ 'not_null',
24
+ ]
25
+
26
+ def initialize(model)
27
+ @model = model
28
+ @data = nil
29
+ @metadata = nil
30
+ @has_results = false
31
+ @result_type = 0
32
+ end
33
+
34
+ def inspect
35
+ "<#{self.class}: #{@model}>"
36
+ end
37
+
38
+ def filter(filters = {}, meta = {})
39
+
40
+ result = fastapi_query(filters)
41
+
42
+ metadata = {}
43
+
44
+ meta.each do |key, value|
45
+ metadata[key] = value
46
+ end
47
+
48
+ metadata[:total] = result[:total]
49
+ metadata[:offset] = result[:offset]
50
+ metadata[:count] = result[:count]
51
+ metadata[:error] = result[:error]
52
+
53
+ @metadata = metadata
54
+ @data = result[:data]
55
+
56
+ @result_type = @@result_types[:multiple]
57
+
58
+ self
59
+
60
+ end
61
+
62
+ def fetch(id, meta = {})
63
+
64
+ result = fastapi_query({id: id})
65
+
66
+ metadata = {}
67
+
68
+ meta.each do |key, value|
69
+ metadata[key] = value
70
+ end
71
+
72
+ if result[:total] == 0
73
+ error = @model.to_s + ' id does not exist'
74
+ else
75
+ error = result[:error]
76
+ end
77
+
78
+ metadata[:total] = result[:total]
79
+ metadata[:offset] = result[:offset]
80
+ metadata[:count] = result[:count]
81
+ metadata[:error] = error
82
+
83
+ @metadata = metadata
84
+ @data = result[:data]
85
+
86
+ @result_type = @@result_types[:multiple]
87
+
88
+ self
89
+
90
+ end
91
+
92
+ def data_json
93
+ Oj.dump(@data)
94
+ end
95
+
96
+ def data
97
+ @data
98
+ end
99
+
100
+ def meta_json
101
+ Oj.dump(meta)
102
+ end
103
+
104
+ def meta
105
+ @metadata
106
+ end
107
+
108
+ def to_hash
109
+ {
110
+ meta: @metadata,
111
+ data: @data
112
+ }
113
+ end
114
+
115
+ def response
116
+ Oj.dump(self.to_hash)
117
+ end
118
+
119
+ def reject(message = 'Access denied')
120
+ Oj.dump({
121
+ meta: {
122
+ total: 0,
123
+ offset: 0,
124
+ count: 0,
125
+ error: message.to_s
126
+ },
127
+ data: [],
128
+ })
129
+ end
130
+
131
+ private
132
+
133
+ def fastapi_query(filters = {})
134
+
135
+ offset = 0
136
+ count = 500
137
+ order = nil
138
+
139
+ if filters.has_key? :__offset
140
+ offset = filters[:__offset].to_i
141
+ filters.delete(:__offset)
142
+ end
143
+
144
+ if filters.has_key? :__count
145
+ count = [1, [500, filters[:__count].to_i].min].max
146
+ filters.delete(:__count)
147
+ end
148
+
149
+ prepared_data = api_generate_sql(filters, offset, count)
150
+
151
+ model_lookup = {}
152
+ prepared_data[:models].each do |key, model|
153
+ columns_hash = model.columns_hash
154
+ model_lookup[key] = {
155
+ model: model,
156
+ fields: model.fastapi_fields_sub,
157
+ types: model.fastapi_fields_sub.map { |field| (columns_hash.has_key? field.to_s) ? columns_hash[field.to_s].type : nil },
158
+ }
159
+ end
160
+ # model_lookup = model_lookup.map { |model| }
161
+ error = nil
162
+
163
+ begin
164
+ count_result = ActiveRecord::Base.connection.execute(prepared_data[:count_query])
165
+ result = ActiveRecord::Base.connection.execute(prepared_data[:query])
166
+ rescue
167
+ return {
168
+ data: [],
169
+ total: 0,
170
+ count: 0,
171
+ offset: offset,
172
+ error: 'Query failed'
173
+ }
174
+ end
175
+
176
+ total_size = count_result.values().size > 0 ? count_result.values()[0][0].to_i : 0
177
+
178
+ start = Time.now()
179
+
180
+ fields = result.fields()
181
+ rows = result.values()
182
+
183
+ dataset = Array.new(rows.size)
184
+
185
+ rows.each_with_index do |row, index|
186
+ currow = {}
187
+ row.each_with_index do |val, key_index|
188
+
189
+ field = fields[key_index]
190
+ split_index = field.index('__')
191
+
192
+ if field[0..7] == '__many__'
193
+
194
+ field = field[8..-1]
195
+ field_sym = field.to_sym
196
+ model = model_lookup[field_sym]
197
+
198
+ currow[field_sym] = parse_many(
199
+ val,
200
+ model_lookup[field_sym][:fields],
201
+ model_lookup[field_sym][:types]
202
+ )
203
+
204
+ elsif split_index
205
+
206
+ obj_name = field[0..split_index - 1].to_sym
207
+ field = field[split_index + 2..-1]
208
+ model = model_lookup[obj_name][:model]
209
+
210
+ if !(currow.has_key? obj_name)
211
+ currow[obj_name] = {}
212
+ end
213
+
214
+ currow[obj_name][field.to_sym] = api_convert_type(val, model.columns_hash[field].type)
215
+
216
+ elsif @model.columns_hash[field]
217
+
218
+ currow[field.to_sym] = api_convert_type(val, @model.columns_hash[field].type)
219
+
220
+ end
221
+
222
+ end
223
+
224
+ dataset[index] = currow
225
+
226
+ end
227
+
228
+ my_end = Time.now()
229
+
230
+ # logger.info dataset.size.to_s + '-length array parsed in ' + (my_end - start).to_s
231
+
232
+ {
233
+ data: dataset,
234
+ total: total_size,
235
+ count: dataset.size,
236
+ offset: offset,
237
+ error: nil
238
+ }
239
+
240
+ end
241
+
242
+ def parse_many(str, fields = [], types = [])
243
+
244
+ rows = []
245
+ cur_row = {}
246
+ entry_index = 0
247
+
248
+ i = 0
249
+ len = str.length
250
+
251
+ i = str.index('(')
252
+
253
+ if not i
254
+ return rows
255
+ end
256
+
257
+ i = i + 1
258
+
259
+ while i < len
260
+
261
+ if str[i] == ')'
262
+
263
+ rows << cur_row
264
+ cur_row = {}
265
+ entry_index = 0
266
+ i = i + 3
267
+
268
+ elsif str[i] == '"'
269
+
270
+ i = i + 1
271
+ nextIndex = str.index('"', i)
272
+
273
+ while str[nextIndex - 1] == '\\'
274
+ nextIndex = str.index('"', nextIndex + 1)
275
+ end
276
+
277
+ cur_row[fields[entry_index]] = api_convert_type(str[i...nextIndex], types[entry_index])
278
+ entry_index = entry_index + 1
279
+
280
+ i = nextIndex + 1
281
+ else
282
+
283
+ if str[i] == ','
284
+ i = i + 1
285
+ end
286
+ parensIndex = str.index(')', i)
287
+ nextIndex = str.index(',', i)
288
+
289
+ if nextIndex.nil? or nextIndex > parensIndex
290
+ nextIndex = parensIndex
291
+ end
292
+
293
+ if i == nextIndex
294
+ cur_row[fields[entry_index]] = nil
295
+ else
296
+ cur_row[fields[entry_index]] = api_convert_type(str[i...nextIndex], types[entry_index])
297
+ end
298
+
299
+ entry_index = entry_index + 1
300
+
301
+ if nextIndex == parensIndex
302
+ rows << cur_row
303
+ cur_row = {}
304
+ entry_index = 0
305
+ i = nextIndex + 3
306
+ else
307
+ i = nextIndex + 1
308
+ end
309
+
310
+ end
311
+
312
+ end
313
+
314
+ rows
315
+
316
+ end
317
+
318
+ def api_comparison(comparator, value)
319
+
320
+ if comparator == 'is'
321
+
322
+ ' = ' + ActiveRecord::Base.connection.quote(value.to_s)
323
+
324
+ elsif comparator == 'not'
325
+
326
+ ' <> ' + ActiveRecord::Base.connection.quote(value.to_s)
327
+
328
+ elsif comparator == 'gt'
329
+
330
+ ' > ' + ActiveRecord::Base.connection.quote(value.to_s)
331
+
332
+ elsif comparator == 'gte'
333
+
334
+ ' >= ' + ActiveRecord::Base.connection.quote(value.to_s)
335
+
336
+ elsif comparator == 'lt'
337
+
338
+ ' < ' + ActiveRecord::Base.connection.quote(value.to_s)
339
+
340
+ elsif comparator == 'lte'
341
+
342
+ ' <= ' + ActiveRecord::Base.connection.quote(value.to_s)
343
+
344
+ elsif comparator == 'in' or comparator == 'not_in'
345
+
346
+ if not value.is_a? Array
347
+
348
+ if value.is_a? Range
349
+ value = value.to_a
350
+ else
351
+ value = [value.to_s]
352
+ end
353
+
354
+ end
355
+
356
+ if comparator == 'in'
357
+ ' IN(' + (value.map { |val| ActiveRecord::Base.connection.quote(val.to_s) }).join(',') + ')'
358
+ else
359
+ ' NOT IN(' + (value.map { |value| ActiveRecord::Base.connection.quote(val.to_s) }).join(',') + ')'
360
+ end
361
+
362
+ elsif comparator == 'contains'
363
+
364
+ ' LIKE \'%\' || ' + ActiveRecord::Base.connection.quote(value.to_s) + ' || \'%\''
365
+
366
+ elsif comparator == 'icontains'
367
+
368
+ ' ILIKE \'%\' || ' + ActiveRecord::Base.connection.quote(value.to_s) + ' || \'%\''
369
+
370
+ elsif comparator == 'is_null'
371
+
372
+ ' IS NULL'
373
+
374
+ elsif comparator == 'not_null'
375
+
376
+ ' IS NOT NULL'
377
+
378
+ end
379
+
380
+ end
381
+
382
+ def api_convert_type(val, type)
383
+
384
+ if not val.nil?
385
+ if type == :integer
386
+ val = val.to_i
387
+ elsif type == :float
388
+ val = val.to_f
389
+ elsif type == :boolean
390
+ val = {
391
+ 't' => true,
392
+ 'f' => false,
393
+ }[val]
394
+ end
395
+ end
396
+
397
+ val
398
+
399
+ end
400
+
401
+ def parse_filters(filters, model = nil)
402
+
403
+ if not filters.has_key? :__order
404
+ filters[:__order] = [:created_at, 'DESC']
405
+ end
406
+
407
+ self_obj = model.nil? ? @model : model
408
+ self_string_table = model.nil? ? @model.to_s.tableize : '__' + model.to_s.tableize
409
+
410
+ filter_array = []
411
+ filter_has_many = {}
412
+
413
+ order = nil
414
+ order_has_many = {}
415
+
416
+ if filters.size > 0
417
+
418
+ filters.each do |key, value|
419
+
420
+ if key == :__order
421
+
422
+ order = value
423
+
424
+ if order.is_a? String
425
+ order = order.split(',')
426
+ if order.size < 2
427
+ order << 'ASC'
428
+ end
429
+ elsif order.is_a? Array
430
+ while order.size < 2
431
+ order << ''
432
+ end
433
+ else
434
+ order = ['', '']
435
+ end
436
+
437
+ if not self_obj.column_names.include? order[0].to_s
438
+ order = nil
439
+ else
440
+ order[0] = self_string_table + '.' + order[0].to_s
441
+ if not ['ASC', 'DESC'].include? order[1]
442
+ order[1] = 'ASC'
443
+ end
444
+ order = order.join(' ')
445
+ end
446
+
447
+ else
448
+
449
+ field = key.to_s
450
+
451
+ if field.index('__').nil?
452
+ comparator = 'is'
453
+ else
454
+
455
+ comparator = field[(field.index('__') + 2)..-1]
456
+ field = field[0...field.index('__')]
457
+
458
+ if not @@api_comparator_list.include? comparator
459
+ next # skip dis bro
460
+ end
461
+
462
+ end
463
+
464
+ if model.nil? and self_obj.reflect_on_all_associations(:has_many).map(&:name).include? key
465
+
466
+ filter_result = parse_filters(value, field.singularize.classify.constantize)
467
+ # logger.info filter_result
468
+ filter_has_many[key] = filter_result[:main]
469
+ order_has_many[key] = filter_result[:main_order]
470
+
471
+ elsif self_obj.column_names.include? field
472
+
473
+ if self_obj.columns_hash[field].type == :boolean
474
+
475
+ if !!value != value
476
+ value = {
477
+ 't' => true,
478
+ 'f' => false
479
+ }[value]
480
+ end
481
+
482
+ if !!value == value
483
+
484
+ if comparator == 'is'
485
+ filter_array << self_string_table + '.' + field + ' IS ' + value.to_s.upcase
486
+ elsif comparator == 'not'
487
+ filter_array << self_string_table + '.' + field + ' IS NOT ' + value.to_s.upcase
488
+ end
489
+
490
+ end
491
+
492
+ elsif value == nil and comparator != 'is_null' and comparator != 'not_null'
493
+
494
+ if comparator == 'is'
495
+ filter_array << self_string_table + '.' + field + ' IS NULL'
496
+ elsif comparator == 'not'
497
+ filter_array << self_string_table + '.' + field + ' IS NOT NULL'
498
+ end
499
+
500
+ elsif value.is_a? Range and comparator == 'is'
501
+
502
+ filter_array << self_string_table + '.' + field + ' >= ' + ActiveRecord::Base.connection.quote(value.first.to_s)
503
+ filter_array << self_string_table + '.' + field + ' <= ' + ActiveRecord::Base.connection.quote(value.last.to_s)
504
+
505
+ else
506
+
507
+ filter_array << self_string_table + '.' + field + api_comparison(comparator, value)
508
+
509
+ end
510
+
511
+ end
512
+
513
+ end
514
+
515
+ end
516
+
517
+ end
518
+
519
+ {
520
+ main: filter_array,
521
+ main_order: order,
522
+ has_many: filter_has_many,
523
+ has_many_order: order_has_many
524
+ }
525
+
526
+ end
527
+
528
+ def api_generate_sql(filters, offset, count)
529
+
530
+ api_filters = {}
531
+
532
+ @model.fastapi_filters.each do |key, value|
533
+ if value.is_a? Hash
534
+ copy = {}
535
+ value.each do |key, value|
536
+ copy[key] = value
537
+ end
538
+ value = copy
539
+ end
540
+ api_filters[key] = value
541
+ end
542
+
543
+ filters.each do |field, value|
544
+ api_filters[field.to_sym] = value
545
+ end
546
+
547
+ filters = parse_filters(api_filters)
548
+
549
+ fields = []
550
+ belongs = []
551
+ has_many = []
552
+
553
+ model_lookup = {}
554
+
555
+ @model.fastapi_fields.each do |field|
556
+ if @model.reflect_on_all_associations(:belongs_to).map(&:name).include? field
557
+ model = field.to_s.classify.constantize
558
+ model_lookup[field] = model
559
+ belongs << model
560
+ elsif @model.reflect_on_all_associations(:has_many).map(&:name).include? field
561
+ model = field.to_s.singularize.classify.constantize
562
+ model_lookup[field] = model
563
+ has_many << model
564
+ elsif @model.column_names.include? field.to_s
565
+ fields << field
566
+ end
567
+ end
568
+
569
+ self_string = @model.to_s.downcase
570
+ self_string_table = @model.to_s.tableize
571
+
572
+ field_list = []
573
+ joins = []
574
+
575
+ # Base fields
576
+ fields.each do |field|
577
+
578
+ field_string = field.to_s
579
+ field_list << [
580
+ self_string_table,
581
+ '.',
582
+ field_string,
583
+ ' as ',
584
+ field_string
585
+ ].join('')
586
+
587
+ end
588
+
589
+ # Belongs fields (1 to 1)
590
+ belongs.each do |model|
591
+
592
+ model_string_field = model.to_s.tableize.singularize
593
+ model_string_table = model.to_s.tableize
594
+
595
+ # fields
596
+ model.fastapi_fields_sub.each do |field|
597
+ field_string = field.to_s
598
+ field_list << [
599
+ model_string_table,
600
+ '.',
601
+ field_string,
602
+ ' as ',
603
+ model_string_field,
604
+ '__',
605
+ field_string
606
+ ].join('')
607
+ end
608
+
609
+ # joins
610
+ joins << [
611
+ 'LEFT JOIN',
612
+ model_string_table,
613
+ 'ON',
614
+ model_string_table + '.id',
615
+ '=',
616
+ self_string_table + '.' + model_string_field + '_id'
617
+ ].join(' ')
618
+
619
+ end
620
+
621
+ # Many fields (Many to 1)
622
+ has_many.each do |model|
623
+
624
+ model_string = model.to_s.downcase
625
+ model_string_table = model.to_s.tableize
626
+ model_symbol = model_string_table.to_sym
627
+
628
+ model_fields = []
629
+
630
+ model.fastapi_fields_sub.each do |field|
631
+ field_string = field.to_s
632
+ model_fields << [
633
+ '__' + model_string_table + '.' + field_string,
634
+ # 'as',
635
+ # field_string
636
+ ].join(' ')
637
+ end
638
+
639
+ has_many_filters = ''
640
+ has_many_order = ''
641
+ if filters[:has_many].has_key? model_symbol
642
+ has_many_filters = 'AND ' + filters[:has_many][model_symbol].join(' AND ')
643
+ if not filters[:has_many_order][model_symbol].nil?
644
+ has_many_order = 'ORDER BY ' + filters[:has_many_order][model_symbol]
645
+ end
646
+ end
647
+
648
+ field_list << [
649
+ 'ARRAY_TO_STRING(ARRAY(',
650
+ 'SELECT',
651
+ 'ROW(',
652
+ model_fields.join(', '),
653
+ ')',
654
+ 'FROM',
655
+ model_string_table,
656
+ 'as',
657
+ '__' + model_string_table,
658
+ 'WHERE',
659
+ '__' + model_string_table + '.' + self_string + '_id',
660
+ '=',
661
+ self_string_table + '.id',
662
+ has_many_filters,
663
+ has_many_order,
664
+ '), \',\')',
665
+ 'as',
666
+ '__many__' + model_string_table
667
+ ].join(' ')
668
+
669
+ end
670
+
671
+ filter_string = (filters[:main].size > 0 ? ('WHERE ' + filters[:main].join(' AND ')) : '')
672
+ order_string = (filters[:main_order].nil? ? '' : 'ORDER BY ' + filters[:main_order])
673
+
674
+ {
675
+ query: [
676
+ 'SELECT',
677
+ field_list.join(', '),
678
+ 'FROM',
679
+ self_string_table,
680
+ joins.join(' '),
681
+ filter_string,
682
+ order_string,
683
+ 'LIMIT',
684
+ count.to_s,
685
+ 'OFFSET',
686
+ offset.to_s,
687
+ ].join(' '),
688
+ count_query: [
689
+ 'SELECT COUNT(id) FROM',
690
+ self_string_table,
691
+ filter_string
692
+ ].join(' '),
693
+ models: model_lookup
694
+ }
695
+
696
+ end
697
+
698
+ end
@@ -0,0 +1,41 @@
1
+ require 'active_record'
2
+
3
+ module FastAPIExtension
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+
9
+ def fastapi_standard_interface(fields)
10
+ @fastapi_fields = fields
11
+ end
12
+
13
+ def fastapi_standard_interface_sub(fields)
14
+ @fastapi_fields_sub = fields
15
+ end
16
+
17
+ def fastapi_default_filters(filters)
18
+ @fastapi_filters = filters
19
+ end
20
+
21
+ def fastapi_fields
22
+ @fastapi_fields or [:id]
23
+ end
24
+
25
+ def fastapi_fields_sub
26
+ @fastapi_fields_sub or [:id]
27
+ end
28
+
29
+ def fastapi_filters
30
+ @fastapi_filters or {}
31
+ end
32
+
33
+ def fastapi
34
+ FastAPI.new(self)
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+
41
+ ActiveRecord::Base.send(:include, FastAPIExtension)
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastapi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Keith Horwood
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: oj
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.9.9
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.9.9
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 3.2.0
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '3.2'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.2.0
47
+ description: Easily create robust, standardized API endpoints using lightning-fast
48
+ database queries
49
+ email: keithwhor@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/fastapi.rb
55
+ - lib/fastapi/active_record_extension.rb
56
+ homepage: https://github.com/thestorefront/FastAPI
57
+ licenses:
58
+ - MIT
59
+ metadata: {}
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 2.2.2
77
+ signing_key:
78
+ specification_version: 4
79
+ summary: Easily create robust, standardized API endpoints using lightning-fast database
80
+ queries
81
+ test_files: []