maxmind-db-rust 0.1.4 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61cf32b3d220486ccb8ab6fe37c1c168a63f29f311343ccba35d28d749c6e757
4
- data.tar.gz: 57340f0580f43f022581e9183ccc01382407bfc086a0eda229fdb1fd10c4051c
3
+ metadata.gz: 662443bf974af98c823cd6eb43f5c898c7d7167329fc996dfd5df947bef78913
4
+ data.tar.gz: 0e69e3734ec1d65f26c2a0e6e9aaa8e6ca7d1560812351e81470681db2d89d69
5
5
  SHA512:
6
- metadata.gz: f8250f3c0af8f5b309c12d37ae5257bd29d0568cf6cd216093b6e703d309523052f76d0b82f62d8028441bd5c1b10c4fec279e5e97804a9c211fb340dbe034e9
7
- data.tar.gz: 2c31553fb7650e2e037638fbcda02c931da6ff4bec59e01b1d5b71dfd9fa09692daf3b6b6ad286fd1056f04ab927aaa87f93460887cda0898e104e863571ca0d
6
+ metadata.gz: c417f7b5bc40b5af80e8976afecf0fa557d2e6b9879ab1c115163cefbe6f5f3db58e71a2af271a961d4fbdf7dd39eecb5eb184356ae3ea800f1de90ff45edc0f
7
+ data.tar.gz: ec18124df44c2991265cf043bd63e6bc512564b11baca819c9b6eedf21bbacf4ba24d0864a2202a3f23a14d4782ba32e3da30df9fc9f5e8b829a7a37134e5af4
data/CHANGELOG.md CHANGED
@@ -5,7 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.2.0] - 2025-11-28
9
+
10
+ ### Changed
11
+
12
+ - Upgraded to the `0.27.0` release of the `maxminddb` crate.
13
+ - Expanded String interning.
9
14
 
10
15
  ## [0.1.4] - 2025-11-16
11
16
 
@@ -15,11 +15,6 @@ crate-type = ["cdylib"]
15
15
  arc-swap = "1.7"
16
16
  ipnetwork = "0.21"
17
17
  magnus = "0.8"
18
- maxminddb = { version = "0.26.0", features = ["unsafe-str-decode"] }
18
+ maxminddb = { version = "0.27", features = ["unsafe-str-decode"] }
19
19
  memmap2 = "0.9"
20
20
  serde = "1.0"
21
-
22
- [profile.release]
23
- lto = "thin"
24
- codegen-units = 1
25
- opt-level = 3
@@ -2,13 +2,13 @@
2
2
  // Ruby validates UTF-8 when we construct `RString`s, so skipping the redundant check in
3
3
  // the decoder is safe and avoids re-validating every string record twice.
4
4
  use ::maxminddb as maxminddb_crate;
5
- use arc_swap::ArcSwapOption;
5
+ use arc_swap::{ArcSwapOption, Guard};
6
6
  use ipnetwork::IpNetwork;
7
7
  use magnus::{
8
8
  error::Error, prelude::*, scan_args::get_kwargs, scan_args::scan_args, value::Lazy,
9
9
  ExceptionClass, IntoValue, RArray, RClass, RHash, RModule, RString, Symbol, Value,
10
10
  };
11
- use maxminddb_crate::{MaxMindDbError, Reader as MaxMindReader, Within, WithinItem};
11
+ use maxminddb_crate::{MaxMindDbError, Reader as MaxMindReader, Within};
12
12
  use memmap2::Mmap;
