htslib 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -51
  3. data/TUTORIAL.md +11 -22
  4. data/ext/htslib_native/extconf.rb +41 -0
  5. data/ext/htslib_native/htslib_native.h +11 -0
  6. data/ext/htslib_native/htslib_native_ext.c +48 -0
  7. data/ext/htslib_native/native_bam.c +1167 -0
  8. data/ext/htslib_native/native_bcf.c +447 -0
  9. data/ext/htslib_native/native_faidx.c +164 -0
  10. data/ext/htslib_native/native_tabix.c +255 -0
  11. data/lib/hts/bam/auxi.rb +87 -342
  12. data/lib/hts/bam/base_mod.rb +38 -73
  13. data/lib/hts/bam/cigar.rb +9 -55
  14. data/lib/hts/bam/flag.rb +24 -30
  15. data/lib/hts/bam/header.rb +29 -56
  16. data/lib/hts/bam/mpileup.rb +158 -132
  17. data/lib/hts/bam/pileup.rb +120 -145
  18. data/lib/hts/bam/record.rb +233 -270
  19. data/lib/hts/bam.rb +154 -45
  20. data/lib/hts/bcf/errors.rb +4 -0
  21. data/lib/hts/bcf/format.rb +283 -377
  22. data/lib/hts/bcf/header.rb +40 -122
  23. data/lib/hts/bcf/header_record.rb +9 -35
  24. data/lib/hts/bcf/info.rb +73 -320
  25. data/lib/hts/bcf/record.rb +73 -101
  26. data/lib/hts/bcf.rb +178 -321
  27. data/lib/hts/faidx.rb +30 -87
  28. data/lib/hts/hts.rb +6 -94
  29. data/lib/hts/native.rb +13 -0
  30. data/lib/hts/tabix.rb +109 -51
  31. data/lib/hts/version.rb +1 -1
  32. data/lib/htslib.rb +6 -52
  33. metadata +13 -64
  34. data/lib/hts/ffi_ext/README.md +0 -8
  35. data/lib/hts/ffi_ext/pointer.rb +0 -18
  36. data/lib/hts/ffi_ext/struct.rb +0 -45
  37. data/lib/hts/libhts/bgzf.rb +0 -199
  38. data/lib/hts/libhts/constants.rb +0 -658
  39. data/lib/hts/libhts/cram.rb +0 -471
  40. data/lib/hts/libhts/fai.rb +0 -146
  41. data/lib/hts/libhts/hfile.rb +0 -121
  42. data/lib/hts/libhts/hts.rb +0 -477
  43. data/lib/hts/libhts/kfunc.rb +0 -38
  44. data/lib/hts/libhts/sam.rb +0 -760
  45. data/lib/hts/libhts/sam_funcs.rb +0 -155
  46. data/lib/hts/libhts/tbx.rb +0 -94
  47. data/lib/hts/libhts/tbx_funcs.rb +0 -36
  48. data/lib/hts/libhts/thread_pool.rb +0 -139
  49. data/lib/hts/libhts/vcf.rb +0 -567
  50. data/lib/hts/libhts/vcf_funcs.rb +0 -366
  51. data/lib/hts/libhts.rb +0 -47
data/lib/hts/bcf/info.rb CHANGED
@@ -2,385 +2,138 @@
2
2
 
3
3
  module HTS
4
4
  class Bcf < Hts
5
- # Info field
6
5
  class Info
7
- def initialize(record)
8
- @record = record
9
- end
10
-
11
- # @note Specify the type. If you don't specify a type, it will still work, but it will be slower.
12
- # @note: Why is this method named "get" instead of "fetch"?
13
- # This is for compatibility with the Crystal language
14
- # which provides methods like `get_int`, `get_float`, etc.
15
- # I think they are better than `fetch_int`` and `fetch_float`.
16
- def get(key, type = nil)
17
- n = FFI::MemoryPointer.new(:int)
18
- p1 = FFI::MemoryPointer.new(:pointer)
19
- p1.write_pointer(FFI::Pointer::NULL)
20
- h = @record.header.struct
21
- r = @record.struct
22
-
23
- info_values = proc do |typ, reader|
24
- ret = LibHTS.bcf_get_info_values(h, r, key, p1, n, typ)
25
- return nil if ret < 0 # return from method.
6
+ TYPE_CODES = {
7
+ flag: Native::BCF_HT_FLAG, bool: Native::BCF_HT_FLAG,
8
+ int: Native::BCF_HT_INT, int32: Native::BCF_HT_INT,
9
+ int64: Native::BCF_HT_LONG, long: Native::BCF_HT_LONG,
10
+ float: Native::BCF_HT_REAL, real: Native::BCF_HT_REAL,
11
+ string: Native::BCF_HT_STR, str: Native::BCF_HT_STR
12
+ }.freeze
26
13
 
