marty 1.0.27 → 1.0.28
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 +4 -4
- data/app/components/marty/api_config_view.rb +72 -0
- data/app/components/marty/api_log_view.rb +73 -0
- data/app/components/marty/main_auth_app.rb +35 -3
- data/app/controllers/marty/rpc_controller.rb +84 -35
- data/app/models/marty/api_auth.rb +2 -1
- data/app/models/marty/api_config.rb +16 -0
- data/app/models/marty/api_log.rb +5 -0
- data/app/models/marty/data_grid.rb +1 -1
- data/db/migrate/300_create_marty_api_configs.rb +13 -0
- data/db/migrate/301_create_marty_api_log.rb +16 -0
- data/lib/marty.rb +1 -0
- data/lib/marty/json_schema.rb +52 -0
- data/lib/marty/monkey.rb +15 -0
- data/lib/marty/version.rb +1 -1
- data/marty.gemspec +1 -0
- data/spec/controllers/rpc_controller_spec.rb +342 -1
- data/spec/lib/json_schema_spec.rb +619 -0
- data/spec/models/api_auth_spec.rb +14 -14
- metadata +24 -2
@@ -0,0 +1,16 @@
|
|
1
|
+
class CreateMartyApiLog < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :marty_api_logs do |t|
|
4
|
+
t.string :script, null: false
|
5
|
+
t.string :node, null: false
|
6
|
+
t.string :attrs, null: false
|
7
|
+
t.json :input, null: true
|
8
|
+
t.json :output, null: true
|
9
|
+
t.datetime :start_time, null: false
|
10
|
+
t.datetime :end_time, null: false
|
11
|
+
t.string :error, null: true
|
12
|
+
t.string :remote_ip, null: false
|
13
|
+
t.string :auth_name, null: true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/marty.rb
CHANGED
@@ -14,6 +14,7 @@ require 'marty/mcfly_query'
|
|
14
14
|
require 'marty/monkey'
|
15
15
|
require 'marty/promise_job'
|
16
16
|
require 'marty/lazy_column_loader'
|
17
|
+
require 'marty/json_schema'
|
17
18
|
|
18
19
|
# This does not get loaded in via bundler unless it is included in the
|
19
20
|
# application's Gemfile. Requiring it here removes the need to add it
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'json-schema'
|
2
|
+
|
3
|
+
module Marty
|
4
|
+
|
5
|
+
private
|
6
|
+
class PgEnumAttribute < JSON::Schema::Attribute
|
7
|
+
def self.validate(curr_schema, data, frag, pro, validator, opt={})
|
8
|
+
enum = nil
|
9
|
+
begin
|
10
|
+
enum = curr_schema.schema["pg_enum"].constantize
|
11
|
+
rescue
|
12
|
+
msg = "#{self.class.name} error: '#{data}' is not a pg_enum class"
|
13
|
+
validation_error(pro, msg, frag, curr_schema, self, opt[:record_errors])
|
14
|
+
end
|
15
|
+
if !enum::VALUES.include?(data)
|
16
|
+
msg = "#{self.class.name} error: '#{data}' not contained in #{enum}"
|
17
|
+
validation_error(pro, msg, frag, curr_schema, self, opt[:record_errors])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class DateTimeFormatAttribute < JSON::Schema::Attribute
|
23
|
+
def self.validate(curr_schema, data, frag, processor, validator, options={})
|
24
|
+
begin
|
25
|
+
DateTime.parse(data).in_time_zone(Rails.configuration.time_zone)
|
26
|
+
rescue
|
27
|
+
msg = "#{self.class.name} error: Can't parse '#{data}' into a DateTime"
|
28
|
+
validation_error( processor,
|
29
|
+
msg,
|
30
|
+
frag,
|
31
|
+
curr_schema,
|
32
|
+
self,
|
33
|
+
options[:record_errors])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class JsonSchema < JSON::Schema::Draft4
|
39
|
+
RAW_URI = "http://json-schema.org/marty-draft/schema#"
|
40
|
+
|
41
|
+
def initialize
|
42
|
+
super
|
43
|
+
@attributes["pg_enum"] = PgEnumAttribute
|
44
|
+
@attributes["datetime_format"] = DateTimeFormatAttribute
|
45
|
+
@uri = JSON::Util::URI.parse(RAW_URI)
|
46
|
+
@names = ["marty-draft", RAW_URI]
|
47
|
+
end
|
48
|
+
|
49
|
+
JSON::Validator.register_validator(self.new)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
data/lib/marty/monkey.rb
CHANGED
@@ -209,6 +209,21 @@ class StringEnum < String
|
|
209
209
|
end
|
210
210
|
delorean_instance_method :name
|
211
211
|
delorean_instance_method :id
|
212
|
+
|
213
|
+
def to_yaml(opts = {})
|
214
|
+
YAML::quick_emit(nil, opts) do |out|
|
215
|
+
out.scalar('stringEnum', self.to_s, :plain)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def marshal_dump
|
220
|
+
self.to_s
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
YAML::add_domain_type("pennymac.com,2017-06-02", "stringEnum") do
|
225
|
+
|type, val|
|
226
|
+
StringEnum.new(val)
|
212
227
|
end
|
213
228
|
|
214
229
|
######################################################################
|
data/lib/marty/version.rb
CHANGED
data/marty.gemspec
CHANGED
@@ -42,9 +42,62 @@ A: M3::A
|
|
42
42
|
p =? 10
|
43
43
|
c = a * 2
|
44
44
|
d = pc - 1
|
45
|
+
e =?
|
46
|
+
f =?
|
47
|
+
g = e * 5 + f
|
48
|
+
h = f + 1
|
49
|
+
ptest = p * 10
|
45
50
|
result = [{"a": 123, "b": 456}, {"a": 789, "b": 101112}]
|
46
51
|
eof
|
47
52
|
|
53
|
+
sample_script5 = <<eof
|
54
|
+
A:
|
55
|
+
f =?
|
56
|
+
res = if f == "Apple"
|
57
|
+
then 0
|
58
|
+
else if f == "Banana"
|
59
|
+
then 1
|
60
|
+
else if f == "Orange"
|
61
|
+
then 2
|
62
|
+
else 9
|
63
|
+
eof
|
64
|
+
|
65
|
+
script3_schema = <<eof
|
66
|
+
A:
|
67
|
+
pc = { "properties : {
|
68
|
+
"p" : { "type" : "integer" },
|
69
|
+
}
|
70
|
+
}
|
71
|
+
eof
|
72
|
+
|
73
|
+
script4_schema = <<eof
|
74
|
+
A:
|
75
|
+
d = { "properties" : {
|
76
|
+
"p" : { "type" : "integer" },
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
g = { "properties" : {
|
81
|
+
"e" : { "type" : "integer" },
|
82
|
+
"f" : { "type" : "integer" },
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
lc = { "properties" : {
|
87
|
+
"p" : { "type" : "integer" },
|
88
|
+
}
|
89
|
+
}
|
90
|
+
eof
|
91
|
+
|
92
|
+
script5_schema = <<eof
|
93
|
+
A:
|
94
|
+
res = { "properties" : {
|
95
|
+
"f" : { "pg_enum" : "FruitsEnum" },
|
96
|
+
}
|
97
|
+
}
|
98
|
+
eof
|
99
|
+
|
100
|
+
|
48
101
|
describe Marty::RpcController do
|
49
102
|
before(:each) {
|
50
103
|
@routes = Marty::Engine.routes
|
@@ -63,6 +116,10 @@ describe Marty::RpcController do
|
|
63
116
|
"M2" => sample_script.gsub(/a/, "aa").gsub(/b/, "bb"),
|
64
117
|
"M3" => sample_script3,
|
65
118
|
"M4" => sample_script4,
|
119
|
+
"M5" => sample_script5,
|
120
|
+
"M3Schemas" => script3_schema,
|
121
|
+
"M4Schemas" => script4_schema,
|
122
|
+
"M5Schemas" => script5_schema,
|
66
123
|
}, Date.today + 1.minute)
|
67
124
|
|
68
125
|
@p1 = Marty::Posting.do_create("BASE", Date.today + 2.minute, 'a comment')
|
@@ -118,6 +175,28 @@ describe Marty::RpcController do
|
|
118
175
|
Delayed::Worker.delay_jobs = true
|
119
176
|
end
|
120
177
|
|
178
|
+
it "should be able to post background job with non-array attrs" do
|
179
|
+
Delayed::Worker.delay_jobs = false
|
180
|
+
post 'evaluate', {
|
181
|
+
format: :json,
|
182
|
+
script: "M1",
|
183
|
+
node: "B",
|
184
|
+
attrs: "e",
|
185
|
+
tag: t1.name,
|
186
|
+
params: { a: 333, d: 5}.to_json,
|
187
|
+
background: true,
|
188
|
+
}
|
189
|
+
res = ActiveSupport::JSON.decode response.body
|
190
|
+
expect(res).to include('job_id')
|
191
|
+
job_id = res['job_id']
|
192
|
+
|
193
|
+
promise = Marty::Promise.find_by_id(job_id)
|
194
|
+
|
195
|
+
expect(promise.result).to eq({"e"=>4})
|
196
|
+
|
197
|
+
Delayed::Worker.delay_jobs = true
|
198
|
+
end
|
199
|
+
|
121
200
|
it "should be able to post with complex data" do
|
122
201
|
post 'evaluate', {
|
123
202
|
format: :json,
|
@@ -291,6 +370,258 @@ describe Marty::RpcController do
|
|
291
370
|
expect(response.body).to eq("a,b\r\n123,456\r\n789,101112\r\n")
|
292
371
|
end
|
293
372
|
|
373
|
+
it "returns an error message on missing schema script" do
|
374
|
+
Marty::ApiConfig.create!(script: "M1",
|
375
|
+
node: "A",
|
376
|
+
attr: nil,
|
377
|
+
logged: false,
|
378
|
+
validated: true)
|
379
|
+
attrs = ["b"].to_json
|
380
|
+
params = {"a" => 5}.to_json
|
381
|
+
get 'evaluate', {
|
382
|
+
format: :csv,
|
383
|
+
script: "M1",
|
384
|
+
node: "A",
|
385
|
+
attrs: attrs,
|
386
|
+
params: params
|
387
|
+
}
|
388
|
+
expect = "Schema error for M1/A attrs=b: Schema not defined\r\n"
|
389
|
+
expect(response.body).to eq("error,#{expect}")
|
390
|
+
end
|
391
|
+
|
392
|
+
it "returns an error message on missing attributes in schema script" do
|
393
|
+
Marty::ApiConfig.create!(script: "M4",
|
394
|
+
node: "A",
|
395
|
+
attr: nil,
|
396
|
+
logged: false,
|
397
|
+
validated: true)
|
398
|
+
attrs = ["h"].to_json
|
399
|
+
params = {"f" => 5}.to_json
|
400
|
+
get 'evaluate', {
|
401
|
+
format: :csv,
|
402
|
+
script: "M4",
|
403
|
+
node: "A",
|
404
|
+
attrs: attrs,
|
405
|
+
params: params
|
406
|
+
}
|
407
|
+
expect = "Schema error for M4/A attrs=h: Problem with schema\r\n"
|
408
|
+
expect(response.body).to eq("error,#{expect}")
|
409
|
+
end
|
410
|
+
|
411
|
+
it "returns an error message on invalid schema" do
|
412
|
+
Marty::ApiConfig.create!(script: "M3",
|
413
|
+
node: "A",
|
414
|
+
attr: nil,
|
415
|
+
logged: false,
|
416
|
+
validated: true)
|
417
|
+
attrs = ["pc"].to_json
|
418
|
+
params = {"p" => 5}.to_json
|
419
|
+
get 'evaluate', {
|
420
|
+
format: :csv,
|
421
|
+
script: "M3",
|
422
|
+
node: "A",
|
423
|
+
attrs: attrs,
|
424
|
+
params: params
|
425
|
+
}
|
426
|
+
expect = "Schema error for M3/A attrs=pc: Problem with schema\r\n"
|
427
|
+
expect(response.body).to eq("error,#{expect}")
|
428
|
+
end
|
429
|
+
|
430
|
+
it "returns a validation error when validating a single attribute" do
|
431
|
+
Marty::ApiConfig.create!(script: "M4",
|
432
|
+
node: "A",
|
433
|
+
attr: nil,
|
434
|
+
logged: false,
|
435
|
+
validated: true)
|
436
|
+
attrs = ["d"].to_json
|
437
|
+
params = {"p" => "132"}.to_json
|
438
|
+
get 'evaluate', {
|
439
|
+
format: :csv,
|
440
|
+
script: "M4",
|
441
|
+
node: "A",
|
442
|
+
attrs: attrs,
|
443
|
+
params: params
|
444
|
+
}
|
445
|
+
expect = '""d""=>[""The property \'#/p\' of type string did not '\
|
446
|
+
'match the following type: integer'
|
447
|
+
expect(response.body).to include(expect)
|
448
|
+
end
|
449
|
+
|
450
|
+
it "returns a validation error when validating multiple attributes" do
|
451
|
+
Marty::ApiConfig.create!(script: "M4",
|
452
|
+
node: "A",
|
453
|
+
attr: nil,
|
454
|
+
logged: false,
|
455
|
+
validated: true)
|
456
|
+
attrs = ["d", "g"].to_json
|
457
|
+
params = {"p" => "132", "e" => "55", "f"=>"16"}.to_json
|
458
|
+
get 'evaluate', {
|
459
|
+
format: :csv,
|
460
|
+
script: "M4",
|
461
|
+
node: "A",
|
462
|
+
attrs: attrs,
|
463
|
+
params: params
|
464
|
+
}
|
465
|
+
expect = '""d""=>[""The property \'#/p\' of type string did not '\
|
466
|
+
'match the following type: integer'
|
467
|
+
expect(response.body).to include(expect)
|
468
|
+
expect = '""g""=>[""The property \'#/e\' of type string did not '\
|
469
|
+
'match the following type: integer'
|
470
|
+
expect(response.body).to include(expect)
|
471
|
+
expect = 'The property \'#/f\' of type string did not '\
|
472
|
+
'match the following type: integer'
|
473
|
+
expect(response.body).to include(expect)
|
474
|
+
end
|
475
|
+
|
476
|
+
it "validates schema" do
|
477
|
+
Marty::ApiConfig.create!(script: "M4",
|
478
|
+
node: "A",
|
479
|
+
attr: nil,
|
480
|
+
logged: false,
|
481
|
+
validated: true)
|
482
|
+
attrs = ["lc"].to_json
|
483
|
+
params = {"p" => 5}.to_json
|
484
|
+
get 'evaluate', {
|
485
|
+
format: :csv,
|
486
|
+
script: "M4",
|
487
|
+
node: "A",
|
488
|
+
attrs: attrs,
|
489
|
+
params: params
|
490
|
+
}
|
491
|
+
expect(response.body).to eq("9\r\n9\r\n")
|
492
|
+
end
|
493
|
+
|
494
|
+
class FruitsEnum
|
495
|
+
VALUES=Set['Apple', 'Banana', 'Orange']
|
496
|
+
end
|
497
|
+
|
498
|
+
it "validates schema with a pg_enum (Positive)" do
|
499
|
+
Marty::ApiConfig.create!(script: "M5",
|
500
|
+
node: "A",
|
501
|
+
attr: nil,
|
502
|
+
logged: false,
|
503
|
+
validated: true)
|
504
|
+
attrs = ["res"].to_json
|
505
|
+
params = {"f" => "Banana"}.to_json
|
506
|
+
get 'evaluate', {
|
507
|
+
format: :csv,
|
508
|
+
script: "M5",
|
509
|
+
node: "A",
|
510
|
+
attrs: attrs,
|
511
|
+
params: params
|
512
|
+
}
|
513
|
+
expect(response.body).to eq("1\r\n")
|
514
|
+
end
|
515
|
+
|
516
|
+
it "validates schema with a pg_enum (Negative)" do
|
517
|
+
Marty::ApiConfig.create!(script: "M5",
|
518
|
+
node: "A",
|
519
|
+
attr: nil,
|
520
|
+
logged: false,
|
521
|
+
validated: true)
|
522
|
+
attrs = ["res"].to_json
|
523
|
+
params = {"f" => "Beans"}.to_json
|
524
|
+
get 'evaluate', {
|
525
|
+
format: :csv,
|
526
|
+
script: "M5",
|
527
|
+
node: "A",
|
528
|
+
attrs: attrs,
|
529
|
+
params: params
|
530
|
+
}
|
531
|
+
expect = '""res""=>[""Class error: \'Beans\' not contained in FruitsEnum'
|
532
|
+
expect(response.body).to include(expect)
|
533
|
+
end
|
534
|
+
|
535
|
+
it "should log good req" do
|
536
|
+
Marty::ApiConfig.create!(script: "M3",
|
537
|
+
node: "A",
|
538
|
+
attr: nil,
|
539
|
+
logged: true)
|
540
|
+
attrs = ["lc"].to_json
|
541
|
+
params = {"p" => 5}
|
542
|
+
get 'evaluate', {
|
543
|
+
format: :csv,
|
544
|
+
script: "M3",
|
545
|
+
node: "A",
|
546
|
+
attrs: attrs,
|
547
|
+
params: params.to_json
|
548
|
+
}
|
549
|
+
expect(response.body).to eq("9\r\n9\r\n")
|
550
|
+
log = Marty::ApiLog.order(id: :desc).first
|
551
|
+
|
552
|
+
expect(log.script).to eq("M3")
|
553
|
+
expect(log.node).to eq("A")
|
554
|
+
expect(log.attrs).to eq(attrs)
|
555
|
+
expect(log.input).to eq(params)
|
556
|
+
expect(log.output).to eq([[9, 9]])
|
557
|
+
expect(log.remote_ip).to eq("0.0.0.0")
|
558
|
+
expect(log.error).to eq(nil)
|
559
|
+
|
560
|
+
end
|
561
|
+
|
562
|
+
it "should log good req [background]" do
|
563
|
+
Marty::ApiConfig.create!(script: "M3",
|
564
|
+
node: "A",
|
565
|
+
attr: nil,
|
566
|
+
logged: true)
|
567
|
+
attrs = ["lc"].to_json
|
568
|
+
params = {"p" => 5}
|
569
|
+
get 'evaluate', {
|
570
|
+
format: :csv,
|
571
|
+
script: "M3",
|
572
|
+
node: "A",
|
573
|
+
attrs: attrs,
|
574
|
+
params: params.to_json,
|
575
|
+
background: true
|
576
|
+
}
|
577
|
+
expect(response.body).to match(/job_id,/)
|
578
|
+
log = Marty::ApiLog.order(id: :desc).first
|
579
|
+
|
580
|
+
expect(log.script).to eq("M3")
|
581
|
+
expect(log.node).to eq("A")
|
582
|
+
expect(log.attrs).to eq(attrs)
|
583
|
+
expect(log.input).to eq(params)
|
584
|
+
expect(log.output).to include("job_id")
|
585
|
+
expect(log.remote_ip).to eq("0.0.0.0")
|
586
|
+
expect(log.error).to eq(nil)
|
587
|
+
|
588
|
+
end
|
589
|
+
|
590
|
+
it "should not log if it should not log" do
|
591
|
+
get 'evaluate', {
|
592
|
+
format: :json,
|
593
|
+
script: "M1",
|
594
|
+
node: "A",
|
595
|
+
attrs: ["a", "b"].to_json,
|
596
|
+
tag: t1.name,
|
597
|
+
}
|
598
|
+
expect(Marty::ApiLog.count).to eq(0)
|
599
|
+
end
|
600
|
+
|
601
|
+
it "should handle atom attribute" do
|
602
|
+
Marty::ApiConfig.create!(script: "M3",
|
603
|
+
node: "A",
|
604
|
+
attr: nil,
|
605
|
+
logged: true)
|
606
|
+
params = {"p" => 5}
|
607
|
+
get 'evaluate', {
|
608
|
+
format: :csv,
|
609
|
+
script: "M3",
|
610
|
+
node: "A",
|
611
|
+
attrs: "lc",
|
612
|
+
params: params.to_json
|
613
|
+
}
|
614
|
+
expect(response.body).to eq("9\r\n9\r\n")
|
615
|
+
log = Marty::ApiLog.order(id: :desc).first
|
616
|
+
expect(log.script).to eq("M3")
|
617
|
+
expect(log.node).to eq("A")
|
618
|
+
expect(log.attrs).to eq("lc")
|
619
|
+
expect(log.input).to eq(params)
|
620
|
+
expect(log.output).to eq([9, 9])
|
621
|
+
expect(log.remote_ip).to eq("0.0.0.0")
|
622
|
+
expect(log.error).to eq(nil)
|
623
|
+
end
|
624
|
+
|
294
625
|
it "should support api authorization - api_key not required" do
|
295
626
|
api = Marty::ApiAuth.new
|
296
627
|
api.app_name = 'TestApp'
|
@@ -327,6 +658,9 @@ describe Marty::RpcController do
|
|
327
658
|
api.script_name = 'M3'
|
328
659
|
api.save!
|
329
660
|
|
661
|
+
apic = Marty::ApiConfig.create!(script: 'M3',
|
662
|
+
logged: true)
|
663
|
+
|
330
664
|
get 'evaluate', {
|
331
665
|
format: :json,
|
332
666
|
script: "M3",
|
@@ -335,6 +669,13 @@ describe Marty::RpcController do
|
|
335
669
|
api_key: api.api_key,
|
336
670
|
}
|
337
671
|
expect(response.body).to eq([7].to_json)
|
672
|
+
log = Marty::ApiLog.order(id: :desc).first
|
673
|
+
expect(log.script).to eq('M3')
|
674
|
+
expect(log.node).to eq('C')
|
675
|
+
expect(log.attrs).to eq(%Q!["pc"]!)
|
676
|
+
expect(log.output).to eq([7])
|
677
|
+
expect(log.remote_ip).to eq("0.0.0.0")
|
678
|
+
expect(log.auth_name).to eq("TestApp")
|
338
679
|
end
|
339
680
|
|
340
681
|
it "should support api authorization - api_key required but incorrect" do
|
@@ -356,7 +697,7 @@ describe Marty::RpcController do
|
|
356
697
|
context "error handling" do
|
357
698
|
it 'returns bad attrs if attrs is not a string' do
|
358
699
|
get :evaluate, format: :json, attrs: 0
|
359
|
-
expect(response.body).to match(/"error":"
|
700
|
+
expect(response.body).to match(/"error":"Malformed attrs"/)
|
360
701
|
end
|
361
702
|
|
362
703
|
it 'returns malformed attrs for improperly formatted json' do
|