jiff-magnus 0.0.1
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 +7 -0
- data/.gitignore +8 -0
- data/Cargo.toml +16 -0
- data/LICENSE +21 -0
- data/README.md +182 -0
- data/extconf.rb +8 -0
- data/lib/jiff_magnus.rb +3 -0
- data/src/lib.rs +276 -0
- data/tmp/.gitignore +2 -0
- metadata +63 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a6f9c863e107b4351d7c9e56a504aaec9f428a1862174281eb4c5007461f2304
|
|
4
|
+
data.tar.gz: '039d66e2dc37e5d5aca5795019e186e430f9c006bbec093ba531ac2cce8e5ef1'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 76209b92a010871e7a910ee4f2fbf1136c668fbbcd7153ba400d039cc2dfb5e181ee2ee97173f5a90fe05eb7ba2dd9541055175917f660a203c9e5cbfbea78f2
|
|
7
|
+
data.tar.gz: 809ffdabfe974ef1561b3752bba1ef429e5a2d8669dc000575e7187edc9f0db758831c0a2a6535d688797cb484a91c560afb3ac848c9ea3b33d49cfbb83706e4
|
data/.gitignore
ADDED
data/Cargo.toml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "jiff-magnus"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
rust-version = "1.85"
|
|
6
|
+
description = "Jiff timezone constructors and bundled timezone data for Ruby Time."
|
|
7
|
+
repository = "https://github.com/matsadler/magnus"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
jiff = { version = "0.2.35", default-features = false, features = ["std", "tzdb-bundle-always"] }
|
|
12
|
+
magnus = { version = "0.10.0", default-features = false, features = ["jiff-zoned", "rb-sys"] }
|
|
13
|
+
rb-sys = { version = "0.9.124", default-features = false, features = ["stable-api-compiled-fallback"] }
|
|
14
|
+
|
|
15
|
+
[dev-dependencies]
|
|
16
|
+
magnus = { version = "0.10.0", default-features = false, features = ["embed", "jiff-zoned", "rb-sys"] }
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Erick Guan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# jiff-magnus
|
|
2
|
+
|
|
3
|
+
A Magnus companion gem that provides Jiff timezones for Ruby `Time`.
|
|
4
|
+
It bundles IANA timezone data.
|
|
5
|
+
|
|
6
|
+
## Install and use
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
gem install jiff-magnus --version 0.0.1
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
require "jiff_magnus"
|
|
14
|
+
|
|
15
|
+
zone = JiffTimezone.find_timezone("America/New_York")
|
|
16
|
+
time = Time.new(2024, 7, 1, 12, 0, 0, in: zone)
|
|
17
|
+
time.utc_offset # => -14400
|
|
18
|
+
time.dst? # => true
|
|
19
|
+
time.strftime("%Z") # => "EDT"
|
|
20
|
+
|
|
21
|
+
# By default, requiring the gem installs Time.find_timezone if absent.
|
|
22
|
+
Time.at(0, in: "Asia/Tokyo")
|
|
23
|
+
|
|
24
|
+
JiffTimezone.utc
|
|
25
|
+
JiffTimezone.fixed(19_800) # Seconds east of UTC.
|
|
26
|
+
JiffTimezone.posix("EST5EDT,M3.2.0,M11.1.0")
|
|
27
|
+
JiffTimezone.unknown
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`find_timezone` returns `nil` for unknown names. Invalid fixed offsets and POSIX
|
|
31
|
+
rules raise `ArgumentError`. Timezone objects are frozen; ordinary `new` and
|
|
32
|
+
`allocate` are disabled. `iana_name` returns `nil` when a zone has no IANA name.
|
|
33
|
+
`inspect` shows the class, timezone label, and portable representation.
|
|
34
|
+
|
|
35
|
+
Local times in DST gaps and repeated local times raise `ArgumentError`.
|
|
36
|
+
Construct from an instant to select either occurrence of an overlap:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
Time.at(Time.utc(2024, 11, 3, 5, 30).to_i, in: "America/New_York") # EDT
|
|
40
|
+
Time.at(Time.utc(2024, 11, 3, 6, 30).to_i, in: "America/New_York") # EST
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Initialization and Marshal
|
|
44
|
+
|
|
45
|
+
Requiring the gem initializes `JiffTimezone`. By default, it also defines
|
|
46
|
+
`Time.find_timezone` if absent. Existing lookup methods, including private ones,
|
|
47
|
+
are preserved. Repeated `require` calls reuse the loaded extension. An existing
|
|
48
|
+
`JiffTimezone` class is reused when it inherits from the same Magnus copy's
|
|
49
|
+
timezone class; its public methods are neither checked nor replaced.
|
|
50
|
+
|
|
51
|
+
To skip automatic `Time.find_timezone` registration, set
|
|
52
|
+
`JIFF_MAGNUS_ENABLE_AUTO_TIME_REGISTRATION` to `0` or `false` before requiring
|
|
53
|
+
the gem. `false` is case-insensitive; other values and an unset variable enable
|
|
54
|
+
automatic registration. This setting is read on the first initialization.
|
|
55
|
+
Timezone constructors and Magnus conversions remain available.
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
ENV["JIFF_MAGNUS_ENABLE_AUTO_TIME_REGISTRATION"] = "0"
|
|
59
|
+
require "jiff_magnus"
|
|
60
|
+
|
|
61
|
+
zone = JiffTimezone.find_timezone("Asia/Tokyo")
|
|
62
|
+
Time.at(0, in: zone)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
To explicitly install or replace name lookup with Jiff, even with automatic
|
|
66
|
+
registration disabled:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
JiffTimezone.register
|
|
70
|
+
Time.at(0, in: "Asia/Tokyo")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Rust callers can use `jiff_magnus::register(ruby)` for the same operation.
|
|
74
|
+
Registration makes `Time.find_timezone` public. If replacement fails, the
|
|
75
|
+
previous method and its visibility are restored.
|
|
76
|
+
|
|
77
|
+
An unrelated class or a class from another Magnus build raises `LoadError`.
|
|
78
|
+
An unrelated `TimeZone` constant does not conflict. Repeated native
|
|
79
|
+
initialization raises `LoadError` if the cached class or a tracked lookup method
|
|
80
|
+
was removed or replaced unexpectedly. Explicit registration updates that cached
|
|
81
|
+
method. Reentrant initialization or registration raises `LoadError`; the outer
|
|
82
|
+
operation can roll back and be retried. A failed initialization removes only
|
|
83
|
+
definitions it added, preserving application definitions created by Ruby hooks.
|
|
84
|
+
Failed resolver replacement also preserves an explicitly disabled inherited
|
|
85
|
+
lookup.
|
|
86
|
+
|
|
87
|
+
Require the gem before loading marshalled named-zone times. Native fixed-offset
|
|
88
|
+
times need no resolver. POSIX, unknown, and explicitly wrapped fixed-offset
|
|
89
|
+
zones have no restorable IANA name; dumping times with those objects raises
|
|
90
|
+
`TypeError`.
|
|
91
|
+
|
|
92
|
+
Magnus owns Rust conversions, timezone storage, and native interoperability.
|
|
93
|
+
See its [Jiff timezone documentation](../docs/jiff-timezones.md) for conversion
|
|
94
|
+
behavior, internal objects, and limits when extensions use different builds.
|
|
95
|
+
The companion owns public constructors, lookup, and initialization.
|
|
96
|
+
|
|
97
|
+
## Build and test from source
|
|
98
|
+
|
|
99
|
+
Use Ruby 3.2 or later, Rust 1.85 or later, a C compiler, and libclang.
|
|
100
|
+
From this directory, run:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
bundle install
|
|
104
|
+
bundle exec rake
|
|
105
|
+
bundle exec rake build
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The checkout builds against the current Magnus source through `path = ".."`.
|
|
109
|
+
The default task runs Standard and the Ruby tests. Run `bundle exec rake standard`
|
|
110
|
+
to lint separately. A local Standard extension requires parentheses on `assert`
|
|
111
|
+
and `refute` calls in the Ruby tests; `bundle exec rake standard:fix` adds them.
|
|
112
|
+
The test task builds the native extension and exercises
|
|
113
|
+
`require`, timezone behavior, and Marshal across processes. The build task creates
|
|
114
|
+
the source gem under `pkg/`; distributed source gems resolve their declared
|
|
115
|
+
Magnus dependency from crates.io. The workspace uses its root Cargo lockfile.
|
|
116
|
+
Rake keeps build intermediates and temporary packaging files under `tmp/`.
|
|
117
|
+
Run `bundle exec rake clobber` to remove them and the generated packages while
|
|
118
|
+
keeping `tmp/.gitignore`.
|
|
119
|
+
|
|
120
|
+
After the required Magnus version is published, you can manually check an
|
|
121
|
+
installed source gem from the repository root:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
ruby docs/examples/jiff_magnus_install.rb jiff-magnus/pkg/jiff-magnus-0.0.1.gem
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Local releases
|
|
128
|
+
|
|
129
|
+
Bundler's gem tasks provide `build`, `install`, and `release`, as in `ffi-icu`.
|
|
130
|
+
The build task uses our staging step to remove checkout-only Cargo paths from
|
|
131
|
+
the source gem. The version comes from `jiff-magnus/Cargo.toml`.
|
|
132
|
+
|
|
133
|
+
After the required Magnus version is published, run from this directory on a
|
|
134
|
+
clean Git branch with working push credentials:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
gem signin
|
|
138
|
+
bundle exec rake release
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The release task runs Standard, the Ruby tests, and an installed-source-gem
|
|
142
|
+
check. It then creates a `jiff-magnus-v<version>` tag, pushes the current branch
|
|
143
|
+
and tag to `origin`, and publishes the gem using your local RubyGems credentials.
|
|
144
|
+
The tag prefix keeps companion releases separate from Magnus releases. Use
|
|
145
|
+
`bundle exec rake release:check` to run validation without tagging or publishing.
|
|
146
|
+
|
|
147
|
+
Bundler's full release task expects an attached Git branch. From a Jujutsu
|
|
148
|
+
checkout with detached Git HEAD, manage the bookmark and release tag separately,
|
|
149
|
+
then publish the committed revision with:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
bundle exec rake release:rubygem_push
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This task runs the same validation and clean-working-copy check, then publishes
|
|
156
|
+
without changing Git references. Local publishing needs no GitHub API-key secret.
|
|
157
|
+
|
|
158
|
+
## GitHub Actions
|
|
159
|
+
|
|
160
|
+
The [lint workflow](../.github/workflows/jiff-magnus-lint.yml) checks Standard,
|
|
161
|
+
Rust formatting, and workflow YAML. The
|
|
162
|
+
[test workflow](../.github/workflows/jiff-magnus-test.yml) runs the Ruby suite
|
|
163
|
+
and builds the source gem on Ruby 3.2 through 4.0 and Ruby head on Linux, plus
|
|
164
|
+
Ruby 3.4 and 4.0 on macOS and Ruby 4.0 on Windows. Magnus's existing Test workflow
|
|
165
|
+
runs the Rust workspace tests.
|
|
166
|
+
|
|
167
|
+
Run the [release workflow](../.github/workflows/jiff-magnus-release.yml) manually
|
|
168
|
+
from the branch or tag you want to publish. It uses the version in
|
|
169
|
+
`jiff-magnus/Cargo.toml`, lints and tests the extension, builds the source gem,
|
|
170
|
+
checks installation against published Rust dependencies, and publishes it to
|
|
171
|
+
RubyGems with the `RUBYGEMS_API_KEY` repository secret.
|
|
172
|
+
|
|
173
|
+
Before the first release, create a RubyGems API key with the
|
|
174
|
+
[Push rubygems scope](https://guides.rubygems.org/api-key-scopes/) and save it as
|
|
175
|
+
the repository's `RUBYGEMS_API_KEY` Actions secret. The workflow supplies it to
|
|
176
|
+
`gem push` through `GEM_HOST_API_KEY`, which RubyGems supports for CI publishing.
|
|
177
|
+
Choose key settings that allow unattended pushes; a key that requires an OTP
|
|
178
|
+
will still require that code. Trusted publishing is an optional later change.
|
|
179
|
+
|
|
180
|
+
The required Magnus crate with `jiff-zoned` must be available on crates.io
|
|
181
|
+
before the installation check can pass. This workflow publishes the Ruby gem;
|
|
182
|
+
release the required Rust crates separately.
|
data/extconf.rb
ADDED
data/lib/jiff_magnus.rb
ADDED
data/src/lib.rs
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
//! Ruby constructors for Jiff timezones, using Magnus's native backing type.
|
|
2
|
+
//!
|
|
3
|
+
//! `require "jiff_magnus"` calls [`init`] and bundles IANA timezone data.
|
|
4
|
+
//! The gem subclasses Magnus's [`JiffTimeZone`] to reuse its timezone methods.
|
|
5
|
+
//! Core [`jiff::Zoned`] conversions work before this gem is initialized.
|
|
6
|
+
|
|
7
|
+
use std::sync::{Mutex, OnceLock};
|
|
8
|
+
|
|
9
|
+
use jiff::tz::TimeZone;
|
|
10
|
+
use magnus::{
|
|
11
|
+
Error, RArray, RClass, Ruby, TypedData, Value, prelude::*, rb_sys::AsRawValue,
|
|
12
|
+
time::JiffTimeZone, typed_data::Obj, value::Opaque,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const CLASS_NAME: &str = "JiffTimezone";
|
|
16
|
+
const AUTO_TIME_REGISTRATION_ENV: &str = "JIFF_MAGNUS_ENABLE_AUTO_TIME_REGISTRATION";
|
|
17
|
+
|
|
18
|
+
struct Initialization {
|
|
19
|
+
class: Opaque<RClass>,
|
|
20
|
+
resolver: Opaque<RArray>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl Initialization {
|
|
24
|
+
fn validate_class(&self, ruby: &Ruby) -> Result<(), Error> {
|
|
25
|
+
let object = ruby.class_object();
|
|
26
|
+
if !object.funcall::<_, _, bool>("const_defined?", (CLASS_NAME, false))? {
|
|
27
|
+
return Err(conflict(ruby, "JiffTimezone initialization was removed"));
|
|
28
|
+
}
|
|
29
|
+
let class: Value = object.const_get(CLASS_NAME)?;
|
|
30
|
+
if class.as_raw() != ruby.get_inner(self.class).as_raw() {
|
|
31
|
+
return Err(conflict(ruby, "JiffTimezone initialization was replaced"));
|
|
32
|
+
}
|
|
33
|
+
Ok(())
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static INITIALIZATION: OnceLock<Initialization> = OnceLock::new();
|
|
38
|
+
// Ruby definition hooks can reenter either public entry point while it publishes
|
|
39
|
+
// or restores definitions. Reject those calls before they can commit the cache.
|
|
40
|
+
static INITIALIZATION_LOCK: Mutex<()> = Mutex::new(());
|
|
41
|
+
|
|
42
|
+
/// Initializes `JiffTimezone` with optional automatic `Time.find_timezone` lookup.
|
|
43
|
+
///
|
|
44
|
+
/// When an existing `JiffTimezone` inherits from this Magnus copy's timezone
|
|
45
|
+
/// class, this function reuses it and leaves its public methods untouched. It
|
|
46
|
+
/// preserves an existing `Time.find_timezone`, even if the method is private.
|
|
47
|
+
/// Otherwise, it adds lookup unless you set
|
|
48
|
+
/// `JIFF_MAGNUS_ENABLE_AUTO_TIME_REGISTRATION` to `0` or `false`
|
|
49
|
+
/// (case-insensitive) before initialization. Constructors remain available
|
|
50
|
+
/// without automatic lookup. Use [`register`] to explicitly install or replace
|
|
51
|
+
/// the method regardless of this setting.
|
|
52
|
+
///
|
|
53
|
+
/// # Errors
|
|
54
|
+
///
|
|
55
|
+
/// We return Ruby `LoadError` in these cases:
|
|
56
|
+
///
|
|
57
|
+
/// - An existing `Object::JiffTimezone` belongs to a different native
|
|
58
|
+
/// implementation.
|
|
59
|
+
/// - Ruby code removed or replaced `JiffTimezone` or a tracked
|
|
60
|
+
/// `Time.find_timezone` method after initialization.
|
|
61
|
+
/// - Initialization or registration is already in progress.
|
|
62
|
+
///
|
|
63
|
+
/// Ruby allocation and method errors propagate. If publishing the class or
|
|
64
|
+
/// resolver fails, this function rolls back only definitions it added.
|
|
65
|
+
#[cfg_attr(not(test), magnus::init(name = "jiff_magnus"))]
|
|
66
|
+
pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
67
|
+
let _guard = INITIALIZATION_LOCK.try_lock().map_err(|_| {
|
|
68
|
+
conflict(
|
|
69
|
+
ruby,
|
|
70
|
+
"timezone initialization or registration is already in progress",
|
|
71
|
+
)
|
|
72
|
+
})?;
|
|
73
|
+
if let Some(existing) = INITIALIZATION.get() {
|
|
74
|
+
existing.validate_class(ruby)?;
|
|
75
|
+
let expected: Option<Value> = ruby.get_inner(existing.resolver).entry(0)?;
|
|
76
|
+
if let Some(expected) = expected {
|
|
77
|
+
let time = ruby.class_time();
|
|
78
|
+
if !time.respond_to("find_timezone", true)? {
|
|
79
|
+
return Err(conflict(ruby, "Time.find_timezone was removed"));
|
|
80
|
+
}
|
|
81
|
+
let method: Value = time.funcall("method", ("find_timezone",))?;
|
|
82
|
+
if !method.funcall::<_, _, bool>("==", (expected,))? {
|
|
83
|
+
return Err(conflict(ruby, "Time.find_timezone was replaced"));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return Ok(());
|
|
87
|
+
}
|
|
88
|
+
let object = ruby.class_object();
|
|
89
|
+
let env: Value = object.const_get("ENV")?;
|
|
90
|
+
let setting: Option<String> = env.funcall("[]", (AUTO_TIME_REGISTRATION_ENV,))?;
|
|
91
|
+
let auto_register = !setting
|
|
92
|
+
.as_deref()
|
|
93
|
+
.is_some_and(|value| value == "0" || value.eq_ignore_ascii_case("false"));
|
|
94
|
+
let existing_class = if object.funcall::<_, _, bool>("const_defined?", (CLASS_NAME, false))? {
|
|
95
|
+
let value: Value = object.const_get(CLASS_NAME)?;
|
|
96
|
+
let class = RClass::from_value(value)
|
|
97
|
+
.filter(|class| class.is_inherited(JiffTimeZone::class(ruby)))
|
|
98
|
+
.ok_or_else(|| {
|
|
99
|
+
conflict(
|
|
100
|
+
ruby,
|
|
101
|
+
"JiffTimezone is already defined by an incompatible native implementation",
|
|
102
|
+
)
|
|
103
|
+
})?;
|
|
104
|
+
Some(class)
|
|
105
|
+
} else {
|
|
106
|
+
None
|
|
107
|
+
};
|
|
108
|
+
let time = ruby.class_time();
|
|
109
|
+
|
|
110
|
+
// Configure a new class before publishing it; never modify a reused class.
|
|
111
|
+
let class = if let Some(class) = existing_class {
|
|
112
|
+
class
|
|
113
|
+
} else {
|
|
114
|
+
// Reuse Magnus's TypedData instead of introducing a second native payload.
|
|
115
|
+
let class = RClass::new(JiffTimeZone::class(ruby))?;
|
|
116
|
+
class.undef_default_alloc_func();
|
|
117
|
+
class.define_singleton_method("find_timezone", magnus::function!(find_timezone, 1))?;
|
|
118
|
+
class.define_singleton_method("utc", magnus::function!(utc, 0))?;
|
|
119
|
+
class.define_singleton_method("unknown", magnus::function!(unknown, 0))?;
|
|
120
|
+
class.define_singleton_method("fixed", magnus::function!(fixed, 1))?;
|
|
121
|
+
class.define_singleton_method("posix", magnus::function!(posix, 1))?;
|
|
122
|
+
class.define_singleton_method("register", magnus::function!(register, 0))?;
|
|
123
|
+
class.freeze();
|
|
124
|
+
class
|
|
125
|
+
};
|
|
126
|
+
let resolver = (|| -> Result<Value, Error> {
|
|
127
|
+
if existing_class.is_none() {
|
|
128
|
+
object.const_set(CLASS_NAME, class)?;
|
|
129
|
+
}
|
|
130
|
+
if !auto_register {
|
|
131
|
+
Ok(ruby.qnil().as_value())
|
|
132
|
+
} else if time.respond_to("find_timezone", true)? {
|
|
133
|
+
time.funcall("method", ("find_timezone",))
|
|
134
|
+
} else {
|
|
135
|
+
install_resolver(ruby)
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
let resolver = match resolver {
|
|
139
|
+
Ok(resolver) => resolver,
|
|
140
|
+
Err(error) => {
|
|
141
|
+
// The resolver installer rolls back its own changes. A class hook
|
|
142
|
+
// may instead have installed application definitions before raising.
|
|
143
|
+
if existing_class.is_none() {
|
|
144
|
+
let _ = (|| -> Result<(), Error> {
|
|
145
|
+
if object.funcall::<_, _, bool>("const_defined?", (CLASS_NAME, false))? {
|
|
146
|
+
let current: Value = object.const_get(CLASS_NAME)?;
|
|
147
|
+
if current.as_raw() == class.as_raw() {
|
|
148
|
+
object.funcall::<_, _, Value>("remove_const", (CLASS_NAME,))?;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
Ok(())
|
|
152
|
+
})();
|
|
153
|
+
}
|
|
154
|
+
return Err(error);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
// A Ruby holder keeps the current method alive without retaining every
|
|
158
|
+
// method replaced by explicit registration.
|
|
159
|
+
let resolver = ruby.ary_new_from_values(&[resolver]);
|
|
160
|
+
ruby.gc_register_mark_object(class);
|
|
161
|
+
ruby.gc_register_mark_object(resolver);
|
|
162
|
+
INITIALIZATION
|
|
163
|
+
.set(Initialization {
|
|
164
|
+
class: class.into(),
|
|
165
|
+
resolver: resolver.into(),
|
|
166
|
+
})
|
|
167
|
+
.map_err(|_| conflict(ruby, "timezone initialization is already in progress"))?;
|
|
168
|
+
Ok(())
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Replaces `Time.find_timezone` with this companion's Jiff name lookup.
|
|
172
|
+
///
|
|
173
|
+
/// Unlike [`init`], this explicitly overrides an existing method, including a
|
|
174
|
+
/// private one, regardless of `JIFF_MAGNUS_ENABLE_AUTO_TIME_REGISTRATION`.
|
|
175
|
+
/// Ruby callers use `JiffTimezone.register`. Calling this before initialization
|
|
176
|
+
/// first initializes the companion.
|
|
177
|
+
///
|
|
178
|
+
/// # Errors
|
|
179
|
+
///
|
|
180
|
+
/// Returns `LoadError` if Ruby code removed or replaced the initialized class,
|
|
181
|
+
/// or if initialization or registration is already in progress.
|
|
182
|
+
/// Ruby errors propagate; a failed replacement restores the previous method.
|
|
183
|
+
pub fn register(ruby: &Ruby) -> Result<(), Error> {
|
|
184
|
+
if INITIALIZATION.get().is_none() {
|
|
185
|
+
init(ruby)?;
|
|
186
|
+
}
|
|
187
|
+
let _guard = INITIALIZATION_LOCK.try_lock().map_err(|_| {
|
|
188
|
+
conflict(
|
|
189
|
+
ruby,
|
|
190
|
+
"timezone initialization or registration is already in progress",
|
|
191
|
+
)
|
|
192
|
+
})?;
|
|
193
|
+
let initialization = INITIALIZATION.get().expect("companion was initialized");
|
|
194
|
+
initialization.validate_class(ruby)?;
|
|
195
|
+
let method = install_resolver(ruby)?;
|
|
196
|
+
ruby.get_inner(initialization.resolver).store(0, method)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fn install_resolver(ruby: &Ruby) -> Result<Value, Error> {
|
|
200
|
+
let time = ruby.class_time();
|
|
201
|
+
let singleton = time.singleton_class()?;
|
|
202
|
+
let previous: Option<Value> = if time.respond_to("find_timezone", true)? {
|
|
203
|
+
Some(time.funcall("method", ("find_timezone",))?)
|
|
204
|
+
} else {
|
|
205
|
+
None
|
|
206
|
+
};
|
|
207
|
+
let mut visibility = None;
|
|
208
|
+
for (query, access) in [
|
|
209
|
+
("public_method_defined?", "public"),
|
|
210
|
+
("protected_method_defined?", "protected"),
|
|
211
|
+
("private_method_defined?", "private"),
|
|
212
|
+
] {
|
|
213
|
+
if singleton.funcall::<_, _, bool>(query, ("find_timezone", false))? {
|
|
214
|
+
visibility = Some(access);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
let result = (|| {
|
|
219
|
+
time.define_singleton_method("find_timezone", magnus::function!(find_timezone, 1))?;
|
|
220
|
+
time.funcall("method", ("find_timezone",))
|
|
221
|
+
})();
|
|
222
|
+
if result.is_err() {
|
|
223
|
+
let _ = singleton.funcall::<_, _, Value>("remove_method", ("find_timezone",));
|
|
224
|
+
// Removing a replacement can expose an ancestor hidden by undef_method.
|
|
225
|
+
if previous.is_none() && time.respond_to("find_timezone", true).unwrap_or(false) {
|
|
226
|
+
let _ = singleton.funcall::<_, _, Value>("undef_method", ("find_timezone",));
|
|
227
|
+
}
|
|
228
|
+
if let (Some(method), Some(access)) = (previous, visibility) {
|
|
229
|
+
// A method-added hook can raise after the old body is restored.
|
|
230
|
+
let _ = singleton.funcall::<_, _, Value>("define_method", ("find_timezone", method));
|
|
231
|
+
let _ = singleton.funcall::<_, _, Value>(access, ("find_timezone",));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
result
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fn conflict(ruby: &Ruby, message: &str) -> Error {
|
|
238
|
+
Error::new(ruby.exception_load_error(), message.to_owned())
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fn wrap(ruby: &Ruby, zone: TimeZone) -> Result<Obj<JiffTimeZone>, Error> {
|
|
242
|
+
let class = INITIALIZATION
|
|
243
|
+
.get()
|
|
244
|
+
.map(|initialization| ruby.get_inner(initialization.class))
|
|
245
|
+
.ok_or_else(|| conflict(ruby, "call jiff_magnus::init before using constructors"))?;
|
|
246
|
+
let object = ruby.obj_wrap_as(JiffTimeZone::new(zone), class);
|
|
247
|
+
object.freeze();
|
|
248
|
+
Ok(object)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fn find_timezone(ruby: &Ruby, name: String) -> Result<Option<Obj<JiffTimeZone>>, Error> {
|
|
252
|
+
TimeZone::get(&name)
|
|
253
|
+
.ok()
|
|
254
|
+
.map(|zone| wrap(ruby, zone))
|
|
255
|
+
.transpose()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fn utc(ruby: &Ruby) -> Result<Obj<JiffTimeZone>, Error> {
|
|
259
|
+
wrap(ruby, TimeZone::UTC)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
fn unknown(ruby: &Ruby) -> Result<Obj<JiffTimeZone>, Error> {
|
|
263
|
+
wrap(ruby, TimeZone::unknown())
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn fixed(ruby: &Ruby, seconds: i32) -> Result<Obj<JiffTimeZone>, Error> {
|
|
267
|
+
let offset = jiff::tz::Offset::from_seconds(seconds)
|
|
268
|
+
.map_err(|err| Error::new(ruby.exception_arg_error(), err.to_string()))?;
|
|
269
|
+
wrap(ruby, TimeZone::fixed(offset))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fn posix(ruby: &Ruby, rule: String) -> Result<Obj<JiffTimeZone>, Error> {
|
|
273
|
+
let zone = TimeZone::posix(&rule)
|
|
274
|
+
.map_err(|err| Error::new(ruby.exception_arg_error(), err.to_string()))?;
|
|
275
|
+
wrap(ruby, zone)
|
|
276
|
+
}
|
data/tmp/.gitignore
ADDED
metadata
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jiff-magnus
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Erick Guan
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rb_sys
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.9.130
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.9.130
|
|
26
|
+
description: A Magnus companion providing JiffTimezone constructors and bundled IANA
|
|
27
|
+
timezone data for Ruby Time.
|
|
28
|
+
executables: []
|
|
29
|
+
extensions:
|
|
30
|
+
- extconf.rb
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- ".gitignore"
|
|
34
|
+
- Cargo.toml
|
|
35
|
+
- LICENSE
|
|
36
|
+
- README.md
|
|
37
|
+
- extconf.rb
|
|
38
|
+
- lib/jiff_magnus.rb
|
|
39
|
+
- src/lib.rs
|
|
40
|
+
- tmp/.gitignore
|
|
41
|
+
homepage: https://github.com/matsadler/magnus
|
|
42
|
+
licenses:
|
|
43
|
+
- MIT
|
|
44
|
+
metadata:
|
|
45
|
+
source_code_uri: https://github.com/matsadler/magnus/tree/main/jiff-magnus
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
require_paths:
|
|
48
|
+
- lib
|
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.2'
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
requirements: []
|
|
60
|
+
rubygems_version: 3.6.9
|
|
61
|
+
specification_version: 4
|
|
62
|
+
summary: Jiff timezone rules and bundled timezone data for Ruby Time.
|
|
63
|
+
test_files: []
|