27
- dst = p1.read_pointer
28
- begin
29
- reader.call(dst, n.read_int)
30
- ensure
31
- LibHTS.hts_free(dst) unless dst.null?
32
- p1.write_pointer(FFI::Pointer::NULL)
33
- end
34
- end
35
-
36
- actual_type = ht_type_to_sym(get_info_type(key))
37
- if type && actual_type && !info_type_compatible?(actual_type, type.to_sym)
38
- raise InfoTypeError, "Tag #{key} is not #{type_label(type)} INFO field"
39
- end
14
+ def initialize(record) = @record = record
40
15
 
41
- type ||= actual_type
42
- return nil if actual_type && !key?(key)
16
+ def get(key, type = nil)
17
+ schema = header.schema("INFO", key.to_s)
18
+ raise InfoDefinitionError, "INFO tag #{key} not defined in header" unless schema
43
19
 
44
- case type&.to_sym
45
- when :int, :int32
46
- info_values.call(LibHTS::BCF_HT_INT, ->(dst, len) { dst.read_array_of_int32(len) })
47
- when :int64, :long
48
- info_values.call(LibHTS::BCF_HT_LONG, ->(dst, len) { dst.read_array_of_int64(len) })
49
- when :float, :real
50
- info_values.call(LibHTS::BCF_HT_REAL, ->(dst, len) { dst.read_array_of_float(len) })
51
- when :flag, :bool
52
- begin
53
- case ret = LibHTS.bcf_get_info_flag(h, r, key, p1, n)
54
- when 1 then true
55
- when 0 then false
56
- when -1 then nil
57
- else
58
- raise InfoReadError, "Unknown return value from bcf_get_info_flag: #{ret}"
59
- end
60
- ensure
61
- dst = p1.read_pointer
62
- LibHTS.hts_free(dst) unless dst.null?
63
- p1.write_pointer(FFI::Pointer::NULL)
64
- end
65
- when :string, :str
66
- info_values.call(LibHTS::BCF_HT_STR, ->(dst, _len) { dst.read_string })
20
+ actual_type = schema.first
21
+ requested = type&.to_sym
22
+ if requested && !type_compatible?(actual_type, requested)
23
+ raise InfoTypeError, "Tag #{key} is not #{type_label(requested)} INFO field"
67
24
  end
68
- end
69
-
70
- # For compatibility with HTS.cr.
71
- def get_int(key)
72
- get(key, :int)
73
- end
74
-
75
- # For compatibility with HTS.cr.
76
- def get_float(key)
77
- get(key, :float)
78
- end
79
-
80
- # For compatibility with HTS.cr.
81
- def get_int64(key)
82
- get(key, :int64)
83
- end
84
-
85
- # For compatibility with HTS.cr.
86
- def get_string(key)
87
- get(key, :string)
88
- end
89
25
 
90
- # For compatibility with HTS.cr.
91
- def get_flag(key)
92
- get(key, :flag)
26
+ code = TYPE_CODES.fetch(requested || actual_type)
27
+ value = native.info_get(header_native, key.to_s, code)
28
+ code == Native::BCF_HT_FLAG && value.nil? ? false : value
93
29
  end
94
30
 
95
- def [](key)
96
- get(key)
97
- end
31
+ def get_int(key) = get(key, :int)
32
+ def get_float(key) = get(key, :float)
33
+ def get_int64(key) = get(key, :int64)
34
+ def get_string(key) = get(key, :string)
35
+ def get_flag(key) = get(key, :flag)
36
+ def [](key) = get(key)
98
37
 