13
13
  use serde::de::{self, Deserialize, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
14
14
  use std::{
@@ -42,10 +42,100 @@ macro_rules! define_interned_keys {
42
42
  )*
43
43
 
44
44
  fn interned_key(ruby: &magnus::Ruby, key: &str) -> Option<Value> {
45
- match key {
46
- $(
47
- $str => Some(ruby.get_inner(&$const_ident).as_value()),
48
- )*
45
+ match key.len() {
46
+ 2 => match key.as_bytes() {
47
+ b"en" => Some(ruby.get_inner(&$crate::EN_KEY).as_value()),
48
+ b"es" => Some(ruby.get_inner(&$crate::ES_KEY).as_value()),
49
+ b"fr" => Some(ruby.get_inner(&$crate::FR_KEY).as_value()),
50
+ b"ja" => Some(ruby.get_inner(&$crate::JA_KEY).as_value()),
51
+ b"ru" => Some(ruby.get_inner(&$crate::RU_KEY).as_value()),
52
+ b"AF" => Some(ruby.get_inner(&$crate::AF_KEY).as_value()),
53
+ b"AN" => Some(ruby.get_inner(&$crate::AN_KEY).as_value()),
54
+ b"AS" => Some(ruby.get_inner(&$crate::AS_KEY).as_value()),
55
+ b"EU" => Some(ruby.get_inner(&$crate::EU_KEY).as_value()),
56
+ b"NA" => Some(ruby.get_inner(&$crate::NA_KEY).as_value()),
57
+ b"OC" => Some(ruby.get_inner(&$crate::OC_KEY).as_value()),
58
+ b"SA" => Some(ruby.get_inner(&$crate::SA_KEY).as_value()),
59
+ b"US" => Some(ruby.get_inner(&$crate::US_VAL).as_value()),
60
+ b"CN" => Some(ruby.get_inner(&$crate::CN_VAL).as_value()),
61
+ b"JP" => Some(ruby.get_inner(&$crate::JP_VAL).as_value()),
62
+ b"DE" => Some(ruby.get_inner(&$crate::DE_VAL).as_value()),
63
+ b"IN" => Some(ruby.get_inner(&$crate::IN_VAL).as_value()),
64
+ b"GB" => Some(ruby.get_inner(&$crate::GB_VAL).as_value()),
65
+ b"FR" => Some(ruby.get_inner(&$crate::FR_VAL).as_value()),
66
+ b"BR" => Some(ruby.get_inner(&$crate::BR_VAL).as_value()),
67
+ b"IT" => Some(ruby.get_inner(&$crate::IT_VAL).as_value()),
68
+ b"CA" => Some(ruby.get_inner(&$crate::CA_VAL).as_value()),
69
+ b"RU" => Some(ruby.get_inner(&$crate::RU_VAL).as_value()),
70
+ b"KR" => Some(ruby.get_inner(&$crate::KR_VAL).as_value()),
71
+ b"AU" => Some(ruby.get_inner(&$crate::AU_VAL).as_value()),
72
+ b"ES" => Some(ruby.get_inner(&$crate::ES_VAL).as_value()),
73
+ b"MX" => Some(ruby.get_inner(&$crate::MX_VAL).as_value()),
74
+ b"ID" => Some(ruby.get_inner(&$crate::ID_VAL).as_value()),
75
+ b"TR" => Some(ruby.get_inner(&$crate::TR_VAL).as_value()),
76
+ _ => None,
77
+ },
78
+ 4 => match key.as_bytes() {
79
+ b"city" => Some(ruby.get_inner(&$crate::CITY_KEY).as_value()),
80
+ b"code" => Some(ruby.get_inner(&$crate::CODE_KEY).as_value()),
81
+ _ => None,
82
+ },
83
+ 5 => match key.as_bytes() {
84
+ b"names" => Some(ruby.get_inner(&$crate::NAMES_KEY).as_value()),
85
+ b"pt-BR" => Some(ruby.get_inner(&$crate::PT_BR_KEY).as_value()),
86
+ b"zh-CN" => Some(ruby.get_inner(&$crate::ZH_CN_KEY).as_value()),
87
+ _ => None,
88
+ },
89
+ 6 => match key.as_bytes() {
90
+ b"postal" => Some(ruby.get_inner(&$crate::POSTAL_KEY).as_value()),
91
+ b"traits" => Some(ruby.get_inner(&$crate::TRAITS_KEY).as_value()),
92
+ _ => None,
93
+ },
94
+ 7 => match key.as_bytes() {
95
+ b"country" => Some(ruby.get_inner(&$crate::COUNTRY_KEY).as_value()),
96
+ b"network" => Some(ruby.get_inner(&$crate::NETWORK_KEY).as_value()),
97
+ _ => None,
98
+ },
99
+ 8 => match key.as_bytes() {
100
+ b"location" => Some(ruby.get_inner(&$crate::LOCATION_KEY).as_value()),
101
+ b"iso_code" => Some(ruby.get_inner(&$crate::ISO_CODE_KEY).as_value()),
102
+ b"latitude" => Some(ruby.get_inner(&$crate::LATITUDE_KEY).as_value()),
103
+ _ => None,
104
+ },
105
+ 9 => match key.as_bytes() {
106
+ b"continent" => Some(ruby.get_inner(&$crate::CONTINENT_KEY).as_value()),
107
+ b"longitude" => Some(ruby.get_inner(&$crate::LONGITUDE_KEY).as_value()),
108
+ b"time_zone" => Some(ruby.get_inner(&$crate::TIME_ZONE_KEY).as_value()),
109
+ _ => None,
110
+ },
111
+ 10 => match key.as_bytes() {
112
+ b"geoname_id" => Some(ruby.get_inner(&$crate::GEONAME_ID_KEY).as_value()),
113
+ b"metro_code" => Some(ruby.get_inner(&$crate::METRO_CODE_KEY).as_value()),
114
+ b"confidence" => Some(ruby.get_inner(&$crate::CONFIDENCE_KEY).as_value()),
115
+ _ => None,
116
+ },
117
+ 12 => match key.as_bytes() {
118
+ b"subdivisions" => Some(ruby.get_inner(&$crate::SUBDIVISIONS_KEY).as_value()),
119
+ _ => None,
120
+ },
121
+ 15 => match key.as_bytes() {
122
+ b"accuracy_radius" => Some(ruby.get_inner(&$crate::ACCURACY_RADIUS_KEY).as_value()),
123
+ _ => None,
124
+ },
125
+ 18 => match key.as_bytes() {
126
+ b"registered_country" => Some(ruby.get_inner(&$crate::REGISTERED_COUNTRY_KEY).as_value()),
127
+ b"population_density" => Some(ruby.get_inner(&$crate::POPULATION_DENSITY_KEY).as_value()),
128
+ _ => None,
129
+ },
130
+ 19 => match key.as_bytes() {
131
+ b"represented_country" => Some(ruby.get_inner(&$crate::REPRESENTED_COUNTRY_KEY).as_value()),
132
+ b"is_anonymous_proxy" => Some(ruby.get_inner(&$crate::IS_ANONYMOUS_PROXY_KEY).as_value()),
133
+ _ => None,
134
+ },
135
+ 21 => match key.as_bytes() {
136
+ b"is_satellite_provider" => Some(ruby.get_inner(&$crate::IS_SATELLITE_PROVIDER_KEY).as_value()),
137
+ _ => None,
138
+ },
49
139
  _ => None,
50
140
  }
51
141
  }
@@ -79,6 +169,37 @@ define_interned_keys!(
79
169
  PT_BR_KEY => "pt-BR",
80
170
  RU_KEY => "ru",
81
171
  ZH_CN_KEY => "zh-CN",
172
+ // Common keys
173
+ CODE_KEY => "code",
174
+ NETWORK_KEY => "network",
175
+ IS_ANONYMOUS_PROXY_KEY => "is_anonymous_proxy",
176
+ IS_SATELLITE_PROVIDER_KEY => "is_satellite_provider",
177
+ // Continent codes
178
+ AF_KEY => "AF",
179
+ AN_KEY => "AN",
180
+ AS_KEY => "AS",
181
+ EU_KEY => "EU",
182
+ NA_KEY => "NA",
183
+ OC_KEY => "OC",
184
+ SA_KEY => "SA",
185
+ // Major Country ISO codes
186
+ US_VAL => "US",
187
+ CN_VAL => "CN",
188
+ JP_VAL => "JP",
189
+ DE_VAL => "DE",
190
+ IN_VAL => "IN",
191
+ GB_VAL => "GB",
192
+ FR_VAL => "FR",
193
+ BR_VAL => "BR",
194
+ IT_VAL => "IT",
195
+ CA_VAL => "CA",
196
+ RU_VAL => "RU", // Already defined above as RU_KEY? No, RU_KEY is "ru" (lang), this is "RU" (country)
197
+ KR_VAL => "KR",
198
+ AU_VAL => "AU",
199
+ ES_VAL => "ES", // "ES" (country) vs "es" (lang). ES_KEY is "es".
200
+ MX_VAL => "MX",
201
+ ID_VAL => "ID",
202
+ TR_VAL => "TR",
82
203
  );
83
204
 
84
205
  /// Wrapper that owns the Ruby value produced by deserializing a MaxMind record
@@ -289,8 +410,8 @@ impl ReaderSource {
289
410
  ip: IpAddr,
290
411
  ) -> Result<Option<RubyDecodedValue>, maxminddb_crate::MaxMindDbError> {
291
412
  match self {
292
- ReaderSource::Mmap(reader) => reader.lookup(ip),
293
- ReaderSource::Memory(reader) => reader.lookup(ip),
413
+ ReaderSource::Mmap(reader) => reader.lookup(ip)?.decode(),
414
+ ReaderSource::Memory(reader) => reader.lookup(ip)?.decode(),
294
415
  }
295
416
  }
296
417
 
@@ -299,10 +420,35 @@ impl ReaderSource {
299
420
  &self,
300
421
  ip: IpAddr,
301
422
  ) -> Result<(Option<RubyDecodedValue>, usize), maxminddb_crate::MaxMindDbError> {
302
- match self {
303
- ReaderSource::Mmap(reader) => reader.lookup_prefix(ip),
304
- ReaderSource::Memory(reader) => reader.lookup_prefix(ip),
305
- }
423
+ let (result, prefix_len) = match self {
424
+ ReaderSource::Mmap(reader) => {
425
+ let result = reader.lookup(ip)?;
426
+ let network = result.network()?;
427
+ let prefix = network.prefix();
428
+
429
+ let prefix_len = if ip.is_ipv4() && network.is_ipv6() {
430
+ 0
431
+ } else {
432
+ prefix as usize
433
+ };
434
+
435
+ (result.decode()?, prefix_len)
436
+ }
437
+ ReaderSource::Memory(reader) => {
438
+ let result = reader.lookup(ip)?;
439
+ let network = result.network()?;
440
+ let prefix = network.prefix();
441
+
442
+ let prefix_len = if ip.is_ipv4() && network.is_ipv6() {
443
+ 0
444
+ } else {
445
+ prefix as usize
446
+ };
447
+
448
+ (result.decode()?, prefix_len)
449
+ }
450
+ };
451
+ Ok((result, prefix_len))
306
452
  }
307
453
 
308
454
  #[inline]
@@ -317,24 +463,18 @@ impl ReaderSource {
317
463
  fn within(&self, network: IpNetwork) -> Result<ReaderWithin, MaxMindDbError> {
318
464
  match self {
319
465
  ReaderSource::Mmap(reader) => {
320
- let iter = reader.within::<RubyDecodedValue>(network)?;
466
+ let iter = reader.within(network, Default::default())?;
321
467
  // SAFETY: the iterator holds a reference into `reader`. We'll store an Arc guard
322
468
  // alongside it so the reader outlives the transmuted iterator.
323
469
  Ok(ReaderWithin::Mmap(unsafe {
324
- std::mem::transmute::<
325
- Within<'_, RubyDecodedValue, Mmap>,
326
- Within<'static, RubyDecodedValue, Mmap>,
327
- >(iter)
470
+ std::mem::transmute::<Within<'_, Mmap>, Within<'static, Mmap>>(iter)
328
471
  }))
329
472
  }
330
473
  ReaderSource::Memory(reader) => {
331
- let iter = reader.within::<RubyDecodedValue>(network)?;
474
+ let iter = reader.within(network, Default::default())?;
332
475
  // SAFETY: same as above, the Arc guard keeps the reader alive.
333
476
  Ok(ReaderWithin::Memory(unsafe {
334
- std::mem::transmute::<
335
- Within<'_, RubyDecodedValue, Vec<u8>>,
336
- Within<'static, RubyDecodedValue, Vec<u8>>,
337
- >(iter)
477
+ std::mem::transmute::<Within<'_, Vec<u8>>, Within<'static, Vec<u8>>>(iter)
338
478
  }))
339
479
  }
340
480
  }
@@ -343,15 +483,47 @@ impl ReaderSource {
343
483
 
344
484
  /// Wrapper enum for Within iterators
345
485
  enum ReaderWithin {
346
- Mmap(Within<'static, RubyDecodedValue, Mmap>),
347
- Memory(Within<'static, RubyDecodedValue, Vec<u8>>),
486
+ Mmap(Within<'static, Mmap>),
487
+ Memory(Within<'static, Vec<u8>>),
348
488
  }
349
489
 
350
490
  impl ReaderWithin {
351
- fn next(&mut self) -> Option<Result<WithinItem<RubyDecodedValue>, MaxMindDbError>> {
491
+ fn next(&mut self) -> Option<Result<(IpNetwork, RubyDecodedValue), MaxMindDbError>> {
352
492
  match self {
353
- ReaderWithin::Mmap(iter) => iter.next(),
354
- ReaderWithin::Memory(iter) => iter.next(),
493
+ ReaderWithin::Mmap(iter) => loop {
494
+ match iter.next() {
495
+ None => return None,
496
+ Some(Err(e)) => return Some(Err(e)),
497
+ Some(Ok(lookup_result)) => {
498
+ let network = match lookup_result.network() {
499
+ Ok(n) => n,
500
+ Err(e) => return Some(Err(e)),
501
+ };
502
+ match lookup_result.decode::<RubyDecodedValue>() {
503
+ Ok(Some(data)) => return Some(Ok((network, data))),
504
+ Ok(None) => continue, // Skip networks without data
505
+ Err(e) => return Some(Err(e)),
506
+ }
507
+ }
508
+ }
509
+ },
510
+ ReaderWithin::Memory(iter) => loop {
511
+ match iter.next() {
512
+ None => return None,
513
+ Some(Err(e)) => return Some(Err(e)),
514
+ Some(Ok(lookup_result)) => {
515
+ let network = match lookup_result.network() {
516
+ Ok(n) => n,
517
+ Err(e) => return Some(Err(e)),
518
+ };
519
+ match lookup_result.decode::<RubyDecodedValue>() {
520
+ Ok(Some(data)) => return Some(Ok((network, data))),
521
+ Ok(None) => continue, // Skip networks without data
522
+ Err(e) => return Some(Err(e)),
523
+ }
524
+ }
525
+ }
526
+ },
355
527
  }
356
528
  }
357
529
  }
@@ -482,7 +654,9 @@ impl Reader {
482
654
  fn get(&self, ip_address: Value) -> Result<Value, Error> {
483
655
  let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
484
656
 
485
- let reader = self.get_reader(&ruby)?;
657
+ let guard = self.get_reader(&ruby)?;
658
+ let reader_option = guard.as_ref();
659
+ let reader = reader_option.as_ref().unwrap();
486
660
 
487
661
  // Parse IP address
488
662
  let parsed_ip = parse_ip_address_fast(ip_address, &ruby)?;
@@ -498,7 +672,7 @@ impl Reader {
498
672
  match reader.lookup(parsed_ip) {
499
673
  Ok(Some(data)) => Ok(data.into_value()),
500
674
  Ok(None) => Ok(ruby.qnil().as_value()),
501
- Err(MaxMindDbError::InvalidDatabase(_)) | Err(MaxMindDbError::Io(_)) => {
675
+ Err(MaxMindDbError::InvalidDatabase { .. }) | Err(MaxMindDbError::Io(_)) => {
502
676
  Err(Error::new(
503
677
  ExceptionClass::from_value(invalid_database_error().as_value())
504
678
  .expect("InvalidDatabaseError should convert to ExceptionClass"),
@@ -516,7 +690,9 @@ impl Reader {
516
690
  fn get_with_prefix_length(&self, ip_address: Value) -> Result<RArray, Error> {
517
691
  let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
518
692
 
519
- let reader = self.get_reader(&ruby)?;
693
+ let guard = self.get_reader(&ruby)?;
694
+ let reader_option = guard.as_ref();
695
+ let reader = reader_option.as_ref().unwrap();
520
696
 
521
697
  // Parse IP address
522
698
  let parsed_ip = parse_ip_address_fast(ip_address, &ruby)?;
@@ -542,7 +718,7 @@ impl Reader {
542
718
  arr.push(prefix.into_value_with(&ruby))?;
543
719
  Ok(arr)
544
720
  }
545
- Err(MaxMindDbError::InvalidDatabase(_)) | Err(MaxMindDbError::Io(_)) => {
721
+ Err(MaxMindDbError::InvalidDatabase { .. }) | Err(MaxMindDbError::Io(_)) => {
546
722
  Err(Error::new(
547
723
  ExceptionClass::from_value(invalid_database_error().as_value())
548
724
  .expect("InvalidDatabaseError should convert to ExceptionClass"),
@@ -559,7 +735,9 @@ impl Reader {
559
735
  fn metadata(&self) -> Result<Metadata, Error> {
560
736
  let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
561
737
 
562
- let reader = self.get_reader(&ruby)?;
738
+ let guard = self.get_reader(&ruby)?;
739
+ let reader_option = guard.as_ref();
740
+ let reader = reader_option.as_ref().unwrap();
563
741
  let meta = reader.metadata();
564
742
 
565
743
  Ok(Metadata {
@@ -589,7 +767,9 @@ impl Reader {
589
767
  fn each(&self, args: &[Value]) -> Result<Value, Error> {
590
768
  let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
591
769
 
592
- let reader = self.get_reader(&ruby)?;
770
+ let guard = self.get_reader(&ruby)?;
771
+ let reader_option = guard.as_ref();
772
+ let reader = reader_option.as_ref().unwrap();
593
773
 
594
774
  // If no block given, return enumerator
595
775
  if !ruby.block_given() {
@@ -681,16 +861,16 @@ impl Reader {
681
861
  // Iterate over all networks
682
862
  while let Some(result) = iter.next() {
683
863
  match result {
684
- Ok(item) => {
864
+ Ok((network, data)) => {
685
865
  // Convert IpNetwork to IPAddr
686
- let ip_str = item.ip_net.to_string();
866
+ let ip_str = network.to_string();
687
867
  let ipaddr = ipaddr_class.funcall::<_, _, Value>("new", (ip_str,))?;
688
868
 
689
869
  // Yield [network, data] to block
690
- let values = (ipaddr, item.info.into_value());
870
+ let values = (ipaddr, data.into_value());
691
871
  ruby.yield_values::<(Value, Value), Value>(values)?;
692
872
  }
693
- Err(MaxMindDbError::InvalidDatabase(_)) | Err(MaxMindDbError::Io(_)) => {
873
+ Err(MaxMindDbError::InvalidDatabase { .. }) | Err(MaxMindDbError::Io(_)) => {
694
874
  return Err(Error::new(
695
875
  ExceptionClass::from_value(invalid_database_error().as_value())
696
876
  .expect("InvalidDatabaseError should convert to ExceptionClass"),
@@ -710,10 +890,12 @@ impl Reader {
710
890
  }
711
891
 
712
892
  /// Helper method to get the reader from the ArcSwapOption
713
- fn get_reader(&self, ruby: &magnus::Ruby) -> Result<Arc<ReaderSource>, Error> {
714
- self.reader
715
- .load_full()
716
- .ok_or_else(|| Error::new(ruby.exception_runtime_error(), ERR_CLOSED_DB))
893
+ fn get_reader(&self, ruby: &magnus::Ruby) -> Result<Guard<Option<Arc<ReaderSource>>>, Error> {
894
+ let guard = self.reader.load();
895
+ if guard.is_none() {
896
+ return Err(Error::new(ruby.exception_runtime_error(), ERR_CLOSED_DB));
897
+ }
898
+ Ok(guard)
717
899
  }
718
900
  }
719
901
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maxmind-db-rust
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregory Oschwald
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-16 00:00:00.000000000 Z
11
+ date: 2025-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys