fastapi 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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: []