99
- # Set INFO field value with automatic type detection.
100
- # @param key [String] INFO tag name
101
- # @param value [Integer, Float, String, Array, true, false, nil] value to set
102
- # - Integer or Array<Integer> -> update_int
103
- # - Float or Array<Float,Integer> -> update_float
104
- # - String -> update_string
105
- # - true/false -> update_flag
106
- # - nil -> delete the INFO field
107
38
  def []=(key, value)
108
39
  case value
109
- when nil
110
- delete(key)
111
- when true, false
112
- update_flag(key, value)
40
+ when nil then delete(key)
41
+ when true, false then update_flag(key, value)
113
42
  when Integer
114
- unless int32_range?(value)
43
+ unless int32?(value)
115
44
  raise RangeError,
116
45
  "Integer out of int32 range for []=. Current htslib backend does not support int64 INFO update."
117
46
  end
118
47
 
119
48
  update_int(key, [value])
120
- when Float
121
- update_float(key, [value])
122
- when String
123
- update_string(key, value)
49
+ when Float then update_float(key, [value])
50
+ when String then update_string(key, value)
124
51
  when Array
125
- if value.empty?
126
- raise ArgumentError, "Cannot set INFO field to empty array. Use nil to delete."
127
- elsif value.all? { |v| v.is_a?(Integer) }
128
- unless value.all? { |v| int32_range?(v) }
52
+ raise ArgumentError, "Cannot set INFO field to empty array. Use nil to delete." if value.empty?
53
+
54
+ if value.all? { |item| item.is_a?(Integer) }
55
+ unless value.all? do |item|
56
+ int32?(item)
57
+ end
129
58
  raise RangeError,
130
59
  "Integer array contains out-of-int32 values for []=. Current htslib backend does not support int64 INFO update."
131
60
  end
132
61
 
133
62
  update_int(key, value)
134
- elsif value.all? { |v| v.is_a?(Numeric) }
63
+ elsif value.all? { |item| item.is_a?(Numeric) }
135
64
  update_float(key, value)
136
65
  else
137
66
  raise ArgumentError, "INFO array must contain only integers or floats, got: #{value.map(&:class).uniq}"
138
67
  end
139
- else
140
- raise ArgumentError, "Unsupported INFO value type: #{value.class}"
68
+ else raise ArgumentError, "Unsupported INFO value type: #{value.class}"
141
69
  end
142
70
  end
143
71
 
144
- # Update INFO field with integer value(s).
145
- # For compatibility with HTS.cr.
146
- # @param key [String] INFO tag name
147
- # @param values [Array<Integer>] integer values (use single-element array for scalar)
148
- def update_int(key, values)
149
- values = Array(values)
150
- ptr = FFI::MemoryPointer.new(:int32, values.size)
151
- ptr.write_array_of_int32(values)
152
- ret = LibHTS.bcf_update_info(
153
- @record.header.struct,
154
- @record.struct,
155
- key,
156
- ptr,
157
- values.size,
158
- LibHTS::BCF_HT_INT
159
- )
160
- raise InfoUpdateError, "Failed to update INFO int field '#{key}': #{ret}" if ret < 0
161
-
162
- ret
163
- end
164
-
165
- # Update INFO field with int64 value(s).
166
- # @note int64 INFO values are primarily relevant for VCF output.
167
- # @param key [String] INFO tag name
168
- # @param values [Array<Integer>] integer values (use single-element array for scalar)
169
- def update_int64(_key, _values)
170
- raise UnsupportedInfoOperationError, "htslib backend does not implement int64 INFO update (BCF_HT_LONG)"
171
- end
172
-
173
- # Update INFO field with float value(s).
174
- # For compatibility with HTS.cr.
175
- # @param key [String] INFO tag name
176
- # @param values [Array<Float>] float values (use single-element array for scalar)
177
- def update_float(key, values)
178
- values = Array(values).map(&:to_f)
179
- ptr = FFI::MemoryPointer.new(:float, values.size)
180
- ptr.write_array_of_float(values)
181
- ret = LibHTS.bcf_update_info(
182
- @record.header.struct,
183
- @record.struct,
184
- key,
185
- ptr,
186
- values.size,
187
- LibHTS::BCF_HT_REAL
188
- )
189
- raise InfoUpdateError, "Failed to update INFO float field '#{key}': #{ret}" if ret < 0
190
-
191
- ret
192
- end
193
-
194
- # Update INFO field with string value.
195
- # For compatibility with HTS.cr.
196
- # @param key [String] INFO tag name
197
- # @param value [String] string value
198
- def update_string(key, value)
199
- ret = LibHTS.bcf_update_info(
200
- @record.header.struct,
201
- @record.struct,
202
- key,
203
- value.to_s,
204
- 1,
205
- LibHTS::BCF_HT_STR
206
- )
207
- raise InfoUpdateError, "Failed to update INFO string field '#{key}': #{ret}" if ret < 0
208
-
209
- ret
210
- end
72
+ def update_int(key, values) = update(key, Native::BCF_HT_INT, Array(values).map { |value| Integer(value) }, "int")
73
+ def update_float(key, values) = update(key, Native::BCF_HT_REAL, Array(values).map(&:to_f), "float")
74
+ def update_string(key, value) = update(key, Native::BCF_HT_STR, value.to_s, "string")
211
75
 
