htsvcf_napi/
lib.rs

1//! Node.js bindings for VCF/BCF file access via HTSlib.
2//!
3//! This crate provides Node-API (N-API) bindings for reading VCF/BCF files,
4//! enabling high-performance genomic data processing from JavaScript/TypeScript.
5//!
6//! # Installation
7//!
8//! ```bash
9//! npm install htsvcf
10//! ```
11//!
12//! # Quick Start
13//!
14//! ```js
15//! import { openReader } from 'htsvcf'
16//!
17//! const reader = await openReader('input.vcf.gz')
18//!
19//! // Iterate all variants
20//! while (true) {
21//!     const { done, value: variant } = await reader.next()
22//!     if (done) break
23//!
24//!     console.log(`${variant.chrom}:${variant.pos} ${variant.ref} -> ${variant.alt}`)
25//! }
26//!
27//! reader.close()
28//! ```
29//!
30//! # API Overview
31//!
32//! ## Opening files
33//!
34//! ```js
35//! // Async (recommended)
36//! const reader = await openReader('input.vcf.gz')
37//!
38//! // Sync constructor
39//! const reader = new Reader('input.vcf.gz')
40//! ```
41//!
42//! ## Iterating variants
43//!
44//! ```js
45//! // Async iteration (recommended)
46//! while (true) {
47//!     const { done, value } = await reader.next()
48//!     if (done) break
49//!     // process value (Variant)
50//! }
51//!
52//! // Sync iteration
53//! while (true) {
54//!     const { done, value } = reader.nextSync()
55//!     if (done) break
56//!     // process value
57//! }
58//! ```
59//!
60//! ## Querying regions (requires index)
61//!
62//! ```js
63//! if (reader.hasIndex()) {
64//!     // Query a region (0-based coordinates)
65//!     await reader.query('chr1', 1000, 2000)
66//!
67//!     // Or use region string (1-based, like samtools)
68//!     await reader.query('chr1:1001-2000')
69//!
70//!     // Then iterate as normal
71//!     while (true) {
72//!         const { done, value } = await reader.next()
73//!         if (done) break
74//!         // variants overlapping the region
75//!     }
76//! }
77//! ```
78//!
79//! ## Variant fields
80//!
81//! ```js
82//! const v = variant
83//!
84//! // Basic fields (read-only)
85//! v.chrom      // "chr1"
86//! v.pos        // 12345 (1-based)
87//! v.start      // 12344 (0-based)
88//! v.stop       // 12345 (end position)
89//! v.ref        // "A"
90//! v.alt        // ["G", "T"]
91//! v.rid        // Reference ID (integer) or undefined
92//!
93//! // Read/write fields
94//! v.id         // "rs12345" or "."
95//! v.id = "rs999"
96//!
97//! v.qual       // 30.5 or null if missing
98//! v.qual = 42.0
99//! v.qual = null  // Set to missing
100//!
101//! v.filter     // ["PASS"] or ["q10", "dp"]
102//! v.filter = ["PASS"]
103//! ```
104//!
105//! ## INFO fields
106//!
107//! ```js
108//! // Read INFO (returns typed values based on header)
109//! v.info('DP')         // 42 (Integer)
110//! v.info('AF')         // [0.25, 0.75] (Float array)
111//! v.info('SOMATIC')    // true (Flag)
112//! v.info('GENE')       // "BRCA1" (String)
113//! v.info('MISSING')    // undefined (not present)
114//!
115//! // Write INFO (type must match header definition)
116//! v.set_info('DP', 100)
117//! v.set_info('AF', [0.1, 0.9])
118//! v.set_info('SOMATIC', true)
119//! v.set_info('GENE', 'TP53')
120//! v.set_info('DP', null)  // Clear/remove the field
121//! ```
122//!
123//! ## FORMAT fields (per-sample)
124//!
125//! ```js
126//! // Get FORMAT values (array with one entry per sample)
127//! v.format('GT')  // ["0/1", "0/0", "1/1"]
128//! v.format('DP')  // [30, 25, null]  (null = missing)
129//! v.format('AD')  // [[10, 20], [25, 0], [0, 30]]
130//!
131//! // Set FORMAT values (array with one entry per sample)
132//! v.set_format('DP', [40, 35, 50])
133//! v.set_format('AD', [[15, 25], [30, 5], [5, 35]])
134//! v.set_format('DP', null)  // Clear the field
135//!
136//! // Get all FORMAT fields for one sample by name
137//! const s = v.sample('NA12878')
138//! s.GT          // "0/1"
139//! s.DP          // 30
140//! s.AD          // [10, 20]
141//! s.sample_name // "NA12878"
142//! s.genotype    // { alleles: [0, 1], phase: [false] }
143//!
144//! // Get all samples at once (more efficient for bulk access)
145//! const all = v.samples()  // Array of sample objects
146//! all[0].GT     // First sample's genotype
147//! all[0].sample_name  // First sample's name
148//!
149//! // Get a subset of samples
150//! const subset = v.samples(['NA12878', 'NA12879'])
151//!
152//! // Get parsed genotypes (alleles and phase info)
153//! const gts = v.genotypes()
154//! // [{ alleles: [0, 1], phase: [false] }, { alleles: [1, 1], phase: [true] }, ...]
155//!
156//! // Genotypes for a subset of samples
157//! const gtSubset = v.genotypes(['NA12878'])
158//! ```
159//!
160//! ## Output
161//!
162//! ```js
163//! // Convert to VCF line (without trailing newline)
164//! v.toString()  // "chr1\t12345\trs12345\tA\tG\t30\tPASS\tDP=42\t..."
165//! ```
166//!
167//! ## Header access
168//!
169//! ```js
170//! const header = reader.header
171//!
172//! // List sample names
173//! header.samples()  // ["NA12878", "NA12879", ...]
174//!
175//! // Get field definitions
176//! header.get('INFO', 'DP')
177//! // { id: 'DP', type: 'Integer', number: '1', description: 'Read depth' }
178//!
179//! header.get('FORMAT', 'GT')
180//! // { id: 'GT', type: 'String', number: '1', description: 'Genotype' }
181//!
182//! // List all header records
183//! header.records()
184//! // [{ section: 'INFO', id: 'DP', number: '1', type: 'Integer', ... }, ...]
185//!
186//! // Add new field definitions
187//! header.addInfo('CUSTOM', '1', 'Integer', 'My custom annotation')
188//! header.addFormat('SCORE', '1', 'Float', 'Per-sample score')
189//!
190//! // Get full header text
191//! header.toString()
192//! ```
193//!
194//! # TypeScript
195//!
196//! Full TypeScript definitions are included. Key types:
197//!
198//! ```typescript
199//! import { Reader, Variant, Header, openReader } from 'htsvcf'
200//!
201//! const reader: Reader = await openReader('input.vcf.gz')
202//! const header: Header = reader.header
203//!
204//! const { value: variant }: { done: boolean; value: Variant } = await reader.next()
205//! ```
206
207use std::sync::{Arc, Mutex};
208
209mod format;
210
211const DEFAULT_BATCH_SIZE: usize = 32;
212
213use htsvcf_core as core;
214use htsvcf_core::variant::FormatValue;
215use napi::bindgen_prelude::*;
216use napi::{sys, Env};
217use napi_derive::napi;
218
219/// Represents a parsed genotype for a single sample.
220///
221/// The `alleles` array contains allele indices (0 = REF, 1+ = ALT),
222/// with `null` representing missing alleles (`.` in VCF notation).
223///
224/// The `phase` array indicates the phase separator between consecutive alleles:
225/// - `false` = unphased (`/`)
226/// - `true` = phased (`|`)
227#[napi(object)]
228pub struct Genotype {
229    /// Allele indices. `null` represents a missing allele (`.`).
230    pub alleles: Vec<Option<i32>>,
231    /// Phase separators. `phase[i]` indicates whether `alleles[i+1]` is phased
232    /// with `alleles[i]` (`true` = `|`, `false` = `/`).
233    pub phase: Vec<bool>,
234}
235
236impl From<core::Genotype> for Genotype {
237    fn from(g: core::Genotype) -> Self {
238        Genotype {
239            alleles: g.alleles,
240            phase: g.phase,
241        }
242    }
243}
244
245impl From<Genotype> for core::Genotype {
246    fn from(g: Genotype) -> Self {
247        core::Genotype {
248            alleles: g.alleles,
249            phase: g.phase,
250        }
251    }
252}
253
254#[napi(object)]
255pub struct ReaderOptions {}
256
257#[napi(object)]
258pub struct WriterOptions {
259    pub format: Option<String>,
260    pub uncompressed: Option<bool>,
261    pub threads: Option<u32>,
262}
263
264#[napi]
265pub struct Reader {
266    inner: Arc<Mutex<Option<core::Reader>>>,
267    header: Arc<core::Header>,
268    /// Stored N-API reference so that `reader.header` always returns the same JS object.
269    /// Without this, each call to the getter would create a new JS wrapper, breaking
270    /// identity checks (`reader.header === reader.header`) and allowing mutations to
271    /// be lost if the user modifies one instance but reads from another.
272    ///
273    /// TODO: If we make Header immutable (mutations return a new Header), we could
274    /// remove this field and create a fresh wrapper on each access.
275    header_ref: Reference<Header>,
276}
277
278#[napi]
279impl Reader {
280    #[napi(constructor)]
281    pub fn new(env: Env, path: String, _opts: Option<ReaderOptions>) -> napi::Result<Self> {
282        let reader = core::open_reader(&path).map_err(|e| {
283            Error::new(
284                Status::GenericFailure,
285                format!("failed to open {path}: {e}"),
286            )
287        })?;
288
289        let header = Arc::new(unsafe { core::Header::new(reader.header_ptr()) });
290        let header_ref = Header::into_reference(
291            Header {
292                inner: header.clone(),
293            },
294            env,
295        )?;
296
297        Ok(Self {
298            inner: Arc::new(Mutex::new(Some(reader))),
299            header,
300            header_ref,
301        })
302    }
303
304    #[napi(getter)]
305    pub fn header(&self, env: Env) -> napi::Result<Reference<Header>> {
306        self.header_ref.clone(env)
307    }
308
309    #[napi(js_name = "hasIndex")]
310    pub fn has_index(&self) -> bool {
311        self.inner
312            .lock()
313            .ok()
314            .and_then(|g| g.as_ref().map(|r| r.has_index()))
315            .unwrap_or(false)
316    }
317
318    #[napi]
319    pub fn query(
320        &self,
321        region_or_chrom: String,
322        start0: Option<u32>,
323        end0: Option<u32>,
324    ) -> AsyncTask<QueryTask> {
325        AsyncTask::new(QueryTask {
326            inner: self.inner.clone(),
327            region_or_chrom,
328            start0,
329            end0,
330        })
331    }
332
333    #[napi]
334    pub fn next(&self) -> AsyncTask<NextTask> {
335        AsyncTask::new(NextTask {
336            inner: self.inner.clone(),
337            header: self.header.clone(),
338        })
339    }
340
341    /// Read the next batch of variants asynchronously.
342    /// Returns an array of Variant objects. Empty array means EOF.
343    /// More efficient than next() for bulk iteration as it avoids
344    /// creating {done, value} wrapper objects for each variant.
345    #[napi(js_name = "nextBatchAsync")]
346    pub fn next_batch_async(&self, size: Option<u32>) -> napi::Result<AsyncTask<NextBatchTask>> {
347        let batch_size = size.map(|s| s as usize).unwrap_or(DEFAULT_BATCH_SIZE);
348        if batch_size == 0 || batch_size > 16384 {
349            return Err(Error::new(
350                Status::InvalidArg,
351                format!("batch size must be between 1 and 16384, got {}", batch_size),
352            ));
353        }
354
355        Ok(AsyncTask::new(NextBatchTask {
356            inner: self.inner.clone(),
357            header: self.header.clone(),
358            size: batch_size,
359        }))
360    }
361
362    #[napi(js_name = "nextSync")]
363    pub fn next_sync(&self, env: Env) -> napi::Result<Object<'static>> {
364        let mut guard = self
365            .inner
366            .lock()
367            .map_err(|_| Error::new(Status::GenericFailure, "reader lock poisoned"))?;
368        let reader = guard
369            .as_mut()
370            .ok_or_else(|| Error::new(Status::GenericFailure, "reader is closed"))?;
371
372        let rec = reader
373            .next_record()
374            .map_err(|e| Error::new(Status::GenericFailure, format!("read failed: {e}")))?;
375
376        let mut out: Object<'static> = Object::new(&env)?;
377
378        match rec.map(core::Variant::from_record) {
379            None => {
380                out.set_named_property("done", true)?;
381                out.set_named_property("value", ())?;
382            }
383            Some(variant) => {
384                out.set_named_property("done", false)?;
385                out.set_named_property(
386                    "value",
387                    Variant {
388                        inner: Some(variant),
389                        header: self.header.clone(),
390                    },
391                )?;
392            }
393        }
394
395        Ok(out)
396    }
397
398    /// Read the next batch of variants synchronously.
399    /// Returns an array of Variant objects. Empty array means EOF.
400    /// This is more efficient than calling nextSync() repeatedly when
401    /// processing many variants, as it avoids creating {done, value}
402    /// wrapper objects for each variant.
403    #[napi(js_name = "nextBatchSync")]
404    pub fn next_batch_sync(&self, size: Option<u32>) -> napi::Result<Vec<Variant>> {
405        let batch_size = size.map(|s| s as usize).unwrap_or(DEFAULT_BATCH_SIZE);
406        if batch_size == 0 || batch_size > 16384 {
407            return Err(Error::new(
408                Status::InvalidArg,
409                format!("batch size must be between 1 and 16384, got {}", batch_size),
410            ));
411        }
412
413        let mut reader_guard = self
414            .inner
415            .lock()
416            .map_err(|_| Error::new(Status::GenericFailure, "reader lock poisoned"))?;
417        let reader = reader_guard
418            .as_mut()
419            .ok_or_else(|| Error::new(Status::GenericFailure, "reader is closed"))?;
420
421        let mut variants = Vec::with_capacity(batch_size);
422
423        for _ in 0..batch_size {
424            let rec = reader
425                .next_record()
426                .map_err(|e| Error::new(Status::GenericFailure, format!("read failed: {e}")))?;
427
428            match rec {
429                None => break, // EOF
430                Some(record) => {
431                    variants.push(Variant {
432                        inner: Some(core::Variant::from_record(record)),
433                        header: self.header.clone(),
434                    });
435                }
436            }
437        }
438
439        Ok(variants)
440    }
441
442    #[napi]
443    pub fn close(&self) {
444        if let Ok(mut guard) = self.inner.lock() {
445            let _ = guard.take();
446        }
447    }
448}
449
450#[napi]
451pub fn open_reader(path: String, opts: Option<ReaderOptions>) -> AsyncTask<OpenReaderTask> {
452    AsyncTask::new(OpenReaderTask { path, opts })
453}
454
455#[napi]
456pub struct Writer {
457    inner: Arc<Mutex<Option<core::Writer>>>,
458    /// Stored N-API reference so that `writer.header` always returns the same JS object.
459    /// Without this, each call to the getter would create a new JS wrapper, breaking
460    /// identity checks (`writer.header === writer.header`) and allowing mutations to
461    /// be lost if the user modifies one instance but reads from another.
462    ///
463    /// TODO: If we make Header immutable (mutations return a new Header), we could
464    /// remove this field and create a fresh wrapper on each access.
465    header_ref: Reference<Header>,
466}
467
468#[napi]
469impl Writer {
470    #[napi(constructor)]
471    pub fn new(
472        env: Env,
473        path: String,
474        header: &Header,
475        opts: Option<WriterOptions>,
476    ) -> napi::Result<Self> {
477        let mut options = core::WriterOptions::default();
478
479        if let Some(opts) = opts {
480            if let Some(format) = opts.format {
481                options.format = match format.as_str() {
482                    "vcf" => Some(core::OutputFormat::Vcf),
483                    "bcf" => Some(core::OutputFormat::Bcf),
484                    _ => {
485                        return Err(Error::new(
486                            Status::InvalidArg,
487                            "WriterOptions.format must be 'vcf' or 'bcf'",
488                        ))
489                    }
490                };
491            }
492            if let Some(uncompressed) = opts.uncompressed {
493                options.uncompressed = uncompressed;
494            }
495            if let Some(threads) = opts.threads {
496                options.threads = Some(threads as usize);
497            }
498        }
499
500        let writer = core::open_writer(&path, header.inner.as_ref(), options).map_err(|e| {
501            Error::new(
502                Status::GenericFailure,
503                format!("failed to open writer: {e}"),
504            )
505        })?;
506
507        let header_ref = Header::into_reference(
508            Header {
509                inner: header.inner.clone(),
510            },
511            env,
512        )?;
513
514        Ok(Self {
515            inner: Arc::new(Mutex::new(Some(writer))),
516            header_ref,
517        })
518    }
519
520    #[napi(getter)]
521    pub fn header(&self, env: Env) -> napi::Result<Reference<Header>> {
522        self.header_ref.clone(env)
523    }
524
525    #[napi]
526    pub fn write(&self, variant: &mut Variant) -> napi::Result<()> {
527        let mut writer_guard = self
528            .inner
529            .lock()
530            .map_err(|_| Error::new(Status::GenericFailure, "writer lock poisoned"))?;
531        let writer = writer_guard
532            .as_mut()
533            .ok_or_else(|| Error::new(Status::GenericFailure, "writer is closed"))?;
534
535        // Keep the header alive while writing.
536        // Required because records translated to a new header may hold raw pointers
537        // into that header.
538        let _header_keepalive = variant.header.clone();
539
540        let mut record = variant
541            .inner
542            .take()
543            .ok_or_else(|| Error::new(Status::GenericFailure, "variant was consumed"))?
544            .into_record();
545
546        writer
547            .write_record(&mut record)
548            .map_err(|e| Error::new(Status::GenericFailure, format!("write failed: {e}")))
549    }
550
551    #[napi]
552    pub fn close(&self) {
553        if let Ok(mut guard) = self.inner.lock() {
554            let _ = guard.take();
555        }
556    }
557}
558
559pub struct OpenReaderTask {
560    path: String,
561    #[allow(dead_code)]
562    opts: Option<ReaderOptions>,
563}
564
565impl Task for OpenReaderTask {
566    type Output = core::Reader;
567    type JsValue = Reader;
568
569    fn compute(&mut self) -> napi::Result<Self::Output> {
570        core::open_reader(&self.path).map_err(|e| {
571            Error::new(
572                Status::GenericFailure,
573                format!("failed to open {}: {e}", self.path),
574            )
575        })
576    }
577
578    fn resolve(&mut self, env: Env, output: Self::Output) -> napi::Result<Self::JsValue> {
579        let header = Arc::new(unsafe { core::Header::new(output.header_ptr()) });
580        let header_ref = Header::into_reference(
581            Header {
582                inner: header.clone(),
583            },
584            env,
585        )?;
586
587        Ok(Reader {
588            inner: Arc::new(Mutex::new(Some(output))),
589            header,
590            header_ref,
591        })
592    }
593}
594
595pub struct QueryTask {
596    inner: Arc<Mutex<Option<core::Reader>>>,
597    region_or_chrom: String,
598    start0: Option<u32>,
599    end0: Option<u32>,
600}
601
602impl Task for QueryTask {
603    type Output = ();
604    type JsValue = ();
605
606    fn compute(&mut self) -> napi::Result<Self::Output> {
607        let mut guard = self
608            .inner
609            .lock()
610            .map_err(|_| Error::new(Status::GenericFailure, "reader lock poisoned"))?;
611        let reader = guard
612            .as_mut()
613            .ok_or_else(|| Error::new(Status::GenericFailure, "reader is closed"))?;
614
615        if !reader.has_index() {
616            return Err(Error::new(
617                Status::GenericFailure,
618                "query() requires an indexed file",
619            ));
620        }
621
622        reader
623            .query(
624                &self.region_or_chrom,
625                self.start0.map(|v| v as u64),
626                self.end0.map(|v| v as u64),
627            )
628            .map_err(|e| Error::new(Status::GenericFailure, format!("query failed: {e}")))?;
629
630        Ok(())
631    }
632
633    fn resolve(&mut self, _env: Env, _output: Self::Output) -> napi::Result<Self::JsValue> {
634        Ok(())
635    }
636}
637
638pub struct NextTask {
639    inner: Arc<Mutex<Option<core::Reader>>>,
640    header: Arc<core::Header>,
641}
642
643impl Task for NextTask {
644    type Output = Option<core::Variant>;
645    type JsValue = Object<'static>;
646
647    fn compute(&mut self) -> napi::Result<Self::Output> {
648        let mut guard = self
649            .inner
650            .lock()
651            .map_err(|_| Error::new(Status::GenericFailure, "reader lock poisoned"))?;
652        let reader = guard
653            .as_mut()
654            .ok_or_else(|| Error::new(Status::GenericFailure, "reader is closed"))?;
655
656        let rec = reader
657            .next_record()
658            .map_err(|e| Error::new(Status::GenericFailure, format!("read failed: {e}")))?;
659
660        Ok(rec.map(core::Variant::from_record))
661    }
662
663    fn resolve(&mut self, env: Env, output: Self::Output) -> napi::Result<Self::JsValue> {
664        let mut out: Object<'static> = Object::new(&env)?;
665
666        match output {
667            None => {
668                out.set_named_property("done", true)?;
669                out.set_named_property("value", ())?;
670            }
671            Some(variant) => {
672                out.set_named_property("done", false)?;
673                out.set_named_property(
674                    "value",
675                    Variant {
676                        inner: Some(variant),
677                        header: self.header.clone(),
678                    },
679                )?;
680            }
681        }
682
683        Ok(out)
684    }
685}
686
687pub struct NextBatchTask {
688    inner: Arc<Mutex<Option<core::Reader>>>,
689    header: Arc<core::Header>,
690    size: usize,
691}
692
693impl Task for NextBatchTask {
694    type Output = Vec<core::Variant>;
695    type JsValue = Vec<Variant>;
696
697    fn compute(&mut self) -> napi::Result<Self::Output> {
698        let mut guard = self
699            .inner
700            .lock()
701            .map_err(|_| Error::new(Status::GenericFailure, "reader lock poisoned"))?;
702        let reader = guard
703            .as_mut()
704            .ok_or_else(|| Error::new(Status::GenericFailure, "reader is closed"))?;
705
706        let mut variants = Vec::with_capacity(self.size);
707
708        for _ in 0..self.size {
709            let rec = reader
710                .next_record()
711                .map_err(|e| Error::new(Status::GenericFailure, format!("read failed: {e}")))?;
712
713            match rec {
714                None => break, // EOF
715                Some(record) => {
716                    variants.push(core::Variant::from_record(record));
717                }
718            }
719        }
720
721        Ok(variants)
722    }
723
724    fn resolve(&mut self, _env: Env, output: Self::Output) -> napi::Result<Self::JsValue> {
725        Ok(output
726            .into_iter()
727            .map(|v| Variant {
728                inner: Some(v),
729                header: self.header.clone(),
730            })
731            .collect())
732    }
733}
734
735#[napi]
736pub struct Variant {
737    pub(crate) inner: Option<core::Variant>,
738    header: Arc<core::Header>,
739}
740
741#[napi]
742impl Variant {
743    fn variant(&self) -> napi::Result<&core::Variant> {
744        self.inner
745            .as_ref()
746            .ok_or_else(|| Error::new(Status::GenericFailure, "variant was consumed"))
747    }
748
749    fn variant_mut(&mut self) -> napi::Result<&mut core::Variant> {
750        self.inner
751            .as_mut()
752            .ok_or_else(|| Error::new(Status::GenericFailure, "variant was consumed"))
753    }
754
755    #[napi(getter)]
756    pub fn chrom(&self) -> napi::Result<String> {
757        Ok(self.variant()?.chrom().to_string())
758    }
759
760    #[napi(getter)]
761    pub fn rid(&self) -> napi::Result<Option<u32>> {
762        Ok(self.variant()?.rid())
763    }
764
765    #[napi(getter)]
766    pub fn pos(&self) -> napi::Result<i64> {
767        Ok(self.variant()?.pos())
768    }
769
770    #[napi(getter)]
771    pub fn start(&self) -> napi::Result<i64> {
772        Ok(self.variant()?.start())
773    }
774
775    #[napi(getter, js_name = "stop")]
776    pub fn stop(&self) -> napi::Result<i64> {
777        Ok(self.variant()?.end())
778    }
779
780    #[napi(getter)]
781    pub fn id(&self) -> napi::Result<String> {
782        Ok(self.variant()?.id())
783    }
784
785    #[napi(setter)]
786    pub fn set_id(&mut self, id: String) -> napi::Result<()> {
787        self.variant_mut()?
788            .set_id(&id)
789            .map_err(|e| Error::new(Status::GenericFailure, format!("failed to set id: {e}")))
790    }
791
792    #[napi(getter, js_name = "ref")]
793    pub fn reference(&self) -> napi::Result<String> {
794        Ok(self.variant()?.reference())
795    }
796
797    #[napi(getter)]
798    pub fn alt(&self) -> napi::Result<Vec<String>> {
799        Ok(self.variant()?.alts())
800    }
801
802    #[napi(getter)]
803    pub fn qual(&self) -> napi::Result<Option<f64>> {
804        Ok(self.variant()?.qual().map(|v| v as f64))
805    }
806
807    #[napi(setter)]
808    pub fn set_qual(&mut self, qual: Option<f64>) -> napi::Result<()> {
809        self.variant_mut()?.set_qual(qual.map(|v| v as f32));
810        Ok(())
811    }
812
813    #[napi(getter)]
814    pub fn filter(&self) -> napi::Result<Vec<String>> {
815        Ok(self.variant()?.filters())
816    }
817
818    #[napi(setter)]
819    pub fn set_filter(&mut self, filter: Vec<String>) -> napi::Result<()> {
820        self.variant_mut()?
821            .set_filters(&filter)
822            .map_err(|e| Error::new(Status::GenericFailure, format!("failed to set filter: {e}")))
823    }
824
825    #[napi(js_name = "toString")]
826    pub fn to_string(&self) -> napi::Result<String> {
827        self.variant()?
828            .to_string(&self.header)
829            .ok_or_else(|| Error::new(Status::GenericFailure, "failed to format record"))
830    }
831
832    #[napi]
833    pub fn info(&self, env: Env, tag: String) -> napi::Result<sys::napi_value> {
834        let v = self.variant()?.info(&self.header, &tag);
835        infovalue_to_napi_value(&env, &v)
836    }
837
838    #[napi]
839    pub fn format(&self, env: Env, tag: String) -> napi::Result<sys::napi_value> {
840        let v = self.variant()?.format(&self.header, &tag);
841        formatvalue_to_napi_value(&env, &v)
842    }
843
844    #[napi]
845    pub fn sample(&self, env: Env, name: String) -> napi::Result<sys::napi_value> {
846        let Some(fields) = self.variant()?.sample(&self.header, &name) else {
847            return unsafe { ToNapiValue::to_napi_value(env.raw(), ()) };
848        };
849
850        let mut out: Object<'static> = Object::new(&env)?;
851        for (tag, value) in fields {
852            let js_value = formatvalue_to_napi_value(&env, &value)?;
853            out.set_named_property(tag.as_str(), unsafe {
854                Unknown::from_raw_unchecked(env.raw(), js_value)
855            })?;
856        }
857
858        Ok(out.raw())
859    }
860
861    #[napi]
862    pub fn samples(&self, env: Env, subset: Option<Vec<String>>) -> napi::Result<sys::napi_value> {
863        let subset_refs: Option<Vec<&str>> = subset
864            .as_ref()
865            .map(|v| v.iter().map(|s| s.as_str()).collect());
866        let all_samples = self
867            .variant()?
868            .samples(&self.header, subset_refs.as_deref());
869
870        let mut arr_items: Vec<sys::napi_value> = Vec::with_capacity(all_samples.len());
871
872        for fields in all_samples {
873            let mut out: Object<'static> = Object::new(&env)?;
874            for (tag, value) in fields {
875                let js_value = formatvalue_to_napi_value(&env, &value)?;
876                out.set_named_property(tag.as_str(), unsafe {
877                    Unknown::from_raw_unchecked(env.raw(), js_value)
878                })?;
879            }
880            arr_items.push(out.raw());
881        }
882
883        let arr = Array::from_vec(&env, arr_items)?;
884        Ok(arr.raw())
885    }
886
887    #[napi]
888    pub fn genotypes(&self, subset: Option<Vec<String>>) -> napi::Result<Vec<Genotype>> {
889        let subset_refs: Option<Vec<&str>> = subset
890            .as_ref()
891            .map(|v| v.iter().map(|s| s.as_str()).collect());
892        let genotypes = self
893            .variant()?
894            .genotypes(&self.header, subset_refs.as_deref());
895
896        Ok(genotypes.into_iter().map(Genotype::from).collect())
897    }
898
899    #[napi(js_name = "set_genotypes")]
900    pub fn set_genotypes(&mut self, genotypes: Vec<Genotype>) -> napi::Result<()> {
901        let gts: Vec<core::Genotype> = genotypes.into_iter().map(core::Genotype::from).collect();
902        self.variant_mut()?.set_genotypes(&gts).map_err(|e| {
903            Error::new(
904                Status::GenericFailure,
905                format!("failed to set genotypes: {e}"),
906            )
907        })
908    }
909
910    #[napi]
911    pub fn translate(&mut self, header: &Header) -> napi::Result<()> {
912        self.header = header.inner.clone();
913
914        self.variant_mut()?
915            .translate(&header.inner)
916            .map_err(|e| Error::new(Status::GenericFailure, format!("translate failed: {e}")))
917    }
918
919    #[napi(js_name = "set_info")]
920    pub fn set_info(&mut self, tag: String, value: Unknown) -> napi::Result<()> {
921        use napi::ValueType;
922        use rust_htslib::bcf::header::TagType;
923
924        let header = self.header.clone();
925
926        let Some((tag_type, _tag_length)) = header.info_type(tag.as_bytes()) else {
927            return Err(Error::new(
928                Status::InvalidArg,
929                format!("undefined INFO tag: {tag}"),
930            ));
931        };
932
933        match value.get_type()? {
934            ValueType::Null | ValueType::Undefined => {
935                self.variant_mut()?.clear_info(&header, &tag).map_err(|e| {
936                    Error::new(
937                        Status::GenericFailure,
938                        format!("failed to clear info {tag}: {e}"),
939                    )
940                })?;
941                return Ok(());
942            }
943            _ => {}
944        }
945
946        match tag_type {
947            TagType::Flag => {
948                if value.is_array()? {
949                    return Err(Error::new(
950                        Status::InvalidArg,
951                        format!("INFO/{tag} is Flag; expected boolean"),
952                    ));
953                }
954
955                if value.get_type()? != ValueType::Boolean {
956                    return Err(Error::new(
957                        Status::InvalidArg,
958                        format!("INFO/{tag} is Flag; expected boolean"),
959                    ));
960                }
961
962                let is_set: bool = unsafe { value.cast()? };
963                self.variant_mut()?
964                    .set_info_flag(&header, &tag, is_set)
965                    .map_err(|e| {
966                        Error::new(
967                            Status::GenericFailure,
968                            format!("failed to set info {tag}: {e}"),
969                        )
970                    })?;
971            }
972            TagType::Integer => {
973                let values = unknown_to_numbers(&tag, value)?;
974                let mut out: Vec<i32> = Vec::with_capacity(values.len());
975                for n in values {
976                    if !n.is_finite() {
977                        return Err(Error::new(Status::InvalidArg, "number must be finite"));
978                    }
979                    if n.fract() != 0.0 {
980                        return Err(Error::new(
981                            Status::InvalidArg,
982                            format!("INFO/{tag} is Integer; got non-integer value"),
983                        ));
984                    }
985                    if n < (i32::MIN as f64) || n > (i32::MAX as f64) {
986                        return Err(Error::new(
987                            Status::InvalidArg,
988                            format!("INFO/{tag} integer out of range"),
989                        ));
990                    }
991                    out.push(n as i32);
992                }
993
994                self.variant_mut()?
995                    .set_info_integer(&header, &tag, &out)
996                    .map_err(|e| {
997                        Error::new(
998                            Status::GenericFailure,
999                            format!("failed to set info {tag}: {e}"),
1000                        )
1001                    })?;
1002            }
1003            TagType::Float => {
1004                let values = unknown_to_numbers(&tag, value)?;
1005                let mut out: Vec<f32> = Vec::with_capacity(values.len());
1006                for n in values {
1007                    if !n.is_finite() {
1008                        return Err(Error::new(Status::InvalidArg, "number must be finite"));
1009                    }
1010                    out.push(n as f32);
1011                }
1012
1013                self.variant_mut()?
1014                    .set_info_float(&header, &tag, &out)
1015                    .map_err(|e| {
1016                        Error::new(
1017                            Status::GenericFailure,
1018                            format!("failed to set info {tag}: {e}"),
1019                        )
1020                    })?;
1021            }
1022            TagType::String => {
1023                let values: Vec<String> = unknown_to_strings(&tag, value)?;
1024                self.variant_mut()?
1025                    .set_info_string(&header, &tag, &values)
1026                    .map_err(|e| {
1027                        Error::new(
1028                            Status::GenericFailure,
1029                            format!("failed to set info {tag}: {e}"),
1030                        )
1031                    })?;
1032            }
1033        }
1034
1035        Ok(())
1036    }
1037
1038    /// Set a FORMAT field value (array with one entry per sample).
1039    ///
1040    /// Values should be an array with one entry per sample. Each entry can be:
1041    /// - A scalar (number or string) for Number=1 fields
1042    /// - An array of values for multi-value fields
1043    /// - null for missing values
1044    ///
1045    /// Pass null to clear the FORMAT field entirely.
1046    #[napi(js_name = "set_format")]
1047    pub fn set_format(&mut self, tag: String, value: Unknown) -> napi::Result<()> {
1048        use napi::ValueType;
1049        use rust_htslib::bcf::header::TagType;
1050
1051        // Reject GT field
1052        if tag == "GT" {
1053            return Err(Error::new(
1054                Status::InvalidArg,
1055                "GT cannot be set via set_format; use dedicated genotype methods",
1056            ));
1057        }
1058
1059        let header = self.header.clone();
1060
1061        let Some((tag_type, _tag_length)) = header.format_type(tag.as_bytes()) else {
1062            return Err(Error::new(
1063                Status::InvalidArg,
1064                format!("undefined FORMAT tag: {tag}"),
1065            ));
1066        };
1067
1068        // Check for clear (null/undefined at top level)
1069        match value.get_type()? {
1070            ValueType::Null | ValueType::Undefined => {
1071                let variant = self.variant_mut()?;
1072                variant.clear_format(&header, &tag).map_err(|e| {
1073                    Error::new(
1074                        Status::GenericFailure,
1075                        format!("failed to clear format {tag}: {e}"),
1076                    )
1077                })?;
1078                return Ok(());
1079            }
1080            _ => {}
1081        }
1082
1083        // Value must be an array
1084        if !value.is_array()? {
1085            return Err(Error::new(
1086                Status::InvalidArg,
1087                "variant.set_format values must be an array",
1088            ));
1089        }
1090
1091        let arr: Array = unsafe { value.cast()? };
1092        let sample_count = self.header.sample_count() as u32;
1093
1094        if arr.len() != sample_count {
1095            return Err(Error::new(
1096                Status::InvalidArg,
1097                format!(
1098                    "variant.set_format array length ({}) must match sample count ({})",
1099                    arr.len(),
1100                    sample_count
1101                ),
1102            ));
1103        }
1104
1105        let missing_int = htsvcf_core::format_int_missing();
1106        let missing_float = htsvcf_core::format_float_missing();
1107
1108        match tag_type {
1109            TagType::Integer => {
1110                let flattened = format::flatten_format_integers(&tag, &arr, missing_int)?;
1111                self.variant_mut()?
1112                    .set_format_integer(&header, &tag, &flattened)
1113                    .map_err(|e| {
1114                        Error::new(
1115                            Status::GenericFailure,
1116                            format!("failed to set format {tag}: {e}"),
1117                        )
1118                    })?;
1119            }
1120            TagType::Float => {
1121                let flattened = format::flatten_format_floats(&tag, &arr, missing_float)?;
1122                self.variant_mut()?
1123                    .set_format_float(&header, &tag, &flattened)
1124                    .map_err(|e| {
1125                        Error::new(
1126                            Status::GenericFailure,
1127                            format!("failed to set format {tag}: {e}"),
1128                        )
1129                    })?;
1130            }
1131            TagType::String => {
1132                let strings = format::flatten_format_strings(&tag, &arr)?;
1133                self.variant_mut()?
1134                    .set_format_string(&header, &tag, &strings)
1135                    .map_err(|e| {
1136                        Error::new(
1137                            Status::GenericFailure,
1138                            format!("failed to set format {tag}: {e}"),
1139                        )
1140                    })?;
1141            }
1142            TagType::Flag => {
1143                return Err(Error::new(
1144                    Status::InvalidArg,
1145                    format!("FORMAT/{tag} is a Flag type which is not supported"),
1146                ));
1147            }
1148        }
1149
1150        Ok(())
1151    }
1152}
1153
1154fn unknown_to_numbers(tag: &str, value: Unknown) -> napi::Result<Vec<f64>> {
1155    if value.is_array()? {
1156        let arr: Array = unsafe { value.cast()? };
1157        let len = arr.len();
1158        let mut out = Vec::with_capacity(len as usize);
1159        for i in 0..len {
1160            let v: Unknown = arr.get_element(i)?;
1161            let n = unknown_to_number(tag, v)?;
1162            out.push(n);
1163        }
1164        return Ok(out);
1165    }
1166
1167    Ok(vec![unknown_to_number(tag, value)?])
1168}
1169
1170fn unknown_to_number(tag: &str, value: Unknown) -> napi::Result<f64> {
1171    use napi::ValueType;
1172
1173    if value.get_type()? != ValueType::Number {
1174        return Err(Error::new(
1175            Status::InvalidArg,
1176            format!("INFO/{tag} expected number"),
1177        ));
1178    }
1179
1180    let n: f64 = unsafe { value.cast()? };
1181    Ok(n)
1182}
1183
1184fn unknown_to_strings(tag: &str, value: Unknown) -> napi::Result<Vec<String>> {
1185    if value.is_array()? {
1186        let arr: Array = unsafe { value.cast()? };
1187        let len = arr.len();
1188        let mut out = Vec::with_capacity(len as usize);
1189        for i in 0..len {
1190            let v: Unknown = arr.get_element(i)?;
1191            let s = unknown_to_string(tag, v)?;
1192            out.push(s);
1193        }
1194        return Ok(out);
1195    }
1196
1197    Ok(vec![unknown_to_string(tag, value)?])
1198}
1199
1200fn unknown_to_string(tag: &str, value: Unknown) -> napi::Result<String> {
1201    use napi::ValueType;
1202
1203    if value.get_type()? != ValueType::String {
1204        return Err(Error::new(
1205            Status::InvalidArg,
1206            format!("INFO/{tag} expected string"),
1207        ));
1208    }
1209
1210    let s: String = unsafe { value.cast()? };
1211    Ok(s)
1212}
1213
1214#[napi]
1215pub struct Header {
1216    pub(crate) inner: Arc<core::Header>,
1217}
1218
1219#[napi]
1220impl Header {
1221    #[napi(js_name = "toString")]
1222    pub fn to_string(&self) -> napi::Result<String> {
1223        self.inner
1224            .to_string()
1225            .ok_or_else(|| Error::new(Status::GenericFailure, "failed to format header"))
1226    }
1227
1228    #[napi(js_name = "addInfo")]
1229    pub fn add_info(&self, id: String, number: String, ty: String, description: String) {
1230        let _ = self.inner.add_info(&id, &number, &ty, &description);
1231    }
1232
1233    #[napi(js_name = "addFormat")]
1234    pub fn add_format(&self, id: String, number: String, ty: String, description: String) {
1235        let _ = self.inner.add_format(&id, &number, &ty, &description);
1236    }
1237
1238    #[napi]
1239    pub fn get(&self, section: String, id: String) -> Option<HeaderField> {
1240        self.inner.get_field(&section, &id).map(HeaderField::from)
1241    }
1242
1243    #[napi]
1244    pub fn records(&self) -> Vec<HeaderRecord> {
1245        self.inner
1246            .all_fields()
1247            .into_iter()
1248            .map(|(section, field)| HeaderRecord {
1249                section,
1250                id: field.id,
1251                r#type: field.r#type,
1252                number: field.number,
1253                description: field.description,
1254            })
1255            .collect()
1256    }
1257
1258    #[napi]
1259    pub fn samples(&self) -> Vec<String> {
1260        self.inner.sample_names().to_vec()
1261    }
1262}
1263
1264/// A VCF header field definition (INFO or FORMAT).
1265#[napi(object)]
1266pub struct HeaderField {
1267    pub id: String,
1268    #[napi(js_name = "type")]
1269    pub r#type: String,
1270    pub number: String,
1271    pub description: String,
1272}
1273
1274impl From<core::header::HeaderField> for HeaderField {
1275    fn from(f: core::header::HeaderField) -> Self {
1276        HeaderField {
1277            id: f.id,
1278            r#type: f.r#type,
1279            number: f.number,
1280            description: f.description,
1281        }
1282    }
1283}
1284
1285/// A VCF header record with its section (INFO, FORMAT, FILTER, etc.).
1286#[napi(object)]
1287pub struct HeaderRecord {
1288    pub section: String,
1289    pub id: String,
1290    #[napi(js_name = "type")]
1291    pub r#type: String,
1292    pub number: String,
1293    pub description: String,
1294}
1295
1296fn infovalue_to_napi_value(env: &Env, v: &core::InfoValue) -> napi::Result<sys::napi_value> {
1297    match v {
1298        core::InfoValue::Absent => unsafe { ToNapiValue::to_napi_value(env.raw(), ()) },
1299        core::InfoValue::Missing => unsafe { ToNapiValue::to_napi_value(env.raw(), Null) },
1300        core::InfoValue::Bool(b) => unsafe { ToNapiValue::to_napi_value(env.raw(), *b) },
1301        core::InfoValue::Int(i) => unsafe {
1302            ToNapiValue::to_napi_value(env.raw(), env.create_int32(*i)?)
1303        },
1304        core::InfoValue::Float(f) => unsafe {
1305            ToNapiValue::to_napi_value(env.raw(), env.create_double(*f as f64)?)
1306        },
1307        core::InfoValue::String(s) => unsafe {
1308            ToNapiValue::to_napi_value(env.raw(), env.create_string(s)?)
1309        },
1310        core::InfoValue::Array(values) => {
1311            let inner_values = values
1312                .iter()
1313                .map(|item| infovalue_to_napi_value(env, item))
1314                .collect::<napi::Result<Vec<sys::napi_value>>>()?;
1315            let arr = Array::from_vec(env, inner_values)?;
1316            Ok(arr.raw())
1317        }
1318    }
1319}
1320
1321fn formatvalue_to_napi_value(env: &Env, v: &FormatValue) -> napi::Result<sys::napi_value> {
1322    match v {
1323        FormatValue::Absent => unsafe { ToNapiValue::to_napi_value(env.raw(), ()) },
1324        FormatValue::Missing => unsafe { ToNapiValue::to_napi_value(env.raw(), Null) },
1325        FormatValue::Int(i) => unsafe {
1326            ToNapiValue::to_napi_value(env.raw(), env.create_int32(*i)?)
1327        },
1328        FormatValue::Float(f) => unsafe {
1329            ToNapiValue::to_napi_value(env.raw(), env.create_double(*f as f64)?)
1330        },
1331        FormatValue::String(s) => unsafe {
1332            ToNapiValue::to_napi_value(env.raw(), env.create_string(s)?)
1333        },
1334        FormatValue::Array(values) => {
1335            let inner_values = values
1336                .iter()
1337                .map(|item| formatvalue_to_napi_value(env, item))
1338                .collect::<napi::Result<Vec<sys::napi_value>>>()?;
1339            let arr = Array::from_vec(env, inner_values)?;
1340            Ok(arr.raw())
1341        }
1342        FormatValue::PerSample(values) => {
1343            let inner_values = values
1344                .iter()
1345                .map(|item| formatvalue_to_napi_value(env, item))
1346                .collect::<napi::Result<Vec<sys::napi_value>>>()?;
1347            let arr = Array::from_vec(env, inner_values)?;
1348            Ok(arr.raw())
1349        }
1350        FormatValue::Genotype(gt) => {
1351            let genotype = Genotype::from(gt.clone());
1352            unsafe { ToNapiValue::to_napi_value(env.raw(), genotype) }
1353        }
1354    }
1355}