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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Marty
2
- VERSION = "1.0.27"
2
+ VERSION = "1.0.28"
3
3
  end
data/marty.gemspec CHANGED
@@ -41,4 +41,5 @@ Gem::Specification.new do |s|
41
41
  s.add_dependency 'net-ldap', '~> 0.12.0'
42
42
  s.add_dependency 'rubyzip'
43
43
  s.add_dependency 'sqlite3'
44
+ s.add_dependency 'json-schema'
44
45
  end
@@ -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":"Bad attrs"/)
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