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     /// Explicit postblit to avoid 
287     /// https://github.com/blachlylab/dhtslib/issues/122
288     this(this)
289     {
290         this.hdr = hdr;
291     }
292 
293     /// copy this header
294     auto dup(){
295         return VCFHeader(bcf_hdr_dup(this.hdr));
296     }
297 
298     /// List of contigs in the header
299     @property string[] sequences()
300     {
301         import core.stdc.stdlib : free;
302         int nseqs;
303 
304         /** Creates a list of sequence names. It is up to the caller to free the list (but not the sequence names) */
305         //const(char) **bcf_hdr_seqnames(const(bcf_hdr_t) *h, int *nseqs);
306         const(char*)*ary = bcf_hdr_seqnames(this.hdr, &nseqs);
307         if (!nseqs) return [];
308 
309         string[] ret;
310         ret.reserve(nseqs);
311 
312         for(int i; i < nseqs; i++) {
313             ret ~= fromStringz(ary[i]).idup;
314         }
315 
316         free(cast(void*)ary);
317         return ret;        
318     }
319 
320     /// Number of samples in the header
321     pragma(inline, true)
322     @property int nsamples() { return bcf_hdr_nsamples(this.hdr); }
323 
324     /// get int index of sample name
325     int getSampleId(string sam){
326         auto ret = bcf_hdr_id2int(this.hdr, HeaderDictTypes.Sample, toUTFz!(char *)(sam));
327         if(ret == -1) hts_log_error(__FUNCTION__, "Couldn't find sample in header: " ~ sam);
328         return ret;
329     }
330 
331     /// get sample list
332     string[] getSamples(){
333         auto samples = this.hdr.samples[0..this.nsamples];
334         return samples.map!(x => fromStringz(x).idup).array;
335     }
336 
337     // TODO
338     /// copy header lines from a template without overwiting existing lines
339     void copyHeaderLines(bcf_hdr_t *other)
340     {
341         assert(this.hdr != null);
342         assert(0);
343         //    bcf_hdr_t *bcf_hdr_merge(bcf_hdr_t *dst, const(bcf_hdr_t) *src);
344     }
345 
346     /// Add sample to this VCF
347     /// * int bcf_hdr_add_sample(bcf_hdr_t *hdr, const(char) *sample);
348     int addSample(string name)
349     in { assert(name != ""); }
350     do
351     {
352         assert(this.hdr != null);
353 
354         bcf_hdr_add_sample(this.hdr, toStringz(name));
355 
356         // AARRRRGGGHHHH
357         // https://github.com/samtools/htslib/issues/767
358         bcf_hdr_sync(this.hdr);
359 
360         return 0;
361     }
362 
363     /** VCF version, e.g. VCFv4.2 */
364     @property string vcfVersion() { return fromStringz( bcf_hdr_get_version(this.hdr) ).idup; }
365 
366     /// Add a new header line
367     int addHeaderLineKV(string key, string value)
368     {
369         // TODO check that key is not Info, FILTER, FORMAT (or contig?)
370         string line = format("##%s=%s", key, value);
371 
372         auto ret = bcf_hdr_append(this.hdr, toStringz(line));
373         if(ret < 0)
374             hts_log_error(__FUNCTION__, "Couldn't add header line with key=%s and value =%s".format(key, value));
375         auto notAdded = bcf_hdr_sync(this.hdr);
376         if(notAdded < 0)
377             hts_log_error(__FUNCTION__, "Couldn't add header line with key=%s and value =%s".format(key, value));
378         return ret;
379     }
380 
381     /// Add a new header line -- must be formatted ##key=value
382     int addHeaderLineRaw(string line)
383     {
384         assert(this.hdr != null);
385         //    int bcf_hdr_append(bcf_hdr_t *h, const(char) *line);
386         const auto ret = bcf_hdr_append(this.hdr, toStringz(line));
387         bcf_hdr_sync(this.hdr);
388         return ret;
389     }
390 
391     /// Add a new header line using HeaderRecord 
392     int addHeaderRecord(HeaderRecord rec)
393     {
394         assert(this.hdr != null);
395         auto ret = bcf_hdr_add_hrec(this.hdr, rec.convert(this.hdr));
396         if(ret < 0)
397             hts_log_error(__FUNCTION__, "Couldn't add HeaderRecord");
398         auto notAdded = bcf_hdr_sync(this.hdr);
399         if(notAdded != 0)
400             hts_log_error(__FUNCTION__, "Couldn't add HeaderRecord");
401         return ret;
402     }
403 
404     /// Remove all header lines of a particular type
405     void removeHeaderLines(HeaderRecordType linetype)
406     {
407         bcf_hdr_remove(this.hdr, linetype, null);
408         bcf_hdr_sync(this.hdr);
409     }
410 
411     /// Remove a header line of a particular type with the key
412     void removeHeaderLines(HeaderRecordType linetype, string key)
413     {
414         bcf_hdr_remove(this.hdr, linetype, toStringz(key));
415         bcf_hdr_sync(this.hdr);
416     }
417 
418     /// get a header record via ID field
419     HeaderRecord getHeaderRecord(HeaderRecordType linetype, string id)
420     {
421         return this.getHeaderRecord(linetype, "ID", id);
422     }
423 
424     /// get a header record via a string value pair
425     HeaderRecord getHeaderRecord(HeaderRecordType linetype, string key, string value)
426     {
427         auto rec = bcf_hdr_get_hrec(this.hdr, linetype, toUTFz!(const(char) *)(key),toUTFz!(const(char) *)(value), null);
428         if(!rec) throw new Exception("Record could not be found");
429         auto ret = HeaderRecord(rec);
430         // bcf_hrec_destroy(rec);
431         return ret;
432     }
433 
434     /// Add a filedate= headerline, which is not called out specifically in  the spec,
435     /// but appears in the spec's example files. We could consider allowing a param here.
436     int addFiledate()
437     {
438         return addHeaderLineKV("filedate", (cast(Date) Clock.currTime()).toISOString );
439     }
440     
441     /** Add INFO (§1.2.2) or FORMAT (§1.2.4) tag
442 
443     The INFO tag describes row-specific keys used in the INFO column;
444     The FORMAT tag describes sample-specific keys used in the last, and optional, genotype column.
445 
446     Template parameter: string; must be INFO or FORMAT
447 
448     The first four parameters are required; NUMBER and TYPE have specific allowable values.
449     source and version are optional, but recommended (for INFO only).
450 
451     *   id:     ID tag
452     *   number: NUMBER tag; here a string because it can also take special values {A,R,G,.} (see §1.2.2)
453     *   type:   Integer, Float, Flag, Character, and String
454     *   description: Text description; will be double quoted
455     *   source:      Annotation source  (eg dbSNP)
456     *   version:     Annotation version (eg 142)
457     */
458 
459     void addHeaderLine(HeaderRecordType lineType, T)(string id, T number, HeaderTypes type,
460                                     string description="",
461                                     string source="",
462                                     string _version="")
463     if((isIntegral!T || is(T == HeaderLengths)) && lineType != HeaderRecordType.None )       
464     {
465         HeaderRecord rec;
466         rec.setHeaderRecordType = lineType;
467         rec.setID(id);
468         rec.setLength(number);
469         rec.setValueType(type);
470         static if(lineType == HeaderRecordType.Info || lineType == HeaderRecordType.Filter || lineType == HeaderRecordType.FORMAT){
471             if(description == ""){
472                 throw new Exception("description cannot be empty for " ~ HeaderRecordTypeStrings[lineType]);    
473             }
474         }
475         rec.setDescription(description);
476         if(source != "")
477             rec["source"] = "\"%s\"".format(source);
478         if(_version != "")
479             rec["version"] = "\"%s\"".format(_version);
480 
481         this.addHeaderRecord(rec);
482     }
483 
484     /** Add FILTER tag (§1.2.3) */
485     void addHeaderLine(HeaderRecordType lineType)(string id, string description)
486     if(lineType == HeaderRecordType.Filter)
487     {
488         HeaderRecord rec;
489         rec.setHeaderRecordType = lineType;
490         rec.setID(id);
491         rec.setDescription("\"%s\"".format(description));
492 
493         this.addHeaderRecord(rec);
494     }
495 
496     /** Add FILTER tag (§1.2.3) */
497     deprecated void addFilter(string id, string description)
498     {
499         addHeaderLine!(HeaderRecordType.Filter)(id, description);
500     }
501 
502     /// string representation of header
503     string toString(){
504         import htslib.kstring;
505         kstring_t s;
506 
507         const int ret = bcf_hdr_format(this.hdr, 0, &s);
508         if (ret)
509         {
510             hts_log_error(__FUNCTION__,
511                 format("bcf_hdr_format returned nonzero (%d) (likely EINVAL, invalid bcf_hdr_t struct?)", ret));
512             return "[VCFHeader bcf_hdr_format parse_error]";
513         }
514 
515         return cast(string) s.s[0 .. s.l];
516     }
517 }
518 
519 ///
520 debug(dhtslib_unittest)
521 unittest
522 {
523     import std.exception: assertThrown;
524     import std.stdio: writeln, writefln;
525 
526     hts_set_log_level(htsLogLevel.HTS_LOG_TRACE);
527 
528 
529     auto hdr = VCFHeader(bcf_hdr_init("w\0"c.ptr));
530 
531     hdr.addHeaderLineRaw("##INFO=<ID=NS,Number=1,Type=Integer,Description=\"Number of Samples With Data\">");
532     hdr.addHeaderLineKV("INFO", "<ID=DP,Number=1,Type=Integer,Description=\"Total Depth\">");
533     // ##INFO=<ID=AF,Number=A,Type=Float,Description="Allele Frequency">
534     hdr.addHeaderLine!(HeaderRecordType.Info)("AF", HeaderLengths.OnePerAltAllele, HeaderTypes.Integer, "Number of Samples With Data");
535     hdr.addHeaderLineRaw("##contig=<ID=20,length=62435964,assembly=B36,md5=f126cdf8a6e0c7f379d618ff66beb2da,species=\"Homo sapiens\",taxonomy=x>"); // @suppress(dscanner.style.long_line)
536     hdr.addHeaderLineRaw("##FILTER=<ID=q10,Description=\"Quality below 10\">");
537     
538 
539     // Exercise header
540     assert(hdr.nsamples == 0);
541     hdr.addSample("NA12878");
542     assert(hdr.nsamples == 1);
543     assert(hdr.vcfVersion == "VCFv4.2");
544 }
545 
546 ///
547 debug(dhtslib_unittest)
548 unittest
549 {
550     import std.exception: assertThrown;
551     import std.stdio: writeln, writefln;
552 
553     hts_set_log_level(htsLogLevel.HTS_LOG_TRACE);
554 
555 
556     auto hdr = VCFHeader(bcf_hdr_init("w\0"c.ptr));
557 
558     hdr.addHeaderLineRaw("##INFO=<ID=NS,Number=1,Type=Integer,Description=\"Number of Samples With Data\">");
559     hdr.addHeaderLineKV("INFO", "<ID=DP,Number=1,Type=Integer,Description=\"Total Depth\">");
560 
561     auto rec = hdr.getHeaderRecord(HeaderRecordType.Info,"ID","NS");
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 
569     assert(rec.idx == 1);
570 
571     writeln(rec.toString);
572 
573 
574     rec = HeaderRecord(rec.convert(hdr.hdr));
575 
576     assert(rec.recType == HeaderRecordType.Info);
577     assert(rec.key == "INFO");
578     assert(rec.nkeys == 4);
579     assert(rec.keys == ["ID", "Number", "Type", "Description"]);
580     assert(rec.vals == ["NS", "1", "Integer", "\"Number of Samples With Data\""]);
581     assert(rec["ID"] == "NS");
582     // assert(rec["IDX"] == "1");
583     // assert(rec.idx == 1);
584 
585     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"ID","NS");
586 
587     assert(rec.recType == HeaderRecordType.Info);
588     assert(rec.getLength == "1");
589     assert(rec.getValueType == HeaderTypes.Integer);
590     
591     rec.idx = -1;
592 
593     rec["ID"] = "NS2";
594 
595     hdr.addHeaderRecord(rec);
596     auto hdr2 = hdr.dup;
597     // writeln(hdr2.toString);
598 
599     rec = hdr2.getHeaderRecord(HeaderRecordType.Info,"ID","NS2");
600     assert(rec.recType == HeaderRecordType.Info);
601     assert(rec.key == "INFO");
602     assert(rec.nkeys == 4);
603     assert(rec.keys == ["ID", "Number", "Type", "Description"]);
604     assert(rec.vals == ["NS2", "1", "Integer", "\"Number of Samples With Data\""]);
605     assert(rec["ID"] == "NS2");
606 
607     assert(rec.idx == 3);
608 
609     rec = HeaderRecord.init;
610     rec.setHeaderRecordType(HeaderRecordType.Generic);
611     rec.key = "source";
612     rec.value = "hello";
613     hdr.addHeaderRecord(rec);
614 
615     rec = hdr.getHeaderRecord(HeaderRecordType.Generic,"source","hello");
616     assert(rec.recType == HeaderRecordType.Generic);
617     assert(rec.key == "source");
618     assert(rec.value == "hello");
619     assert(rec.nkeys == 0);
620 
621     hdr.addHeaderLine!(HeaderRecordType.Filter)("nonsense","filter");
622 
623     rec = hdr.getHeaderRecord(HeaderRecordType.Filter,"ID","nonsense");
624     assert(rec.recType == HeaderRecordType.Filter);
625     assert(rec.key == "FILTER");
626     assert(rec.value == "");
627     assert(rec.getID == "nonsense");
628     assert(rec.idx == 4);
629 
630     hdr.removeHeaderLines(HeaderRecordType.Filter);
631 
632     auto expected = "##fileformat=VCFv4.2\n" ~ 
633         "##INFO=<ID=NS,Number=1,Type=Integer,Description=\"Number of Samples With Data\">\n"~
634         "##INFO=<ID=DP,Number=1,Type=Integer,Description=\"Total Depth\">\n"~
635         "##INFO=<ID=NS2,Number=1,Type=Integer,Description=\"Number of Samples With Data\">\n"~
636         "##source=hello\n"~
637         "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\n";
638     assert(hdr.toString == expected);
639 
640     rec = rec.init;
641     rec.setHeaderRecordType(HeaderRecordType.Contig);
642     rec.setID("test");
643     rec["length"] = "5";
644 
645     hdr.addHeaderRecord(rec);
646 
647     assert(hdr.sequences == ["test"]);
648     hdr.removeHeaderLines(HeaderRecordType.Generic, "source");
649     hdr.addFilter("test","test");
650     expected = "##fileformat=VCFv4.2\n" ~ 
651         "##INFO=<ID=NS,Number=1,Type=Integer,Description=\"Number of Samples With Data\">\n"~
652         "##INFO=<ID=DP,Number=1,Type=Integer,Description=\"Total Depth\">\n"~
653         "##INFO=<ID=NS2,Number=1,Type=Integer,Description=\"Number of Samples With Data\">\n"~
654         "##contig=<ID=test,length=5>\n"~
655         "##FILTER=<ID=test,Description=\"test\">\n"~
656         "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\n";
657     assert(hdr.toString == expected);
658     rec = hdr.getHeaderRecord(HeaderRecordType.Filter,"test");
659     assert(rec.getDescription() == "\"test\"");
660 
661     rec = HeaderRecord.init;
662     rec.setHeaderRecordType(HeaderRecordType.Info);
663     rec.setID("test");
664     rec.setLength(HeaderLengths.OnePerGenotype);
665     rec.setValueType(HeaderTypes.Integer);
666     hdr.addHeaderRecord(rec);
667 
668     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"test");
669     assert(rec.recType == HeaderRecordType.Info);
670     assert(rec.getLength == "G");
671     assert(rec.getID == "test");
672     assert(rec.getValueType == HeaderTypes.Integer);
673 
674     rec = HeaderRecord.init;
675     rec.setHeaderRecordType(HeaderRecordType.Info);
676     rec.setID("test2");
677     rec.setLength(HeaderLengths.OnePerAllele);
678     rec.setValueType(HeaderTypes.Integer);
679     hdr.addHeaderRecord(rec);
680 
681     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"test2");
682     assert(rec.recType == HeaderRecordType.Info);
683     assert(rec.getLength == "R");
684     assert(rec.getID == "test2");
685     assert(rec.getValueType == HeaderTypes.Integer);
686 
687     rec = HeaderRecord.init;
688     rec.setHeaderRecordType(HeaderRecordType.Info);
689     rec.setID("test3");
690     rec.setLength(HeaderLengths.Variable);
691     rec.setValueType(HeaderTypes.Integer);
692     hdr.addHeaderRecord(rec);
693 
694     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"test3");
695     assert(rec.recType == HeaderRecordType.Info);
696     assert(rec.getLength == ".");
697     assert(rec.getID == "test3");
698     assert(rec.getValueType == HeaderTypes.Integer);
699 
700     rec = HeaderRecord.init;
701     rec.setHeaderRecordType(HeaderRecordType.Info);
702     rec.setID("test4");
703     rec.setLength(1);
704     rec.setValueType(HeaderTypes.Flag);
705     hdr.addHeaderRecord(rec);
706 
707     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"test4");
708     assert(rec.recType == HeaderRecordType.Info);
709     assert(rec.getID == "test4");
710     assert(rec.getValueType == HeaderTypes.Flag);
711 
712     rec = HeaderRecord.init;
713     rec.setHeaderRecordType(HeaderRecordType.Info);
714     rec.setID("test5");
715     rec.setLength(1);
716     rec.setValueType(HeaderTypes.Character);
717     hdr.addHeaderRecord(rec);
718 
719     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"test5");
720     assert(rec.recType == HeaderRecordType.Info);
721     assert(rec.getLength == "1");
722     assert(rec.getID == "test5");
723     assert(rec.getValueType == HeaderTypes.Character);
724 
725     rec = HeaderRecord.init;
726     rec.setHeaderRecordType(HeaderRecordType.Info);
727     rec.setID("test6");
728     rec.setLength(HeaderLengths.Variable);
729     rec.setValueType(HeaderTypes.String);
730     hdr.addHeaderRecord(rec);
731 
732     rec = hdr.getHeaderRecord(HeaderRecordType.Info,"test6");
733     assert(rec.recType == HeaderRecordType.Info);
734     assert(rec.getLength == ".");
735     assert(rec.getID == "test6");
736     assert(rec.getValueType == HeaderTypes.String);
737 
738     expected = "##fileformat=VCFv4.2\n" ~ 
739         "##INFO=<ID=NS,Number=1,Type=Integer,Description=\"Number of Samples With Data\">\n"~
740         "##INFO=<ID=DP,Number=1,Type=Integer,Description=\"Total Depth\">\n"~
741         "##INFO=<ID=NS2,Number=1,Type=Integer,Description=\"Number of Samples With Data\">\n"~
742         "##contig=<ID=test,length=5>\n"~
743         "##FILTER=<ID=test,Description=\"test\">\n"~
744         "##INFO=<ID=test,Number=G,Type=Integer>\n"~
745         "##INFO=<ID=test2,Number=R,Type=Integer>\n"~
746         "##INFO=<ID=test3,Number=.,Type=Integer>\n"~
747         "##INFO=<ID=test4,Number=1,Type=Flag>\n"~
748         "##INFO=<ID=test5,Number=1,Type=Character>\n"~
749         "##INFO=<ID=test6,Number=.,Type=String>\n"~
750         "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\n";
751     writeln(hdr.toString);
752     assert(hdr.toString == expected);
753 
754 }