212
- # Update INFO flag field.
213
- # For compatibility with HTS.cr.
214
- # @param key [String] INFO tag name
215
- # @param present [Boolean] true to set flag, false to remove it
216
- def update_flag(key, present = true)
217
- ret = if present
218
- LibHTS.bcf_update_info(
219
- @record.header.struct,
220
- @record.struct,
221
- key,
222
- FFI::Pointer::NULL,
223
- 1,
224
- LibHTS::BCF_HT_FLAG
225
- )
226
- else
227
- # Remove flag by setting n=0
228
- LibHTS.bcf_update_info(
229
- @record.header.struct,
230
- @record.struct,
231
- key,
232
- FFI::Pointer::NULL,
233
- 0,
234
- LibHTS::BCF_HT_FLAG
235
- )
236
- end
237
- raise InfoUpdateError, "Failed to update INFO flag field '#{key}': #{ret}" if ret < 0
76
+ def update_int64(_key,
77
+ _values) = raise(UnsupportedInfoOperationError,
78
+ "htslib backend does not implement int64 INFO update (BCF_HT_LONG)")
238
79
 
239
- ret
240
- end
80
+ def update_flag(key, present = true) = update(key, Native::BCF_HT_FLAG, !!present, "flag")
241
81
 
242
- # Delete an INFO field.
243
- # @param key [String] INFO tag name
244
- # @return [Boolean] true if field was deleted, false if it didn't exist
245
82
  def delete(key)
246
- type = get_info_type(key)
247
- return false if type.nil?
248
- return false unless key?(key)
249
-
250
- # Delete by setting n=0
251
- ret = LibHTS.bcf_update_info(
252
- @record.header.struct,
253
- @record.struct,
254
- key,
255
- FFI::Pointer::NULL,
256
- 0,
257
- type
258
- )
259
- return false if ret < 0
83
+ schema = header.schema("INFO", key.to_s)
84
+ return false unless schema && key?(key)
260
85
 
86
+ native.info_update(header_native, key.to_s, TYPE_CODES.fetch(schema.first), false)
261
87
  true
262
88
  end
263
89
 
264
- # Check if an INFO field exists.
265
- # @param key [String] INFO tag name
266
- # @return [Boolean] true if the field exists
267
90
  def key?(key)
268
- type = header_info_type(key)
269
- return false if type.nil?
270
-
271
- ndst = FFI::MemoryPointer.new(:int)
272
- ndst.write_int(0)
273
- dst_ptr = FFI::MemoryPointer.new(:pointer)
274
- dst_ptr.write_pointer(FFI::Pointer::NULL)
275
-
276
- ret = LibHTS.bcf_get_info_values(@record.header.struct, @record.struct, key, dst_ptr, ndst, type)
277
- type == LibHTS::BCF_HT_FLAG ? ret == 1 : ret >= 0
278
- ensure
279
- if dst_ptr
280
- dst = dst_ptr.read_pointer
281
- LibHTS.hts_free(dst) unless dst.null?
282
- end
91
+ schema = header.schema("INFO", key.to_s)
92
+ schema ? native.info_present?(header_native, key.to_s, TYPE_CODES.fetch(schema.first)) : false
283
93
  end
