etna 0.1.27 → 0.1.32

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.
data/lib/etna/command.rb CHANGED
@@ -39,6 +39,10 @@ module Etna
39
39
  def string_flags
40
40
  @string_flags ||= []
41
41
  end
42
+
43
+ def multi_flags
44
+ @multi_flags ||= []
45
+ end
42
46
  end
43
47
 
44
48
  def flag_as_parameter(flag)
@@ -69,6 +73,12 @@ module Etna
69
73
  else
70
74
  flags[arg_name] = args.shift
71
75
  end
76
+ elsif self.class.multi_flags.include?(next_arg)
77
+ if args.empty?
78
+ raise "flag #{next_arg} requires an argument"
79
+ else
80
+ (flags[arg_name] ||= []) << args.shift
81
+ end
72
82
  elsif !found_non_flag
73
83
  raise "#{program_name} does not recognize flag #{next_arg}"
74
84
  else
data/lib/etna/cwl.rb ADDED
@@ -0,0 +1,701 @@
1
+ require 'yaml'
2
+
3
+ module Etna
4
+ class Cwl
5
+ FIELD_LOADERS = {}
6
+
7
+ def initialize(attributes)
8
+ @attributes = attributes
9
+ end
10
+
11
+ def self.loader
12
+ Etna::Cwl::RecordLoader.new(self)
13
+ end
14
+
15
+ def self.as_json(obj)
16
+ if obj.is_a?(Cwl)
17
+ as_json(obj.instance_variable_get(:@attributes))
18
+ elsif obj.is_a?(Hash)
19
+ {}.tap do |result|
20
+ obj.each do |k, v|
21
+ result[k] = as_json(v)
22
+ end
23
+ end
24
+ elsif obj.is_a?(Array)
25
+ obj.map { |v| as_json(v) }
26
+ else
27
+ obj
28
+ end
29
+ end
30
+
31
+ def as_json
32
+ self.class.as_json(@attributes)
33
+ end
34
+
35
+ class Loader
36
+ def load(val)
37
+ raise "Unimplemented"
38
+ end
39
+
40
+ def optional
41
+ OptionalLoader.new(self)
42
+ end
43
+
44
+ def map(&block)
45
+ FunctorMapLoader.new(self, &block)
46
+ end
47
+
48
+ def as_mapped_array(id_key = nil, value_key = nil)
49
+ MapLoader.new(self.as_array, id_key, value_key)
50
+ end
51
+
52
+ def or(*alternatives)
53
+ UnionLoader.new(self, *alternatives)
54
+ end
55
+
56
+ def as_array
57
+ ArrayLoader.new(self)
58
+ end
59
+ end
60
+
61
+ class AnyLoader < Loader
62
+ def load(val)
63
+ val
64
+ end
65
+
66
+ ANY = AnyLoader.new
67
+ end
68
+
69
+ class PrimitiveLoader < Loader
70
+ def initialize(name, type)
71
+ @name = name
72
+ @type = type
73
+ end
74
+
75
+ def load(val)
76
+ unless val.is_a?(@type)
77
+ raise "Unexpected val #{val.inspect} for #{@name} type"
78
+ end
79
+
80
+ val
81
+ end
82
+
83
+ def name
84
+ @name
85
+ end
86
+
87
+ def self.find_primitive_type_loader(type_name)
88
+ constants.each do |c|
89
+ c = const_get(c)
90
+ if c.is_a?(Loader)
91
+ return c if c.name == type_name
92
+ end
93
+ end
94
+ end
95
+
96
+ STRING = PrimitiveLoader.new('string', String)
97
+ INT = PrimitiveLoader.new('int', Integer)
98
+ LONG = PrimitiveLoader.new('long', Integer)
99
+ FLOAT = PrimitiveLoader.new('float', Float)
100
+ DOUBLE = PrimitiveLoader.new('double', Float)
101
+ NULL = PrimitiveLoader.new('null', NilClass)
102
+
103
+ class BooleanLoader < Loader
104
+ def name
105
+ 'boolean'
106
+ end
107
+
108
+ def load(val)
109
+ raise "Invalid value #{val.inspect} for boolean" unless val.instance_of?(TrueClass) || val.instance_of?(FalseClass)
110
+ val
111
+ end
112
+ end
113
+
114
+ BOOLEAN = BooleanLoader.new
115
+ end
116
+
117
+ class SourceLoader < Loader
118
+ # Resolves a string of the forms "a-primary-identifier" or "step_name/output" into
119
+ # [:primary_inputs, "a-primary-identifier"] or
120
+ # ["step_name", "output"] respectively
121
+ def load(val)
122
+ parts = []
123
+
124
+ if val.is_a?(Symbol)
125
+ val = val.to_s
126
+ end
127
+
128
+ if val.is_a?(Array)
129
+ parts = PrimitiveLoader::STRING.as_array.load(val)
130
+ elsif val.is_a?(String)
131
+ parts = val.split('/', max = 2)
132
+ end
133
+
134
+ if parts.length == 1
135
+ return [:primary_inputs, parts[0]]
136
+ elsif parts.length == 2
137
+ return parts
138
+ end
139
+
140
+ raise "Unexpected value for source #{val.inspect}"
141
+ end
142
+ end
143
+
144
+ class StrictMapLoader < Loader
145
+ def initialize(items, keys)
146
+ @items = items
147
+ @keys = keys
148
+ end
149
+
150
+ def load(val)
151
+ if val.is_a?(Hash)
152
+ val.map do |k, v|
153
+ [@keys.load(k), @items.load(v)]
154
+ end.to_h
155
+ else
156
+ raise "Unexpected val #{val.inspect} for hash"
157
+ end
158
+ end
159
+ end
160
+
161
+ class MapLoader < Loader
162
+ def initialize(items, idKey = nil, valueKey = nil)
163
+ @items = items
164
+ @idKey = idKey
165
+ @valueKey = valueKey
166
+ end
167
+
168
+ def load(val)
169
+ if val.is_a?(Hash)
170
+ val = [].tap do |result|
171
+ errors = {}
172
+ val.keys.sort.each do |k|
173
+ begin
174
+ v = val[k]
175
+ if v.is_a?(Hash)
176
+ v[@idKey] = k
177
+ else
178
+ v = {@idKey => k, @valueKey => v}
179
+ end
180
+
181
+ result << v
182
+ rescue => e
183
+ errors[k] = e.to_s
184
+ end
185
+ end
186
+
187
+ unless errors.empty?
188
+ raise errors.map { |k, v| "#{k}: #{v}" }.join("\n")
189
+ end
190
+ end
191
+ end
192
+
193
+ @items.load(val)
194
+ end
195
+ end
196
+
197
+ class ArrayLoader < Loader
198
+ def initialize(items)
199
+ @items = items
200
+ end
201
+
202
+ def load(val)
203
+ unless val.is_a?(Array)
204
+ raise "Unexpected val #{val.inspect} for array"
205
+ end
206
+
207
+ [].tap do |result|
208
+ errors = []
209
+ val.each do |item|
210
+ begin
211
+ loaded = Cwl.load_item(item, UnionLoader.new(self, @items))
212
+ if loaded.is_a?(Array)
213
+ result.push(*loaded)
214
+ else
215
+ result << loaded
216
+ end
217
+ rescue => e
218
+ errors << e.to_s
219
+ end
220
+ end
221
+
222
+ unless errors.empty?
223
+ raise errors.join("\n")
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ class EnumLoader < Loader
230
+ def initialize(*options)
231
+ @options = options
232
+ end
233
+
234
+ def load(val)
235
+ if @options.include?(val)
236
+ return val
237
+ end
238
+
239
+ raise "Value #{val.inspect} does not belong to one of (#{@options.join(', ')})"
240
+ end
241
+
242
+ PRIMITIVE_TYPE = EnumLoader.new("null", "boolean", "int", "long", "float", "double", "string")
243
+ NOMINAL_TYPE = EnumLoader.new("File")
244
+ end
245
+
246
+ class OptionalLoader < Loader
247
+ def initialize(inner_loader)
248
+ @inner_loader = inner_loader
249
+ end
250
+
251
+ def load(val)
252
+ if val.nil?
253
+ return nil
254
+ end
255
+
256
+ @inner_loader.load(val)
257
+ end
258
+ end
259
+
260
+ class RecordLoader < Loader
261
+ def initialize(klass, field_loaders = nil)
262
+ @klass = klass
263
+ @field_loaders = field_loaders
264
+ end
265
+
266
+ def field_loaders
267
+ @field_loaders || @klass::FIELD_LOADERS
268
+ end
269
+
270
+ def load(val)
271
+ unless val.is_a?(Hash)
272
+ raise "Unexpected value #{val.inspect} for type #{@klass.name}"
273
+ end
274
+
275
+ errors = {}
276
+ @klass.new({}.tap do |result|
277
+ field_loaders.each do |field_sym, loader|
278
+ field_str = field_sym.to_s
279
+ begin
280
+ result[field_str] = loader.load(val[field_str])
281
+ rescue => e
282
+ errors[field_str] = e.to_s
283
+ end
284
+ end
285
+
286
+ unless errors.empty?
287
+ raise errors.map { |k, e| "#{k}: #{e}" }.join(',')
288
+ end
289
+ end)
290
+ end
291
+ end
292
+
293
+ class NeverLoader < Loader
294
+ def load(val)
295
+ raise "This feature is not supported"
296
+ end
297
+
298
+ UNSUPPORTED = NeverLoader.new.optional
299
+ end
300
+
301
+ class UnionLoader < Loader
302
+ def initialize(*alternatives)
303
+ @alternatives = alternatives
304
+ end
305
+
306
+ def load(val)
307
+ errors = []
308
+ @alternatives.each do |loader|
309
+ begin
310
+ return loader.load(val)
311
+ rescue => e
312
+ errors << e.to_s
313
+ end
314
+ end
315
+
316
+ raise errors.join(", ")
317
+ end
318
+ end
319
+
320
+ class ArrayType < Cwl
321
+ class InnerLoader < Loader
322
+ def load(val)
323
+ RecordLoader.new(ArrayType, {
324
+ type: EnumLoader.new("array"),
325
+ items: TypedDSLLoader::WITH_UNIONS_TYPE_LOADER,
326
+ }).load(val)
327
+ end
328
+ end
329
+
330
+ def type_loader
331
+ loader = RecordType::Field.type_loader(@attributes['items'])
332
+ return nil if loader.nil?
333
+ @type_loader ||= ArrayLoader.new(loader)
334
+ end
335
+ end
336
+
337
+ class EnumType < Cwl
338
+ class InnerLoader < Loader
339
+ def load(val)
340
+ RecordLoader.new(EnumType, {
341
+ type: EnumLoader.new("enum"),
342
+ symbols: PrimitiveLoader::STRING.as_array,
343
+ }).load(val)
344
+ end
345
+ end
346
+
347
+ def type_loader
348
+ @type_loader ||= EnumLoader.new(*@attributes['symbols'])
349
+ end
350
+ end
351
+
352
+ class RecordType < Cwl
353
+ class RecordTypeLoader
354
+ def load(val)
355
+ RecordLoader.new(RecordType, {
356
+ type: EnumLoader.new("record"),
357
+ fields: Field::FieldLoader.new.as_mapped_array('name', 'type')
358
+ }).load(val)
359
+ end
360
+ end
361
+
362
+ class Record
363
+ def self.new(h)
364
+ h
365
+ end
366
+ end
367
+
368
+ def type_loader
369
+ @type_loader ||= begin
370
+ record_class = Class.new(Record)
371
+ RecordLoader.new(record_class, @attributes['fields'].map do |field|
372
+ loader = Field.type_loader(field.type)
373
+ return nil if loader.nil?
374
+ [field.name.to_sym, loader]
375
+ end.to_h)
376
+ end
377
+ end
378
+
379
+ class Field < Cwl
380
+ class FieldLoader < Loader
381
+ def load(val)
382
+ RecordLoader.new(Field, {
383
+ name: PrimitiveLoader::STRING,
384
+ type: TypedDSLLoader::WITH_UNIONS_TYPE_LOADER,
385
+ doc: PrimitiveLoader::STRING.optional,
386
+ }).load(val)
387
+ end
388
+ end
389
+
390
+ def name
391
+ @attributes['name']
392
+ end
393
+
394
+ def type
395
+ @attributes['type']
396
+ end
397
+
398
+ def type_loader
399
+ self.class.type_loader(self.type)
400
+ end
401
+
402
+ def self.type_loader(type)
403
+ case type
404
+ when Array
405
+ type_loaders = type.map { |t| Field.type_loader(t) }
406
+ return nil if type_loaders.any?(&:nil?)
407
+ UnionLoader.new(*type_loaders)
408
+ when EnumType
409
+ type.type_loader
410
+ when RecordType
411
+ type.type_loader
412
+ when ArrayType
413
+ type.type_loader
414
+ when String
415
+ PrimitiveLoader.find_primitive_type_loader(type)
416
+ else
417
+ raise "Could not determine loader for type #{type.inspect}"
418
+ end
419
+ end
420
+ end
421
+ end
422
+
423
+ class FunctorMapLoader < Loader
424
+ def initialize(inner, &block)
425
+ @block = block
426
+ @inner = inner
427
+ end
428
+
429
+ def load(val)
430
+ @block.call(@inner.load(val))
431
+ end
432
+ end
433
+
434
+ # Prepares a unique set of structured nominal types for an inner
435
+ # loading of types
436
+ class TypedDSLLoader < Loader
437
+ def initialize(inner)
438
+ @inner = inner
439
+ end
440
+
441
+ REGEX = /^([^\[?]+)(\[\])?(\?)?$/
442
+
443
+ def resolve(val)
444
+ m = REGEX.match(val)
445
+
446
+ unless m.nil?
447
+ type = m[1]
448
+ unless m[2].nil?
449
+ type = {'type' => 'array', 'items' => type}
450
+ end
451
+ unless m[3].nil?
452
+ type = ["null", type]
453
+ end
454
+
455
+ return type
456
+ end
457
+
458
+ val
459
+ end
460
+
461
+ def load(val)
462
+ if val.is_a?(Array)
463
+ @inner.load(val.map do |item|
464
+ item.is_a?(String) ? resolve(item) : item
465
+ end)
466
+ elsif val.is_a?(String)
467
+ @inner.load(resolve(val))
468
+ else
469
+ @inner.load(val)
470
+ end
471
+ end
472
+
473
+ OUTER_TYPE_LOADER = TypedDSLLoader.new(
474
+ UnionLoader.new(
475
+ RecordType::RecordTypeLoader.new,
476
+ ArrayType::InnerLoader.new,
477
+ EnumType::InnerLoader.new,
478
+ EnumLoader::PRIMITIVE_TYPE,
479
+ EnumLoader::NOMINAL_TYPE,
480
+ )
481
+ )
482
+
483
+ WITH_UNIONS_TYPE_LOADER = TypedDSLLoader.new(
484
+ UnionLoader.new(
485
+ OUTER_TYPE_LOADER,
486
+ ArrayLoader.new(OUTER_TYPE_LOADER),
487
+ )
488
+ )
489
+ end
490
+
491
+ def self.load_item(val, field_type)
492
+ if val.is_a?(Hash)
493
+ if val.include?("$import")
494
+ raise "$import expressions are not yet supported"
495
+ elsif val.include?("$include")
496
+ raise "$include expressions are not yet supported"
497
+ end
498
+ end
499
+
500
+ return field_type.load(val)
501
+ end
502
+
503
+ class InputParameter < Cwl
504
+ FIELD_LOADERS = {
505
+ id: PrimitiveLoader::STRING.optional,
506
+ label: PrimitiveLoader::STRING.optional,
507
+ secondaryFiles: NeverLoader::UNSUPPORTED,
508
+ streamable: NeverLoader::UNSUPPORTED,
509
+ loadContents: NeverLoader::UNSUPPORTED,
510
+ loadListing: NeverLoader::UNSUPPORTED,
511
+ valueFrom: NeverLoader::UNSUPPORTED,
512
+ doc: PrimitiveLoader::STRING.optional,
513
+
514
+ type: TypedDSLLoader::WITH_UNIONS_TYPE_LOADER,
515
+ default: AnyLoader::ANY.optional,
516
+ format: PrimitiveLoader::STRING.optional,
517
+ }
518
+
519
+ def default
520
+ default = @attributes['default']
521
+ return nil unless default
522
+ RecordType::Field.type_loader(@attributes['type'])&.load(default)
523
+ end
524
+ end
525
+
526
+ class OutputParameter < Cwl
527
+ FIELD_LOADERS = {
528
+ id: PrimitiveLoader::STRING.optional,
529
+ label: PrimitiveLoader::STRING.optional,
530
+ secondaryFiles: NeverLoader::UNSUPPORTED,
531
+ streamable: NeverLoader::UNSUPPORTED,
532
+ doc: PrimitiveLoader::STRING.optional,
533
+
534
+ outputBinding: NeverLoader::UNSUPPORTED,
535
+ type: TypedDSLLoader::WITH_UNIONS_TYPE_LOADER,
536
+ format: PrimitiveLoader::STRING.optional,
537
+ }
538
+ end
539
+
540
+ class WorkflowOutputParameter < Cwl
541
+ FIELD_LOADERS = {
542
+ id: PrimitiveLoader::STRING,
543
+ label: PrimitiveLoader::STRING.optional,
544
+ secondaryFiles: NeverLoader::UNSUPPORTED,
545
+ streamable: NeverLoader::UNSUPPORTED,
546
+ linkMerge: NeverLoader::UNSUPPORTED,
547
+ pickValue: NeverLoader::UNSUPPORTED,
548
+ doc: PrimitiveLoader::STRING.optional,
549
+
550
+ outputSource: SourceLoader.new,
551
+ type: TypedDSLLoader::WITH_UNIONS_TYPE_LOADER,
552
+ format: PrimitiveLoader::STRING.optional,
553
+ }
554
+
555
+ def outputSource
556
+ @attributes['outputSource']
557
+ end
558
+
559
+ def id
560
+ @attributes['id']
561
+ end
562
+ end
563
+
564
+ class WorkflowInputParameter < Cwl
565
+ FIELD_LOADERS = {
566
+ id: PrimitiveLoader::STRING,
567
+ label: PrimitiveLoader::STRING.optional,
568
+ secondaryFiles: NeverLoader::UNSUPPORTED,
569
+ streamable: NeverLoader::UNSUPPORTED,
570
+ loadContents: NeverLoader::UNSUPPORTED,
571
+ loadListing: NeverLoader::UNSUPPORTED,
572
+ doc: PrimitiveLoader::STRING.optional,
573
+ inputBinding: NeverLoader::UNSUPPORTED,
574
+
575
+ default: AnyLoader::ANY,
576
+ type: TypedDSLLoader::WITH_UNIONS_TYPE_LOADER,
577
+ format: PrimitiveLoader::STRING.optional,
578
+ }
579
+
580
+ def id
581
+ @attributes['id']
582
+ end
583
+
584
+ def default
585
+ @attributes['default']
586
+ end
587
+
588
+ def type
589
+ @attributes['type']
590
+ end
591
+ end
592
+
593
+ class StepOutput < Cwl
594
+ FIELD_LOADERS = {
595
+ id: PrimitiveLoader::STRING,
596
+ }
597
+
598
+ def id
599
+ @attributes['id']
600
+ end
601
+ end
602
+
603
+
604
+ class StepInput < Cwl
605
+ FIELD_LOADERS = {
606
+ id: PrimitiveLoader::STRING.optional,
607
+ source: SourceLoader.new.optional,
608
+ label: PrimitiveLoader::STRING.optional,
609
+ linkMerge: NeverLoader::UNSUPPORTED,
610
+ pickValue: NeverLoader::UNSUPPORTED,
611
+ loadContents: NeverLoader::UNSUPPORTED,
612
+ loadListing: NeverLoader::UNSUPPORTED,
613
+ valueFrom: NeverLoader::UNSUPPORTED,
614
+ default: AnyLoader::ANY.optional,
615
+ }
616
+
617
+ def id
618
+ @attributes['id']
619
+ end
620
+
621
+ def source
622
+ @attributes['source']
623
+ end
624
+ end
625
+
626
+ class Operation < Cwl
627
+ FIELD_LOADERS = {
628
+ id: PrimitiveLoader::STRING.optional,
629
+ label: PrimitiveLoader::STRING.optional,
630
+ doc: PrimitiveLoader::STRING.optional,
631
+ requirements: NeverLoader::UNSUPPORTED,
632
+ hints: NeverLoader::UNSUPPORTED,
633
+ cwlVersion: EnumLoader.new("v1.0", "v1.1", "v1.2").optional,
634
+ intent: NeverLoader::UNSUPPORTED,
635
+ class: EnumLoader.new("Operation"),
636
+ inputs: InputParameter.loader.as_mapped_array('id', 'type'),
637
+ outputs: OutputParameter.loader.as_mapped_array('id', 'type'),
638
+ }
639
+
640
+ def id
641
+ @attributes['id']
642
+ end
643
+ end
644
+
645
+ class Step < Cwl
646
+ FIELD_LOADERS = {
647
+ id: PrimitiveLoader::STRING.optional,
648
+ label: PrimitiveLoader::STRING.optional,
649
+ doc: PrimitiveLoader::STRING.optional,
650
+ in: StepInput.loader.as_mapped_array('id', 'source'),
651
+ out: StepOutput.loader.or(PrimitiveLoader::STRING.map { |id| StepOutput.loader.load({'id' => id}) }).as_array,
652
+ requirements: NeverLoader::UNSUPPORTED,
653
+ hints: NeverLoader::UNSUPPORTED,
654
+ run: PrimitiveLoader::STRING.map { |id| Operation.loader.load({'id' => id, 'class' => 'Operation', 'inputs' => [], 'outputs' => []}) }.or(Operation.loader),
655
+ when: NeverLoader::UNSUPPORTED,
656
+ scatter: NeverLoader::UNSUPPORTED,
657
+ scatterMethod: NeverLoader::UNSUPPORTED,
658
+ }
659
+
660
+ def id
661
+ @attributes['id']
662
+ end
663
+
664
+ def in
665
+ @attributes['in']
666
+ end
667
+
668
+ def out
669
+ @attributes['out']
670
+ end
671
+ end
672
+
673
+ class Workflow < Cwl
674
+ FIELD_LOADERS = {
675
+ id: PrimitiveLoader::STRING.optional,
676
+ label: PrimitiveLoader::STRING.optional,
677
+ doc: PrimitiveLoader::STRING.optional,
678
+ requirements: NeverLoader::UNSUPPORTED,
679
+ hints: NeverLoader::UNSUPPORTED,
680
+ intent: NeverLoader::UNSUPPORTED,
681
+ class: EnumLoader.new("Workflow"),
682
+ cwlVersion: EnumLoader.new("v1.0", "v1.1", "v1.2"),
683
+ inputs: WorkflowInputParameter.loader.as_mapped_array('id', 'type'),
684
+ outputs: WorkflowOutputParameter.loader.as_mapped_array('id', 'type'),
685
+ steps: Step.loader.as_mapped_array('id', 'source')
686
+ }
687
+
688
+ def inputs
689
+ @attributes['inputs']
690
+ end
691
+
692
+ def outputs
693
+ @attributes['outputs']
694
+ end
695
+
696
+ def steps
697
+ @attributes['steps']
698
+ end
699
+ end
700
+ end
701
+ end