1 /**
2 
3 This module provides structs that encapsulate VCFHeader and HeaderRecord
4 
5 `VCFHeader` encapsulates and owns a `bcf_hdr_t*`,
6 and provides convenience functions to read and write header records.
7 
8 `HeaderRecord` provides an easy way to coonstruct new header records and
9 convert them to bcf_hrec_t * for use by the htslib API.
10 
11 */
12 
13 module dhtslib.vcf.header;
14 
15 import std.datetime;
16 import std.string: fromStringz, toStringz;
17 import std.format: format;
18 import std.traits : isArray, isIntegral, isSomeString;
19 import std.conv: to, ConvException;
20 import std.algorithm : map;
21 import std.array : array;
22 import std.utf : toUTFz;
23 
24 import dhtslib.memory;
25 import dhtslib.vcf;
26 import htslib.vcf;
27 import htslib.hts_log;
28 
29 
30 /// Struct for easy setting and getting of bcf_hrec_t values for VCFheader
31 struct HeaderRecord
32 {
33     /// HeaderRecordType type i.e INFO, contig, FORMAT
34     HeaderRecordType recType = HeaderRecordType.None;
35 
36     /// string of HeaderRecordType type i.e INFO, contig, FORMAT ?
37     /// or could be ##source=program
38     ///               ======
39     string key;
40 
41     /// mostly empty except for
42     /// this ##source=program
43     ///               =======
44     string value;
45 
46     /// number kv pairs
47     int nkeys;
48 
49     /// kv pair keys
50     string[] keys;
51 
52     /// kv pair values
53     string[] vals;
54 
55     /// HDR IDX value
56     int idx = -1;
57 
58     /// HDR Length value A, R, G, ., FIXED
59     HeaderLengths lenthType = HeaderLengths.None;
60 
61     /// if HDR Length value is FIXED
62     /// this is the number
63     int length = -1;
64 
65     /// HDR Length value INT, FLOAT, STRING
66     HeaderTypes valueType = HeaderTypes.None;
67 
68     invariant
69     {
70         assert(this.keys.length == this.vals.length);
71     }
72     /// ctor from a bcf_hrec_t
73     this(bcf_hrec_t * rec){
74 
75         /// Set the easy stuff
76         this.recType = cast(HeaderRecordType) rec.type;
77         this.key = fromStringz(rec.key).dup;
78         this.value = fromStringz(rec.value).dup;
79         this.nkeys = rec.nkeys;
80 
81         /// get the kv pairs
82         /// special logic for Number and Type
83         for(auto i=0; i < rec.nkeys; i++){
84             keys ~= fromStringz(rec.keys[i]).dup;
85             vals ~= fromStringz(rec.vals[i]).dup;
86             if(keys[i] == "Number")
87             {
88                 switch(vals[i]){
89                     case "A":
90                         this.lenthType = HeaderLengths.OnePerAltAllele;
91                         break;
92                     case "G":
93                         this.lenthType = HeaderLengths.OnePerGenotype;
94                         break;
95                     case "R":
96                         this.lenthType = HeaderLengths.OnePerAllele;
97                         break;
98                     case ".":
99                         this.lenthType = HeaderLengths.Variable;
100                         break;
101                     default:
102                         this.lenthType = HeaderLengths.Fixed;
103                         this.length = vals[i].to!int;
104                         break;
105                 }
106             }
107             if(keys[i] == "Type")
108             {
109                 switch(vals[i]){
110                     case "Flag":
111                         this.valueType = HeaderTypes.Flag;
112                         break;
113                     case "Integer":
114                         this.valueType = HeaderTypes.Integer;
115                         break;
116                     case "Float":
117                         this.valueType = HeaderTypes.Float;
118                         break;
119                     case "Character":
120                         this.valueType = HeaderTypes.Character;
121                         break;
122                     case "String":
123                         this.valueType = HeaderTypes.String;
124                         break;
125                     default:
126                         throw new Exception(vals[i]~" is not a know BCF Header Type");
127                 }
128             }
129             if(keys[i] == "IDX"){
130                 this.nkeys--;
131                 this.idx = this.vals[$-1].to!int;
132                 this.keys = this.keys[0..$-1];
133                 this.vals = this.vals[0..$-1];
134             }
135 
136         }
137     }
138 
139     /// set Record Type i.e INFO, FORMAT ...
140     void setHeaderRecordType(HeaderRecordType line)
141     {
142         this.recType = line;
143         this.key = HeaderRecordTypeStrings[line];
144     }
145 
146     /// get Record Type i.e INFO, FORMAT ...
147     HeaderRecordType getHeaderRecordType()
148     {
149         return this.recType;
150     }
151 
152     /// set Value Type length with integer
153     void setLength(T)(T number)
154     if(isIntegral!T)
155     {
156         this.lenthType = HeaderLengths.Fixed;
157         this["Number"] = number.to!string;
158     }
159 
160     /// set Value Type length i.e A, R, G, .
161     void setLength(HeaderLengths number)
162     {
163         this.lenthType = number;
164         this["Number"] = HeaderLengthsStrings[number];
165     }
166 
167     /// get Value Type length
168     string getLength()
169     {
170         return this["Number"];
171     }
172 
173     /// set Value Type i.e Integer, String, Float
174     void setValueType(HeaderTypes type)
175     {
176         this.valueType = type;
177         this["Type"] = HeaderTypesStrings[type];
178     }
179 
180     /// get Value Type i.e Integer, String, Float
181     HeaderTypes getValueType()
182     {
183         return this.valueType;
184     }
185 
186     /// set ID field
187     void setID(string id)
188     {
189         this["ID"] = id;
190     }
191 
192     /// get ID field
193     string getID()
194     {
195         return this["ID"];
196     }
197 
198     /// set Description field
199     void setDescription(string des)
200     {
201         this["Description"] = des;
202     }
203 
204     /// get Description field
205     string getDescription()
206     {
207         return this["Description"];
208     }
209 
210     /// get a value from the KV pairs
211     /// if key isn't present thows exception
212     ref auto opIndex(string index)
213     {
214         foreach (i, string key; keys)
215         {
216             if(key == index){
217                 return vals[i];
218             }
219         }
220         throw new Exception("Key " ~ index ~" not found");
221     }
222 
223     /// set a value from the KV pairs
224     /// if key isn't present a new KV pair is 
225     /// added
226     void opIndexAssign(string value, string index)
227     {
228         foreach (i, string key; keys)
229         {
230             if(key == index){
231                 vals[i] = value;
232                 return;
233             }
234         }
235         this.nkeys++;
236         keys~=index;
237         vals~=value;
238     }
239 
240     /// convert to bcf_hrec_t for use with htslib functions
241     bcf_hrec_t * convert(bcf_hdr_t * hdr)
242     {
243         if(this.recType == HeaderRecordType.Info || this.recType == HeaderRecordType.Format){
244             assert(this.valueType != HeaderTypes.None);
245             assert(this.lenthType != HeaderLengths.None);
246         }
247 
248         auto str = this.toString;
249         int parsed;
250         auto rec = bcf_hdr_parse_line(hdr, toUTFz!(char *)(str), &parsed);
251         rec.type = this.recType;
252         return rec;
253     }
254 
255     /// print a string representation of the header record
256     string toString()
257     {
258         string ret = "##" ~ this.key ~ "=" ~ this.value;
259         if(this.nkeys > 0){
260             ret ~= "<";
261             for(auto i =0; i < this.nkeys - 1; i++)
262             {
263                 ret ~= this.keys[i] ~ "=" ~ this.vals[i] ~ ", ";
264             }
265             ret ~= this.keys[$-1] ~ "=" ~ this.vals[$-1] ~ ">";    
266         }
267         
268         return ret;
269     }
270 }
271 
272 /** VCFHeader encapsulates `bcf_hdr_t*`
273     and provides convenience wrappers to manipulate the header metadata/records.
274 */
275 struct VCFHeader
276 {
277     /// Pointer to htslib BCF/VCF header struct; will be freed from VCFHeader dtor 
278     BcfHdr hdr;
279 
280     /// pointer ctor
281     this(bcf_hdr_t * h)
282     {
283         this.hdr = BcfHdr(h);
284     }
285 
286     /// copy this header
287     auto dup(){
288         return VCFHeader(bcf_hdr_dup(this.hdr));
289     }
290 
291     /// List of contigs in the header
292     @property string[] sequences()
293     {
294         import core.stdc.stdlib : free;
295         int nseqs;
296 
297         /** Creates a list of sequence names. It is up to the caller to free the list (but not the sequence names) */
298         //const(char) **bcf_hdr_seqnames(const(bcf_hdr_t) *h, int *nseqs);
299         const(char*)*ary = bcf_hdr_seqnames(this.hdr, &nseqs);
300         if (!nseqs) return [];
301 
302         string[] ret;
303         ret.reserve(nseqs);
304 
305         for(int i; i < nseqs; i++) {
306             ret ~= fromStringz(ary[i]).idup;
307         }
308 
309         free(cast(void*)ary);
310         return ret;        
311     }
312 
313     /// Number of samples in the header
314     pragma(inline, true)
315     @property int nsamples() { return bcf_hdr_nsamples(this.hdr); }
316 
317     int getSampleId(string sam){
318         auto ret = bcf_hdr_id2int(this.hdr, HeaderDictTypes.Sample, toUTFz!(char *)(sam));
319         if(ret == -1) hts_log_error(__FUNCTION__, "Couldn't find sample in header: " ~ sam);
320         return ret;
321     }
322 
323     // TODO
324     /// copy header lines from a template without overwiting existing lines
325     void copyHeaderLines(bcf_hdr_t *other)
326     {
327         assert(this.hdr != null);
328         assert(0);
329         //    bcf_hdr_t *bcf_hdr_merge(bcf_hdr_t *dst, const(bcf_hdr_t) *src);
330     }
331 
332     /// Add sample to this VCF
333     /// * int bcf_hdr_add_sample(bcf_hdr_t *hdr, const(char) *sample);
334     int addSample(string name)
335     in { assert(name != ""); }
336     do
337     {
338         assert(this.hdr != null);
339 
340         bcf_hdr_add_sample(this.hdr, toStringz(name));
341 
342         // AARRRRGGGHHHH
343         // https://github.com/samtools/htslib/issues/767
344         bcf_hdr_sync(this.hdr);
345 
346         return 0;
347     }
348 
349     /** VCF version, e.g. VCFv4.2 */
350     @property string vcfVersion() { return fromStringz( bcf_hdr_get_version(this.hdr) ).idup; }
351 
352     /// Add a new header line
353     int addHeaderLineKV(string key, string value)
354     {
355         // TODO check that key is not Info, FILTER, FORMAT (or contig?)
356         string line = format("##%s=%s", key, value);
357 
358         auto ret = bcf_hdr_append(this.hdr, toStringz(line));
359         if(ret < 0)
360             hts_log_error(__FUNCTION__, "Couldn't add header line with key=%s and value =%s".format(key, value));
361         auto notAdded = bcf_hdr_sync(this.hdr);
362         if(notAdded < 0)
363             hts_log_error(__FUNCTION__, "Couldn't add header line with key=%s and value =%s".format(key, value));
364         return ret;
365     }
366 
367     /// Add a new header line -- must be formatted ##key=value
368     int addHeaderLineRaw(string line)
369     {
370         assert(this.hdr != null);
371         //    int bcf_hdr_append(bcf_hdr_t *h, const(char) *line);
372         const auto ret = bcf_hdr_append(this.hdr, toStringz(line));
373         bcf_hdr_sync(this.hdr);
374         return ret;
375     }
376 
377     /// Add a new header line using HeaderRecord 
378     int addHeaderRecord(HeaderRecord rec)
379     {
380         assert(this.hdr != null);
381         auto ret = bcf_hdr_add_hrec(this.hdr, rec.convert(this.hdr));
382         if(ret < 0)
383             hts_log_error(__FUNCTION__, "Couldn't add HeaderRecord");
384         auto notAdded = bcf_hdr_sync(this.hdr);
385         if(notAdded != 0)
386             hts_log_error(__FUNCTION__, "Couldn't add HeaderRecord");
387         return ret;
388     }
389 
390     /// Remove all header lines of a particular type
391     void removeHeaderLines(HeaderRecordType linetype)
392     {
393         bcf_hdr_remove(this.hdr, linetype, null);
394         bcf_hdr_sync(this.hdr);
395     }
396 
397     /// Remove a header line of a particular type with the key
398     void removeHeaderLines(HeaderRecordType linetype, string key)
399     {
400         bcf_hdr_remove(this.hdr, linetype, toStringz(key));
401         bcf_hdr_sync(this.hdr);
402     }
403 
404     /// get a header record via ID field
405     HeaderRecord getHeaderRecord(HeaderRecordType linetype, string id)
406     {
407         return this.getHeaderRecord(linetype, "ID", id);
408     }
409 
410     /// get a header record via a string value pair
411     HeaderRecord getHeaderRecord(HeaderRecordType linetype, string key, string value)
412     {
413         auto rec = bcf_hdr_get_hrec(this.hdr, linetype, toUTFz!(const(char) *)(key),toUTFz!(const(char) *)(value), null);
414         if(!rec) throw new Exception("Record could not be found");
415         auto ret = HeaderRecord(rec);
416         // bcf_hrec_destroy(rec);
417         return ret;
418     }
419 
420     /// Add a filedate= headerline, which is not called out specifically in  the spec,
421     /// but appears in the spec's example files. We could consider allowing a param here.
422     int addFiledate()
423     {
424         return addHeaderLineKV("filedate", (cast(Date) Clock.currTime()).toISOString );
425     }
426     
427     /** Add INFO (§1.2.2) or FORMAT (§1.2.4) tag
428 
429     The INFO tag describes row-specific keys used in the INFO column;
430     The FORMAT tag describes sample-specific keys used in the last, and optional, genotype column.
431 
432     Template parameter: string; must be INFO or FORMAT
433 
434     The first four parameters are required; NUMBER and TYPE have specific allowable values.
435     source and version are optional, but recommended (for INFO only).
436 
437     *   id:     ID tag
438     *   number: NUMBER tag; here a string because it can also take special values {A,R,G,.} (see §1.2.2)
439     *   type:   Integer, Float, Flag, Character, and String
440     *   description: Text description; will be double quoted
441     *   source:      Annotation source  (eg dbSNP)
442     *   version:     Annotation version (eg 142)
443     */
444 
445     void addHeaderLine(HeaderRecordType lineType, T)(string id, T number, HeaderTypes type,
446                                     string description="",
447                                     string source="",
448                                     string _version="")
449     if((isIntegral!T || is(T == HeaderLengths)) && lineType != HeaderRecordType.None )       
450     {
451         HeaderRecord rec;
452         rec.setHeaderRecordType = lineType;
453         rec.setID(id);
454         rec.setLength(number);
455         rec.setValueType(type);
456         static if(lineType == HeaderRecordType.Info || lineType == HeaderRecordType.Filter || lineType == HeaderRecordType.FORMAT){
457             if(description == ""){
458                 throw new Exception("description cannot be empty for " ~ HeaderRecordTypeStrings[lineType]);    
459             }
460         }
461         rec.setDescription(description);
462         if(source != "")
463             rec["source"] = "\"%s\"".format(source);
464         if(_version != "")
465             rec["version"] = "\"%s\"".format(_version);
466 
467         this.addHeaderRecord(rec);
468     }
469 
470     /** Add FILTER tag (§1.2.3) */
471     void addHeaderLine(HeaderRecordType lineType)(string id, string description)
472     if(lineType == HeaderRecordType.Filter)
473     {
474         HeaderRecord rec;
475         rec.setHeaderRecordType = lineType;
476         rec.setID(id);
477         rec.setDescription("\"%s\"".format(description));
478 
479         this.addHeaderRecord(rec);
480     }
481 
482     /** Add FILTER tag (§1.2.3) */
483     deprecated void addFilter(string id, string description)
484     {
485         addHeaderLine!(HeaderRecordType.Filter)(id, description);
486     }
487 
488     /// string representation of header
489     string toString(){
490         import htslib.kstring;
491         kstring_t s;
492 
493         const int ret = bcf_hdr_format(this.hdr, 0, &s);
494         if (ret)
495         {
496             hts_log_error(__FUNCTION__,
497                 format("bcf_hdr_format returned nonzero (%d) (likely EINVAL, invalid bcf_hdr_t struct?)", ret));
498             return "[VCFHeader bcf_hdr_format parse_error]";
499         }
500 
501         return cast(string) s.s[0 .. s.l];
502     }
503 }
504 
505 ///
506 debug(dhtslib_unittest)
507 unittest
508 {
509     import std.exception: assertThrown;
510     import std.stdio: writeln, writefln;
511 
512     hts_set_log_level(htsLogLevel.HTS_LOG_TRACE);
513 
514 
515     auto hdr = VCFHeader(bcf_hdr_init("w\0"c.ptr));
516 
517     hdr.addHeaderLineRaw("##INFO=<ID=NS,Number=1,Type=Integer,Description=\"Number of Samples With Data\">");
518     hdr.addHeaderLineKV("INFO", "<ID=DP,Number=1,Type=Integer,Description=\"Total Depth\">");
519     // ##INFO=<ID=AF,Number=A,Type=Float,Description="Allele Frequency">
520     hdr.addHeaderLine!(HeaderRecordType.Info)("AF", HeaderLengths.OnePerAltAllele, HeaderTypes.Integer, "Number of Samples With Data");
521     hdr.addHeaderLineRaw("##contig=<ID=20,length=62435964,assembly=B36,md5=f126cdf8a6e0c7f379d618ff66beb2da,species=\"Homo sapiens\",taxonomy=x>"); // @suppress(dscanner.style.long_line)
522     hdr.addHeaderLineRaw("##FILTER=<ID=q10,Description=\"Quality below 10\">");
523     
524 
525     // Exercise header
526     assert(hdr.nsamples == 0);
527     hdr.addSample("NA12878");
528     assert(hdr.nsamples == 1);
529     assert(hdr.vcfVersion == "VCFv4.2");
530 }
531 
532 ///
533 debug(dhtslib_unittest)
534 unittest
535 {
536     import std.exception: assertThrown;
537     import std.stdio: writeln, writefln;
538 
539     hts_set_log_level(htsLogLevel.HTS_LOG_TRACE);
540 
541 
542     auto hdr = VCFHeader(bcf_hdr_init("w\0"c.ptr));
543 
544     hdr.addHeaderLineRaw("##INFO=<ID=NS,Number=1,Type=Integer,Description=\"Number of Samples With Data\">");
545     hdr.addHeaderLineKV("INFO", "<ID=DP,Number=1,Type=Integer,Description=\"Total Depth\">");
546 
547     auto rec = hdr.getHeaderRecord(HeaderRecordType.Info,"ID","NS");
548     assert(rec.recType == HeaderRecordType.Info);
549     assert(rec.key == "INFO");
550     assert(rec.nkeys == 4);
551     assert(rec.keys == ["ID", "Number", "Type", "Description"]);
552     assert(rec.vals == ["NS", "1", "Integer", "\"Number of Samples With Data\""]);
553     assert(rec["ID"] == "NS");
554 
555     assert(rec.idx == 1);
556 
557     writeln(rec.toString);
558 
559 
560     rec = HeaderRecord(rec.convert(hdr.hdr));
561 
562     assert(rec.recType == HeaderRecordType.Info);
563     assert(rec.key == "INFO");
564     assert(rec.nkeys == 4);
565     assert(rec.keys == ["ID", "Number", "Type", "Description"]);
566     assert(rec.vals == ["NS", "1", "Integer", "\"Number of Samples With Data\""]);
567     assert(rec["ID"] == "NS");
568     // assert(rec["IDX"] == "1");
569     // assert(rec.idx == 1);
570 
571     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"ID","NS");
572 
573     assert(rec.recType == HeaderRecordType.Info);
574     assert(rec.getLength == "1");
575     assert(rec.getValueType == HeaderTypes.Integer);
576     
577     rec.idx = -1;
578 
579     rec["ID"] = "NS2";
580 
581     hdr.addHeaderRecord(rec);
582     auto hdr2 = hdr.dup;
583     // writeln(hdr2.toString);
584 
585     rec = hdr2.getHeaderRecord(HeaderRecordType.Info,"ID","NS2");
586     assert(rec.recType == HeaderRecordType.Info);
587     assert(rec.key == "INFO");
588     assert(rec.nkeys == 4);
589     assert(rec.keys == ["ID", "Number", "Type", "Description"]);
590     assert(rec.vals == ["NS2", "1", "Integer", "\"Number of Samples With Data\""]);
591     assert(rec["ID"] == "NS2");
592 
593     assert(rec.idx == 3);
594 
595     rec = HeaderRecord.init;
596     rec.setHeaderRecordType(HeaderRecordType.Generic);
597     rec.key = "source";
598     rec.value = "hello";
599     hdr.addHeaderRecord(rec);
600 
601     rec = hdr.getHeaderRecord(HeaderRecordType.Generic,"source","hello");
602     assert(rec.recType == HeaderRecordType.Generic);
603     assert(rec.key == "source");
604     assert(rec.value == "hello");
605     assert(rec.nkeys == 0);
606 
607     hdr.addHeaderLine!(HeaderRecordType.Filter)("nonsense","filter");
608 
609     rec = hdr.getHeaderRecord(HeaderRecordType.Filter,"ID","nonsense");
610     assert(rec.recType == HeaderRecordType.Filter);
611     assert(rec.key == "FILTER");
612     assert(rec.value == "");
613     assert(rec.getID == "nonsense");
614     assert(rec.idx == 4);
615 
616     hdr.removeHeaderLines(HeaderRecordType.Filter);
617 
618     auto expected = "##fileformat=VCFv4.2\n" ~ 
619         "##INFO=<ID=NS,Number=1,Type=Integer,Description=\"Number of Samples With Data\">\n"~
620         "##INFO=<ID=DP,Number=1,Type=Integer,Description=\"Total Depth\">\n"~
621         "##INFO=<ID=NS2,Number=1,Type=Integer,Description=\"Number of Samples With Data\">\n"~
622         "##source=hello\n"~
623         "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\n";
624     assert(hdr.toString == expected);
625 
626     rec = rec.init;
627     rec.setHeaderRecordType(HeaderRecordType.Contig);
628     rec.setID("test");
629     rec["length"] = "5";
630 
631     hdr.addHeaderRecord(rec);
632 
633     assert(hdr.sequences == ["test"]);
634     hdr.removeHeaderLines(HeaderRecordType.Generic, "source");
635     hdr.addFilter("test","test");
636     expected = "##fileformat=VCFv4.2\n" ~ 
637         "##INFO=<ID=NS,Number=1,Type=Integer,Description=\"Number of Samples With Data\">\n"~
638         "##INFO=<ID=DP,Number=1,Type=Integer,Description=\"Total Depth\">\n"~
639         "##INFO=<ID=NS2,Number=1,Type=Integer,Description=\"Number of Samples With Data\">\n"~
640         "##contig=<ID=test,length=5>\n"~
641         "##FILTER=<ID=test,Description=\"test\">\n"~
642         "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\n";
643     assert(hdr.toString == expected);
644     rec = hdr.getHeaderRecord(HeaderRecordType.Filter,"test");
645     assert(rec.getDescription() == "\"test\"");
646 
647     rec = HeaderRecord.init;
648     rec.setHeaderRecordType(HeaderRecordType.Info);
649     rec.setID("test");
650     rec.setLength(HeaderLengths.OnePerGenotype);
651     rec.setValueType(HeaderTypes.Integer);
652     hdr.addHeaderRecord(rec);
653 
654     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"test");
655     assert(rec.recType == HeaderRecordType.Info);
656     assert(rec.getLength == "G");
657     assert(rec.getID == "test");
658     assert(rec.getValueType == HeaderTypes.Integer);
659 
660     rec = HeaderRecord.init;
661     rec.setHeaderRecordType(HeaderRecordType.Info);
662     rec.setID("test2");
663     rec.setLength(HeaderLengths.OnePerAllele);
664     rec.setValueType(HeaderTypes.Integer);
665     hdr.addHeaderRecord(rec);
666 
667     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"test2");
668     assert(rec.recType == HeaderRecordType.Info);
669     assert(rec.getLength == "R");
670     assert(rec.getID == "test2");
671     assert(rec.getValueType == HeaderTypes.Integer);
672 
673     rec = HeaderRecord.init;
674     rec.setHeaderRecordType(HeaderRecordType.Info);
675     rec.setID("test3");
676     rec.setLength(HeaderLengths.Variable);
677     rec.setValueType(HeaderTypes.Integer);
678     hdr.addHeaderRecord(rec);
679 
680     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"test3");
681     assert(rec.recType == HeaderRecordType.Info);
682     assert(rec.getLength == ".");
683     assert(rec.getID == "test3");
684     assert(rec.getValueType == HeaderTypes.Integer);
685 
686     rec = HeaderRecord.init;
687     rec.setHeaderRecordType(HeaderRecordType.Info);
688     rec.setID("test4");
689     rec.setLength(1);
690     rec.setValueType(HeaderTypes.Flag);
691     hdr.addHeaderRecord(rec);
692 
693     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"test4");
694     assert(rec.recType == HeaderRecordType.Info);
695     assert(rec.getID == "test4");
696     assert(rec.getValueType == HeaderTypes.Flag);
697 
698     rec = HeaderRecord.init;
699     rec.setHeaderRecordType(HeaderRecordType.Info);
700     rec.setID("test5");
701     rec.setLength(1);
702     rec.setValueType(HeaderTypes.Character);
703     hdr.addHeaderRecord(rec);
704 
705     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"test5");
706     assert(rec.recType == HeaderRecordType.Info);
707     assert(rec.getLength == "1");
708     assert(rec.getID == "test5");
709     assert(rec.getValueType == HeaderTypes.Character);
710 
711     rec = HeaderRecord.init;
712     rec.setHeaderRecordType(HeaderRecordType.Info);
713     rec.setID("test6");
714     rec.setLength(HeaderLengths.Variable);
715     rec.setValueType(HeaderTypes.String);
716     hdr.addHeaderRecord(rec);
717 
718     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"test6");
719     assert(rec.recType == HeaderRecordType.Info);
720     assert(rec.getLength == ".");
721     assert(rec.getID == "test6");
722     assert(rec.getValueType == HeaderTypes.String);
723 
724     expected = "##fileformat=VCFv4.2\n" ~ 
725         "##INFO=<ID=NS,Number=1,Type=Integer,Description=\"Number of Samples With Data\">\n"~
726         "##INFO=<ID=DP,Number=1,Type=Integer,Description=\"Total Depth\">\n"~
727         "##INFO=<ID=NS2,Number=1,Type=Integer,Description=\"Number of Samples With Data\">\n"~
728         "##contig=<ID=test,length=5>\n"~
729         "##FILTER=<ID=test,Description=\"test\">\n"~
730         "##INFO=<ID=test,Number=G,Type=Integer>\n"~
731         "##INFO=<ID=test2,Number=R,Type=Integer>\n"~
732         "##INFO=<ID=test3,Number=.,Type=Integer>\n"~
733         "##INFO=<ID=test4,Number=1,Type=Flag>\n"~
734         "##INFO=<ID=test5,Number=1,Type=Character>\n"~
735         "##INFO=<ID=test6,Number=.,Type=String>\n"~
736         "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\n";
737     writeln(hdr.toString);
738     assert(hdr.toString == expected);
739 
740 }