284
-
285
94
  alias include? key?
286
95
 
287
- # FIXME: naming? room for improvement.
288
- def fields
289
- keys.map do |key|
290
- name = LibHTS.bcf_hdr_int2id(@record.header.struct, LibHTS::BCF_DT_ID, key)
291
- num = LibHTS.bcf_hdr_id2number(@record.header.struct, LibHTS::BCF_HL_INFO, key)
292
- type = LibHTS.bcf_hdr_id2type(@record.header.struct, LibHTS::BCF_HL_INFO, key)
293
- {
294
- name:,
295
- n: num,
296
- type: ht_type_to_sym(type),
297
- key:
298
- }
299
- end
300
- end
301
-
302
- def length
303
- @record.struct[:n_info]
304
- end
305
-
306
- def size
307
- length
308
- end
309
-
310
- def to_h
311
- ret = {}
312
- keys.each do |key|
313
- name = LibHTS.bcf_hdr_int2id(@record.header.struct, LibHTS::BCF_DT_ID, key)
314
- ret[name] = get(name)
315
- end
316
- ret
317
- end
96
+ def fields = native.info_fields(header_native)
97
+ def keys = fields.map { |field| field[:key] }
98
+ def length = fields.length
99
+ alias size length
100
+ def to_h = fields.to_h { |field| [field[:name], get(field[:name])] }
318
101
 
319
102
  private
320
103
 
321
- def info_ptr
322
- @record.struct[:d][:info].to_ptr
323
- end
104
+ def native = @record.__send__(:native_handle)
105
+ def header = @record.header.__send__(:native_handle)
106
+ alias header_native header
324
107
 
325
- def keys
326
- info_ptr.read_array_of_struct(LibHTS::BcfInfo, length).map do |info|
327
- info[:key]
328
- end
329
- end
108
+ def update(key, type, value, label)
109
+ result = native.info_update(header_native, key.to_s, type, value)
110
+ raise InfoUpdateError, "Failed to update INFO #{label} field '#{key}': #{result}" if result.negative?
330
111
 
331
- def get_info_type(key)
332
- header_info_type(key)
112
+ result
333
113
  end
334
114
 
335
- def header_info_type(key)
336
- id = LibHTS.bcf_hdr_id2int(@record.header.struct, LibHTS::BCF_DT_ID, key)
337
- return nil if id.negative?
338
- return nil unless LibHTS.bcf_hdr_idinfo_exists(@record.header.struct, LibHTS::BCF_HL_INFO, id)
339
-
340
- LibHTS.bcf_hdr_id2type(@record.header.struct, LibHTS::BCF_HL_INFO, id)
341
- end
342
-
343
- def ht_type_to_sym(t)
344
- case t
345
- when LibHTS::BCF_HT_FLAG then :flag
346
- when LibHTS::BCF_HT_INT then :int
347
- when LibHTS::BCF_HT_REAL then :float
348
- when LibHTS::BCF_HT_STR then :string
349
- when LibHTS::BCF_HT_LONG then :int64
350
- end
351
- end
352
-
353
- def int32_range?(value)
354
- value >= -2_147_483_648 && value <= 2_147_483_647
355
- end
356
-
357
- def info_type_compatible?(actual_type, requested_type)
358
- case requested_type
359
- when :int, :int32
360
- actual_type == :int
361
- when :int64, :long
362
- %i[int int64].include?(actual_type)
363
- when :float, :real
364
- actual_type == :float
365
- when :flag, :bool
366
- actual_type == :flag
367
- when :string, :str
368
- actual_type == :string
369
- else
370
- actual_type == requested_type
115
+ def type_compatible?(actual, requested)
116
+ case requested
117
+ when :int, :int32 then actual == :int
118
+ when :int64, :long then %i[int int64].include?(actual)
119
+ when :float, :real then actual == :float
120
+ when :flag, :bool then actual == :flag
121
+ when :string, :str then actual == :string
122
+ else actual == requested
371
123
  end
372
124
  end
373
125
 
374
126
  def type_label(type)
375
- case type.to_sym
376
- when :int, :int32 then "integer"
377
- when :int64, :long then "integer"
127
+ case type
128
+ when :int, :int32, :int64, :long then "integer"
378
129
  when :float, :real then "float"
379
130
  when :flag, :bool then "flag"
380
131
  when :string, :str then "string"
381
132
  else type.to_s
382
133
  end
383
134
  end
135
+
136
+ def int32?(value) = value.between?(-2_147_483_648, 2_147_483_647)
384
137
  end
385
138
  end
386
139
  end
@@ -2,148 +2,120 @@
2
2
 
3
3
  module HTS
4
4
  class Bcf < Hts
5
- # A class for working with VCF records.
6
5
  class Record
7
- def initialize(header, bcf_t = nil)
8
- @bcf1 = bcf_t || LibHTS.bcf_init
6
+ attr_reader :header
7
+
8
+ def initialize(header, native_record = nil)
9
+ @native = native_record || Native::BcfRecordHandle.create
9
10
  @header = header
10
11
  end
11
12
 
12
- attr_reader :header
13
+ def rid = @native.core_get(:rid)
13
14
 
14
- def struct
15
- @bcf1
15
+ def rid=(value)
16
+ @native.core_set(:rid, value)
16
17
  end
17
18
 
18
- def to_ptr
19
- @bcf1.to_ptr
20
- end
19
+ def chrom = @header.id2name(rid)
20
+ def pos = @native.core_get(:pos)
21
21
 
22
- # Get the reference id of the record.
23
- def rid
24
- @bcf1[:rid]
22
+ def pos=(value)
23
+ @native.core_set(:pos, value)
25
24
  end
26
25
 
27
- def rid=(rid)
28
- @bcf1[:rid] = rid
29
- end
26
+ def endpos = pos + @native.core_get(:rlen)
27
+ def id = @native.id
30
28
 
31
- # Get the chromosome of variant.
32
- def chrom
33
- LibHTS.bcf_hdr_id2name(@header.struct, rid)
29
+ def id=(value)
30
+ check_update_rc!(@native.set_id(@header.__send__(:native_handle), value), "ID", value)
31
+ value
34
32
  end
35
33
 
36
- # Return 0-based position.
37
- def pos
38
- @bcf1[:pos]
34
+ def clear_id
35
+ check_update_rc!(@native.set_id(@header.__send__(:native_handle), "."), "ID", ".")
36
+ nil
39
37
  end
40
38
 
41
- def pos=(pos)
42
- @bcf1[:pos] = pos
43
- end
39
+ def alleles = @native.alleles
40
+ def ref = alleles.first
41
+ def alt = alleles.drop(1)
42
+ def allele_count = alleles.length
44
43
 
45
- # Return the 0-based, exclusive end position
46
- def endpos
47
- pos + @bcf1[:rlen]
48
- end
44
+ def alleles=(values)
45
+ values = Array(values).map(&:to_s)
46
+ raise ArgumentError, "at least one allele is required" if values.empty?
49
47
 
50
- # Return the value of the ID column.
51
- def id
52
- LibHTS.bcf_unpack(@bcf1, LibHTS::BCF_UN_INFO)
53
- @bcf1[:d][:id]
48
+ encoded = values.join(",")
49
+ check_update_rc!(@native.set_alleles(@header.__send__(:native_handle), encoded), "alleles", encoded)
50
+ values
54
51
  end
55
52
 
56
- def id=(id)
57
- LibHTS.bcf_update_id(@header.struct, @bcf1, id)
58
- end
53
+ # Native migration replacement for the former pointer-yielding API.
54
+ def each_allele_raw
55
+ return to_enum(__method__) unless block_given?
59
56
 
60
- def clear_id
61
- LibHTS.bcf_update_id(@header.struct, @bcf1, ".")
57
+ alleles.each { |allele| yield allele, allele.bytesize }
58
+ self
62
59
  end
63
60
 
64
- def ref
65
- LibHTS.bcf_unpack(@bcf1, LibHTS::BCF_UN_STR)
66
- @bcf1[:d][:allele].get_pointer(0).read_string
67
- end
61
+ def qual = @native.core_get(:qual)
68
62
 
69
- def alt
70
- LibHTS.bcf_unpack(@bcf1, LibHTS::BCF_UN_STR)
71
- @bcf1[:d][:allele].get_array_of_pointer(
72
- FFI::TYPE_POINTER.size, @bcf1[:n_allele] - 1
73
- ).map(&:read_string)
63
+ def qual=(value)
64
+ @native.core_set(:qual, value)
74
65
  end
75
66
 
76
- def alleles
77
- LibHTS.bcf_unpack(@bcf1, LibHTS::BCF_UN_STR)
78
- @bcf1[:d][:allele].get_array_of_pointer(
79
- 0, @bcf1[:n_allele]
80
- ).map(&:read_string)
67
+ def filters
68
+ names = @native.filter_names(@header.__send__(:native_handle))
69
+ names
81
70
  end
82
71
 
83
- # Get variant quality.
84
- def qual
85
- @bcf1[:qual]
86
- end
87
72
 
88
- def qual=(qual)
89
- @bcf1[:qual] = qual
90
- end
73
+ # VCF FILTER is a list even when it contains a single value. Keeping a
74
+ # stable return type avoids substring matching for single-filter records.
75
+ alias filter filters
76
+
77
+ def passed? = filters == ["PASS"]
78
+ def filter_missing? = filters.empty?
79
+ def filtered? = !passed? && !filter_missing?
80
+
81
+ def each_filter_id(&block)
82
+ return to_enum(__method__) unless block_given?
91
83
 
92
- def filter
93
- LibHTS.bcf_unpack(@bcf1, LibHTS::BCF_UN_FLT)
94
- d = @bcf1[:d]
95
- n_flt = d[:n_flt]
96
-
97
- case n_flt
98
- when 0
99
- "PASS"
100
- when 1
101
- id = d[:flt].read_int
102
- LibHTS.bcf_hdr_int2id(@header.struct, LibHTS::BCF_DT_ID, id)
103
- when 2..
104
- d[:flt].get_array_of_int(0, n_flt).map do |i|
105
- LibHTS.bcf_hdr_int2id(@header.struct, LibHTS::BCF_DT_ID, i)
106
- end
107
- else
108
- raise "Unexpected number of filters. n_flt: #{n_flt}"
109
- end
84
+ @native.filter_ids.each(&block)
85
+ self
110
86
  end
111
87
 
88
+ def filter_ids = @native.filter_ids
89
+ def filter_id?(target_id) = filter_ids.include?(Integer(target_id))
90
+
112
91
  def info(key = nil)
113
- LibHTS.bcf_unpack(@bcf1, LibHTS::BCF_UN_SHR)
114
- info = Info.new(self)
115
- if key
116
- info.get(key)
117
- else
118
- info
119
- end
92
+ accessor = (@info_accessor ||= Info.new(self))
93
+ key ? accessor.get(key) : accessor
120
94
  end
121
95
 
122
96
  def format(key = nil)
123
- LibHTS.bcf_unpack(@bcf1, LibHTS::BCF_UN_FMT)
124
- if key
125
- Format.new(self).get(key)
126
- else
127
- Format.new(self)
128
- end
97
+ accessor = (@format_accessor ||= Format.new(self))
98
+ key ? accessor.get(key) : accessor
129
99
  end
130
100
 
131
- def to_s
132
- ksr = LibHTS::KString.new
133
- begin
134
- raise "Failed to format record" if LibHTS.vcf_format(@header.struct, @bcf1, ksr) == -1
101
+ def to_s = @native.format_record(@header.__send__(:native_handle))
102
+
103
+ private
135
104
 
136
- ksr.read_string_copy
137
- ensure
138
- ksr.free_buffer
139
- end
105
+ def check_update_rc!(result, field, value)
106
+ return result unless result.negative?
107
+
108
+ raise RecordError, "Failed to update #{field} to #{value.inspect} (HTSlib error #{result})"
140
109
  end
141
110
 
142
- private
111
+ def native_handle = @native
143
112
 
144
- def initialize_copy(orig)
145
- @header = orig.header
146
- @bcf1 = LibHTS.bcf_dup(orig.struct)
113
+ def initialize_copy(original)
114
+ super
115
+ @header = original.header
116
+ @native = original.__send__(:native_handle).duplicate
117
+ @info_accessor = nil
118
+ @format_accessor = nil
147
119
  end
148
120
  end
149
121
  end