siderust-js 0.1.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 (272) hide show
  1. package/.github/workflows/ci.yml +166 -0
  2. package/.gitmodules +9 -0
  3. package/CHANGELOG.md +26 -0
  4. package/LICENSE +661 -0
  5. package/README.md +138 -0
  6. package/package.json +12 -0
  7. package/qtty-js/.github/workflows/ci.yml +151 -0
  8. package/qtty-js/.gitmodules +3 -0
  9. package/qtty-js/CHANGELOG.md +31 -0
  10. package/qtty-js/LICENSE +661 -0
  11. package/qtty-js/README.md +132 -0
  12. package/qtty-js/package.json +20 -0
  13. package/qtty-js/qtty/.github/workflows/ci.yml +155 -0
  14. package/qtty-js/qtty/CHANGELOG.md +120 -0
  15. package/qtty-js/qtty/Cargo.lock +1462 -0
  16. package/qtty-js/qtty/Cargo.toml +12 -0
  17. package/qtty-js/qtty/LICENSE +661 -0
  18. package/qtty-js/qtty/README.md +9 -0
  19. package/qtty-js/qtty/qtty/Cargo.toml +41 -0
  20. package/qtty-js/qtty/qtty/README.md +8 -0
  21. package/qtty-js/qtty/qtty/examples/angles.rs +14 -0
  22. package/qtty-js/qtty/qtty/examples/astronomy.rs +17 -0
  23. package/qtty-js/qtty/qtty/examples/dimensional_arithmetic.rs +83 -0
  24. package/qtty-js/qtty/qtty/examples/python_integration.rs +61 -0
  25. package/qtty-js/qtty/qtty/examples/quickstart.rs +15 -0
  26. package/qtty-js/qtty/qtty/examples/ratios.rs +12 -0
  27. package/qtty-js/qtty/qtty/examples/serde_with_unit.rs +234 -0
  28. package/qtty-js/qtty/qtty/examples/serialization.rs +141 -0
  29. package/qtty-js/qtty/qtty/examples/serialization_advanced.rs +155 -0
  30. package/qtty-js/qtty/qtty/src/f32.rs +108 -0
  31. package/qtty-js/qtty/qtty/src/f64.rs +30 -0
  32. package/qtty-js/qtty/qtty/src/i128.rs +111 -0
  33. package/qtty-js/qtty/qtty/src/i16.rs +111 -0
  34. package/qtty-js/qtty/qtty/src/i32.rs +111 -0
  35. package/qtty-js/qtty/qtty/src/i64.rs +111 -0
  36. package/qtty-js/qtty/qtty/src/i8.rs +111 -0
  37. package/qtty-js/qtty/qtty/src/lib.rs +238 -0
  38. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/Cargo.lock +83 -0
  39. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/Cargo.toml +10 -0
  40. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/src/lib.rs +7 -0
  41. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/Cargo.lock +83 -0
  42. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/Cargo.toml +10 -0
  43. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/src/lib.rs +7 -0
  44. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/Cargo.lock +83 -0
  45. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/Cargo.toml +10 -0
  46. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/src/lib.rs +5 -0
  47. package/qtty-js/qtty/qtty/tests/integration_tests.rs +529 -0
  48. package/qtty-js/qtty/qtty/tests/qtty_vec_feature_matrix.rs +58 -0
  49. package/qtty-js/qtty/qtty-core/Cargo.toml +41 -0
  50. package/qtty-js/qtty/qtty-core/README.md +8 -0
  51. package/qtty-js/qtty/qtty-core/examples/diesel_integration.rs +145 -0
  52. package/qtty-js/qtty/qtty-core/examples/quantity_db_serde.rs +215 -0
  53. package/qtty-js/qtty/qtty-core/src/dimension.rs +249 -0
  54. package/qtty-js/qtty/qtty-core/src/feature_diesel.rs +318 -0
  55. package/qtty-js/qtty/qtty-core/src/feature_pyo3.rs +27 -0
  56. package/qtty-js/qtty/qtty-core/src/feature_serde.rs +203 -0
  57. package/qtty-js/qtty/qtty-core/src/feature_tiberius.rs +28 -0
  58. package/qtty-js/qtty/qtty-core/src/lib.rs +744 -0
  59. package/qtty-js/qtty/qtty-core/src/macros.rs +93 -0
  60. package/qtty-js/qtty/qtty-core/src/quantity.rs +810 -0
  61. package/qtty-js/qtty/qtty-core/src/scalar.rs +1742 -0
  62. package/qtty-js/qtty/qtty-core/src/unit.rs +332 -0
  63. package/qtty-js/qtty/qtty-core/src/units/angular.rs +1228 -0
  64. package/qtty-js/qtty/qtty-core/src/units/area.rs +243 -0
  65. package/qtty-js/qtty/qtty-core/src/units/frequency.rs +179 -0
  66. package/qtty-js/qtty/qtty-core/src/units/length.rs +1270 -0
  67. package/qtty-js/qtty/qtty-core/src/units/mass.rs +488 -0
  68. package/qtty-js/qtty/qtty-core/src/units/mod.rs +26 -0
  69. package/qtty-js/qtty/qtty-core/src/units/power.rs +324 -0
  70. package/qtty-js/qtty/qtty-core/src/units/time.rs +667 -0
  71. package/qtty-js/qtty/qtty-core/src/units/unitless.rs +212 -0
  72. package/qtty-js/qtty/qtty-core/src/units/velocity.rs +210 -0
  73. package/qtty-js/qtty/qtty-core/src/units/volume.rs +269 -0
  74. package/qtty-js/qtty/qtty-core/tests/core.rs +628 -0
  75. package/qtty-js/qtty/qtty-core/tests/diesel.rs +461 -0
  76. package/qtty-js/qtty/qtty-core/tests/integers.rs +632 -0
  77. package/qtty-js/qtty/qtty-core/tests/no_cross_unit_ops.rs +35 -0
  78. package/qtty-js/qtty/qtty-core/tests/pyo3.rs +334 -0
  79. package/qtty-js/qtty/qtty-core/tests/quantity_f32.rs +276 -0
  80. package/qtty-js/qtty/qtty-core/tests/scalar_decimal.rs +258 -0
  81. package/qtty-js/qtty/qtty-core/tests/scalar_f32.rs +286 -0
  82. package/qtty-js/qtty/qtty-core/tests/scalar_f64_real.rs +287 -0
  83. package/qtty-js/qtty/qtty-core/tests/scalar_rational.rs +260 -0
  84. package/qtty-js/qtty/qtty-core/tests/serde.rs +256 -0
  85. package/qtty-js/qtty/qtty-core/tests/tiberius.rs +208 -0
  86. package/qtty-js/qtty/qtty-derive/Cargo.toml +23 -0
  87. package/qtty-js/qtty/qtty-derive/README.md +8 -0
  88. package/qtty-js/qtty/qtty-derive/src/lib.rs +340 -0
  89. package/qtty-js/qtty/qtty-ffi/ARCHITECTURE.md +3 -0
  90. package/qtty-js/qtty/qtty-ffi/Cargo.toml +31 -0
  91. package/qtty-js/qtty/qtty-ffi/README.md +9 -0
  92. package/qtty-js/qtty/qtty-ffi/build.rs +326 -0
  93. package/qtty-js/qtty/qtty-ffi/cbindgen.toml +105 -0
  94. package/qtty-js/qtty/qtty-ffi/include/qtty_ffi.h +1126 -0
  95. package/qtty-js/qtty/qtty-ffi/src/ffi.rs +1251 -0
  96. package/qtty-js/qtty/qtty-ffi/src/ffi_serde.rs +294 -0
  97. package/qtty-js/qtty/qtty-ffi/src/helpers.rs +310 -0
  98. package/qtty-js/qtty/qtty-ffi/src/lib.rs +229 -0
  99. package/qtty-js/qtty/qtty-ffi/src/macros.rs +121 -0
  100. package/qtty-js/qtty/qtty-ffi/src/registry.rs +274 -0
  101. package/qtty-js/qtty/qtty-ffi/src/types.rs +620 -0
  102. package/qtty-js/qtty/qtty-ffi/tests/integration_tests.rs +842 -0
  103. package/qtty-js/qtty/qtty-ffi/units.csv +156 -0
  104. package/qtty-js/qtty/qtty-ffi/units.csv.md +3 -0
  105. package/qtty-js/qtty-node/.prettierignore +6 -0
  106. package/qtty-js/qtty-node/.prettierrc.json +6 -0
  107. package/qtty-js/qtty-node/README.md +250 -0
  108. package/qtty-js/qtty-node/c8.config.json +11 -0
  109. package/qtty-js/qtty-node/eslint.config.js +31 -0
  110. package/qtty-js/qtty-node/examples/arithmetic.mjs +64 -0
  111. package/qtty-js/qtty-node/examples/astronomy.mjs +90 -0
  112. package/qtty-js/qtty-node/examples/quickstart.mjs +36 -0
  113. package/qtty-js/qtty-node/examples/serialization.mjs +125 -0
  114. package/qtty-js/qtty-node/examples/unit_factories.mjs +74 -0
  115. package/qtty-js/qtty-node/index.d.ts +219 -0
  116. package/qtty-js/qtty-node/index.js +323 -0
  117. package/qtty-js/qtty-node/lib/DerivedQuantity.js +122 -0
  118. package/qtty-js/qtty-node/lib/Quantity.js +151 -0
  119. package/qtty-js/qtty-node/lib/backend.js +25 -0
  120. package/qtty-js/qtty-node/native.cjs +306 -0
  121. package/qtty-js/qtty-node/package-lock.json +3223 -0
  122. package/qtty-js/qtty-node/package.json +70 -0
  123. package/qtty-js/qtty-node/units.d.ts +299 -0
  124. package/qtty-js/qtty-node/units.js +210 -0
  125. package/qtty-js/qtty-web/Cargo.lock +767 -0
  126. package/qtty-js/qtty-web/Cargo.toml +21 -0
  127. package/qtty-js/qtty-web/index.d.ts +140 -0
  128. package/qtty-js/qtty-web/index.js +20 -0
  129. package/qtty-js/qtty-web/lib/DerivedQuantity.js +58 -0
  130. package/qtty-js/qtty-web/lib/Quantity.js +75 -0
  131. package/qtty-js/qtty-web/lib/backend.js +80 -0
  132. package/qtty-js/qtty-web/package.json +45 -0
  133. package/qtty-js/qtty-web/src/lib.rs +111 -0
  134. package/qtty-js/scripts/ci.sh +73 -0
  135. package/scripts/ci.sh +123 -0
  136. package/siderust-core/Cargo.lock +787 -0
  137. package/siderust-core/Cargo.toml +18 -0
  138. package/siderust-core/DEDUPLICATION.md +124 -0
  139. package/siderust-core/src/body.rs +120 -0
  140. package/siderust-core/src/events.rs +184 -0
  141. package/siderust-core/src/lib.rs +20 -0
  142. package/siderust-core/src/observer.rs +55 -0
  143. package/siderust-core/src/position.rs +213 -0
  144. package/siderust-node/.prettierignore +7 -0
  145. package/siderust-node/.prettierrc.json +6 -0
  146. package/siderust-node/Cargo.lock +906 -0
  147. package/siderust-node/Cargo.toml +29 -0
  148. package/siderust-node/README.md +109 -0
  149. package/siderust-node/__test__/index.test.mjs +248 -0
  150. package/siderust-node/build.rs +5 -0
  151. package/siderust-node/c8.config.json +3 -0
  152. package/siderust-node/eslint.config.js +31 -0
  153. package/siderust-node/examples/01_basic_coordinates.mjs +24 -0
  154. package/siderust-node/examples/02_coordinate_transformations.mjs +25 -0
  155. package/siderust-node/examples/03_all_frames_conversions.mjs +26 -0
  156. package/siderust-node/examples/04_all_center_conversions.mjs +24 -0
  157. package/siderust-node/examples/05_target_tracking.mjs +22 -0
  158. package/siderust-node/examples/06_night_events.mjs +18 -0
  159. package/siderust-node/examples/07_moon_properties.mjs +21 -0
  160. package/siderust-node/examples/08_solar_system.mjs +19 -0
  161. package/siderust-node/examples/09_star_observability.mjs +22 -0
  162. package/siderust-node/examples/10_time_periods.mjs +9 -0
  163. package/siderust-node/examples/11_serialization.mjs +31 -0
  164. package/siderust-node/examples/12_runtime_ephemeris.mjs +27 -0
  165. package/siderust-node/examples/13_coordinate_operations.mjs +20 -0
  166. package/siderust-node/index.d.ts +623 -0
  167. package/siderust-node/index.js +79 -0
  168. package/siderust-node/lib/Observer.js +112 -0
  169. package/siderust-node/lib/Star.js +118 -0
  170. package/siderust-node/lib/backend.js +63 -0
  171. package/siderust-node/lib/wrappers.js +566 -0
  172. package/siderust-node/main.js +20 -0
  173. package/siderust-node/native.cjs +360 -0
  174. package/siderust-node/package-lock.json +3261 -0
  175. package/siderust-node/package.json +71 -0
  176. package/siderust-node/src/body.rs +74 -0
  177. package/siderust-node/src/coordinates.rs +372 -0
  178. package/siderust-node/src/ephemeris.rs +462 -0
  179. package/siderust-node/src/events.rs +577 -0
  180. package/siderust-node/src/lib.rs +43 -0
  181. package/siderust-node/src/observer.rs +132 -0
  182. package/siderust-node/src/phase.rs +218 -0
  183. package/siderust-node/src/position.rs +292 -0
  184. package/siderust-node/src/star.rs +200 -0
  185. package/siderust-web/Cargo.lock +855 -0
  186. package/siderust-web/Cargo.toml +34 -0
  187. package/siderust-web/README.md +100 -0
  188. package/siderust-web/__test__/index.test.mjs +118 -0
  189. package/siderust-web/examples/github-pages/README.md +31 -0
  190. package/siderust-web/examples/github-pages/index.html +135 -0
  191. package/siderust-web/index.d.ts +311 -0
  192. package/siderust-web/index.js +66 -0
  193. package/siderust-web/lib/Observer.js +103 -0
  194. package/siderust-web/lib/Star.js +116 -0
  195. package/siderust-web/lib/backend.js +400 -0
  196. package/siderust-web/lib/wrappers.js +512 -0
  197. package/siderust-web/package.json +55 -0
  198. package/siderust-web/src/body.rs +69 -0
  199. package/siderust-web/src/coordinates.rs +302 -0
  200. package/siderust-web/src/ephemeris.rs +456 -0
  201. package/siderust-web/src/events.rs +520 -0
  202. package/siderust-web/src/lib.rs +51 -0
  203. package/siderust-web/src/observer.rs +117 -0
  204. package/siderust-web/src/phase.rs +190 -0
  205. package/siderust-web/src/position.rs +291 -0
  206. package/siderust-web/src/star.rs +178 -0
  207. package/tempoch-js/.github/workflows/ci.yml +142 -0
  208. package/tempoch-js/.gitmodules +3 -0
  209. package/tempoch-js/CHANGELOG.md +25 -0
  210. package/tempoch-js/LICENSE +661 -0
  211. package/tempoch-js/README.md +126 -0
  212. package/tempoch-js/package.json +20 -0
  213. package/tempoch-js/scripts/ci.sh +73 -0
  214. package/tempoch-js/tempoch/.github/workflows/ci.yml +113 -0
  215. package/tempoch-js/tempoch/CHANGELOG.md +82 -0
  216. package/tempoch-js/tempoch/Cargo.lock +947 -0
  217. package/tempoch-js/tempoch/Cargo.toml +3 -0
  218. package/tempoch-js/tempoch/LICENSE +661 -0
  219. package/tempoch-js/tempoch/README.md +76 -0
  220. package/tempoch-js/tempoch/tempoch/Cargo.toml +27 -0
  221. package/tempoch-js/tempoch/tempoch/examples/periods.rs +45 -0
  222. package/tempoch-js/tempoch/tempoch/examples/quickstart.rs +13 -0
  223. package/tempoch-js/tempoch/tempoch/src/lib.rs +49 -0
  224. package/tempoch-js/tempoch/tempoch/tests/integration.rs +57 -0
  225. package/tempoch-js/tempoch/tempoch-core/Cargo.toml +24 -0
  226. package/tempoch-js/tempoch/tempoch-core/src/delta_t.rs +345 -0
  227. package/tempoch-js/tempoch/tempoch-core/src/instant.rs +811 -0
  228. package/tempoch-js/tempoch/tempoch-core/src/julian_date_ext.rs +142 -0
  229. package/tempoch-js/tempoch/tempoch-core/src/lib.rs +81 -0
  230. package/tempoch-js/tempoch/tempoch-core/src/period.rs +1168 -0
  231. package/tempoch-js/tempoch/tempoch-core/src/scales.rs +779 -0
  232. package/tempoch-js/tempoch/tempoch-ffi/Cargo.lock +889 -0
  233. package/tempoch-js/tempoch/tempoch-ffi/Cargo.toml +26 -0
  234. package/tempoch-js/tempoch/tempoch-ffi/build.rs +24 -0
  235. package/tempoch-js/tempoch/tempoch-ffi/cbindgen.toml +30 -0
  236. package/tempoch-js/tempoch/tempoch-ffi/src/error.rs +19 -0
  237. package/tempoch-js/tempoch/tempoch-ffi/src/lib.rs +82 -0
  238. package/tempoch-js/tempoch/tempoch-ffi/src/period.rs +101 -0
  239. package/tempoch-js/tempoch/tempoch-ffi/src/time.rs +711 -0
  240. package/tempoch-js/tempoch/tempoch-ffi/tests/ffi.rs +265 -0
  241. package/tempoch-js/tempoch-node/.prettierignore +6 -0
  242. package/tempoch-js/tempoch-node/.prettierrc.json +6 -0
  243. package/tempoch-js/tempoch-node/Cargo.lock +496 -0
  244. package/tempoch-js/tempoch-node/Cargo.toml +29 -0
  245. package/tempoch-js/tempoch-node/README.md +265 -0
  246. package/tempoch-js/tempoch-node/__test__/index.test.mjs +598 -0
  247. package/tempoch-js/tempoch-node/build.rs +5 -0
  248. package/tempoch-js/tempoch-node/c8.config.json +3 -0
  249. package/tempoch-js/tempoch-node/eslint.config.js +31 -0
  250. package/tempoch-js/tempoch-node/examples/periods.mjs +79 -0
  251. package/tempoch-js/tempoch-node/examples/quickstart.mjs +71 -0
  252. package/tempoch-js/tempoch-node/examples/timescales.mjs +92 -0
  253. package/tempoch-js/tempoch-node/index.d.ts +280 -0
  254. package/tempoch-js/tempoch-node/index.js +32 -0
  255. package/tempoch-js/tempoch-node/lib/JulianDate.js +176 -0
  256. package/tempoch-js/tempoch-node/lib/ModifiedJulianDate.js +156 -0
  257. package/tempoch-js/tempoch-node/lib/Period.js +133 -0
  258. package/tempoch-js/tempoch-node/lib/backend.js +38 -0
  259. package/tempoch-js/tempoch-node/lib/qttyCompat.js +92 -0
  260. package/tempoch-js/tempoch-node/native.cjs +317 -0
  261. package/tempoch-js/tempoch-node/package-lock.json +3223 -0
  262. package/tempoch-js/tempoch-node/package.json +56 -0
  263. package/tempoch-js/tempoch-node/src/lib.rs +573 -0
  264. package/tempoch-js/tempoch-web/Cargo.toml +23 -0
  265. package/tempoch-js/tempoch-web/index.d.ts +95 -0
  266. package/tempoch-js/tempoch-web/index.js +27 -0
  267. package/tempoch-js/tempoch-web/lib/JulianDate.js +170 -0
  268. package/tempoch-js/tempoch-web/lib/ModifiedJulianDate.js +145 -0
  269. package/tempoch-js/tempoch-web/lib/Period.js +121 -0
  270. package/tempoch-js/tempoch-web/lib/backend.js +118 -0
  271. package/tempoch-js/tempoch-web/package.json +46 -0
  272. package/tempoch-js/tempoch-web/src/lib.rs +184 -0
@@ -0,0 +1,294 @@
1
+ //! JSON serialization/deserialization FFI functions.
2
+ //!
3
+ //! Provides `extern "C"` functions for serializing and deserializing
4
+ //! [`QttyQuantity`] values to/from JSON, as well as a companion
5
+ //! `qtty_string_free` for releasing heap-allocated C strings returned by
6
+ //! these functions.
7
+ //!
8
+ //! # JSON formats
9
+ //!
10
+ //! * **value-only** (`qtty_quantity_to/from_json_value`): a bare JSON number,
11
+ //! e.g. `123.456`.
12
+ //! * **full object** (`qtty_quantity_to/from_json`): a JSON object with
13
+ //! `"value"` (f64) and `"unit_id"` (u32) fields,
14
+ //! e.g. `{"value":123.456,"unit_id":10001}`.
15
+
16
+ use crate::types::{
17
+ QttyQuantity, UnitId, QTTY_ERR_INVALID_VALUE, QTTY_ERR_NULL_OUT, QTTY_ERR_UNKNOWN_UNIT, QTTY_OK,
18
+ };
19
+ use core::ffi::c_char;
20
+ use std::ffi::{CStr, CString};
21
+
22
+ // =============================================================================
23
+ // Helper macro (mirrors the one in ffi.rs)
24
+ // =============================================================================
25
+
26
+ macro_rules! catch_panic {
27
+ ($default:expr, $body:expr) => {{
28
+ match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $body)) {
29
+ Ok(result) => result,
30
+ Err(_) => $default,
31
+ }
32
+ }};
33
+ }
34
+
35
+ // =============================================================================
36
+ // String management
37
+ // =============================================================================
38
+
39
+ /// Frees a C string previously returned by a `qtty_quantity_to_json*` function.
40
+ ///
41
+ /// Passing a null pointer is safe and is a no-op.
42
+ ///
43
+ /// # Safety
44
+ ///
45
+ /// The pointer **must** have been returned by `qtty_quantity_to_json` or
46
+ /// `qtty_quantity_to_json_value` and must not be freed more than once.
47
+ #[no_mangle]
48
+ pub unsafe extern "C" fn qtty_string_free(s: *mut c_char) {
49
+ if !s.is_null() {
50
+ // SAFETY: Pointer was produced by `CString::into_raw` in this crate.
51
+ unsafe { drop(CString::from_raw(s)) }
52
+ }
53
+ }
54
+
55
+ // =============================================================================
56
+ // Value-only JSON (bare number)
57
+ // =============================================================================
58
+
59
+ /// Serializes only the numeric value of a quantity as a JSON number string.
60
+ ///
61
+ /// On success, `*out_json` is set to a heap-allocated NUL-terminated C string
62
+ /// (e.g. `"123.456789"`) that the caller **must** release with
63
+ /// [`qtty_string_free`].
64
+ ///
65
+ /// # Returns
66
+ ///
67
+ /// * `QTTY_OK` on success.
68
+ /// * `QTTY_ERR_NULL_OUT` if `out_json` is null.
69
+ /// * `QTTY_ERR_INVALID_VALUE` if serialization fails.
70
+ ///
71
+ /// # Safety
72
+ ///
73
+ /// `out_json` must point to a writable `*mut c_char` location.
74
+ #[no_mangle]
75
+ pub unsafe extern "C" fn qtty_quantity_to_json_value(
76
+ src: QttyQuantity,
77
+ out_json: *mut *mut c_char,
78
+ ) -> i32 {
79
+ catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
80
+ if out_json.is_null() {
81
+ return QTTY_ERR_NULL_OUT;
82
+ }
83
+
84
+ let s = match serde_json::to_string(&src.value) {
85
+ Ok(s) => s,
86
+ Err(_) => return QTTY_ERR_INVALID_VALUE,
87
+ };
88
+
89
+ match CString::new(s) {
90
+ Ok(c) => {
91
+ // SAFETY: `out_json` is not null (checked above).
92
+ unsafe { *out_json = c.into_raw() };
93
+ QTTY_OK
94
+ }
95
+ Err(_) => QTTY_ERR_INVALID_VALUE,
96
+ }
97
+ })
98
+ }
99
+
100
+ /// Deserializes a JSON number string into a quantity with the given unit.
101
+ ///
102
+ /// # Returns
103
+ ///
104
+ /// * `QTTY_OK` on success.
105
+ /// * `QTTY_ERR_NULL_OUT` if either `json` or `out` is null.
106
+ /// * `QTTY_ERR_INVALID_VALUE` if `json` is not a valid JSON number.
107
+ ///
108
+ /// # Safety
109
+ ///
110
+ /// `json` must be a valid NUL-terminated C string.
111
+ /// `out` must point to a writable [`QttyQuantity`] location.
112
+ #[no_mangle]
113
+ pub unsafe extern "C" fn qtty_quantity_from_json_value(
114
+ unit: UnitId,
115
+ json: *const c_char,
116
+ out: *mut QttyQuantity,
117
+ ) -> i32 {
118
+ catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
119
+ if json.is_null() || out.is_null() {
120
+ return QTTY_ERR_NULL_OUT;
121
+ }
122
+
123
+ // SAFETY: Caller guarantees `json` is a valid NUL-terminated string.
124
+ let json_str = match unsafe { CStr::from_ptr(json) }.to_str() {
125
+ Ok(s) => s,
126
+ Err(_) => return QTTY_ERR_INVALID_VALUE,
127
+ };
128
+
129
+ match serde_json::from_str::<f64>(json_str) {
130
+ Ok(value) => {
131
+ // SAFETY: `out` is not null (checked above).
132
+ unsafe { *out = QttyQuantity::new(value, unit) };
133
+ QTTY_OK
134
+ }
135
+ Err(_) => QTTY_ERR_INVALID_VALUE,
136
+ }
137
+ })
138
+ }
139
+
140
+ // =============================================================================
141
+ // Full-object JSON {"value":<f64>, "unit_id":<u32>}
142
+ // =============================================================================
143
+
144
+ /// Serializes a quantity as a JSON object `{"value":<f64>,"unit_id":<u32>}`.
145
+ ///
146
+ /// On success, `*out_json` is set to a heap-allocated NUL-terminated C string
147
+ /// that the caller **must** release with [`qtty_string_free`].
148
+ ///
149
+ /// # Returns
150
+ ///
151
+ /// * `QTTY_OK` on success.
152
+ /// * `QTTY_ERR_NULL_OUT` if `out_json` is null.
153
+ /// * `QTTY_ERR_INVALID_VALUE` if serialization fails.
154
+ ///
155
+ /// # Safety
156
+ ///
157
+ /// `out_json` must point to a writable `*mut c_char` location.
158
+ #[no_mangle]
159
+ pub unsafe extern "C" fn qtty_quantity_to_json(
160
+ src: QttyQuantity,
161
+ out_json: *mut *mut c_char,
162
+ ) -> i32 {
163
+ catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
164
+ if out_json.is_null() {
165
+ return QTTY_ERR_NULL_OUT;
166
+ }
167
+
168
+ let obj = serde_json::json!({
169
+ "value": src.value,
170
+ "unit_id": src.unit as u32,
171
+ });
172
+
173
+ let s = match serde_json::to_string(&obj) {
174
+ Ok(s) => s,
175
+ Err(_) => return QTTY_ERR_INVALID_VALUE,
176
+ };
177
+
178
+ match CString::new(s) {
179
+ Ok(c) => {
180
+ // SAFETY: `out_json` is not null (checked above).
181
+ unsafe { *out_json = c.into_raw() };
182
+ QTTY_OK
183
+ }
184
+ Err(_) => QTTY_ERR_INVALID_VALUE,
185
+ }
186
+ })
187
+ }
188
+
189
+ /// Deserializes a JSON object `{"value":<f64>,"unit_id":<u32>}` into a quantity.
190
+ ///
191
+ /// # Returns
192
+ ///
193
+ /// * `QTTY_OK` on success.
194
+ /// * `QTTY_ERR_NULL_OUT` if either `json` or `out` is null.
195
+ /// * `QTTY_ERR_INVALID_VALUE` if `json` is not a valid JSON object with the
196
+ /// expected fields.
197
+ /// * `QTTY_ERR_UNKNOWN_UNIT` if the `unit_id` field does not map to a known unit.
198
+ ///
199
+ /// # Safety
200
+ ///
201
+ /// `json` must be a valid NUL-terminated C string.
202
+ /// `out` must point to a writable [`QttyQuantity`] location.
203
+ #[no_mangle]
204
+ pub unsafe extern "C" fn qtty_quantity_from_json(
205
+ json: *const c_char,
206
+ out: *mut QttyQuantity,
207
+ ) -> i32 {
208
+ catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
209
+ if json.is_null() || out.is_null() {
210
+ return QTTY_ERR_NULL_OUT;
211
+ }
212
+
213
+ // SAFETY: Caller guarantees `json` is a valid NUL-terminated string.
214
+ let json_str = match unsafe { CStr::from_ptr(json) }.to_str() {
215
+ Ok(s) => s,
216
+ Err(_) => return QTTY_ERR_INVALID_VALUE,
217
+ };
218
+
219
+ #[derive(serde::Deserialize)]
220
+ struct QtyJson {
221
+ value: f64,
222
+ unit_id: u32,
223
+ }
224
+
225
+ match serde_json::from_str::<QtyJson>(json_str) {
226
+ Ok(q) => match UnitId::from_u32(q.unit_id) {
227
+ Some(unit) => {
228
+ // SAFETY: `out` is not null (checked above).
229
+ unsafe { *out = QttyQuantity::new(q.value, unit) };
230
+ QTTY_OK
231
+ }
232
+ None => QTTY_ERR_UNKNOWN_UNIT,
233
+ },
234
+ Err(_) => QTTY_ERR_INVALID_VALUE,
235
+ }
236
+ })
237
+ }
238
+
239
+ #[cfg(test)]
240
+ mod tests {
241
+ use super::*;
242
+
243
+ #[test]
244
+ fn test_to_json_value_roundtrip() {
245
+ let src = QttyQuantity::new(123.456, UnitId::Meter);
246
+ let mut ptr: *mut c_char = std::ptr::null_mut();
247
+
248
+ let status = unsafe { qtty_quantity_to_json_value(src, &mut ptr) };
249
+ assert_eq!(status, QTTY_OK);
250
+ assert!(!ptr.is_null());
251
+
252
+ let mut out = QttyQuantity::default();
253
+ let status2 = unsafe { qtty_quantity_from_json_value(UnitId::Meter, ptr, &mut out) };
254
+ unsafe { qtty_string_free(ptr) };
255
+
256
+ assert_eq!(status2, QTTY_OK);
257
+ assert!((out.value - 123.456).abs() < 1e-12);
258
+ assert_eq!(out.unit, UnitId::Meter);
259
+ }
260
+
261
+ #[test]
262
+ fn test_to_json_roundtrip() {
263
+ let src = QttyQuantity::new(1.5, UnitId::Kilometer);
264
+ let mut ptr: *mut c_char = std::ptr::null_mut();
265
+
266
+ let status = unsafe { qtty_quantity_to_json(src, &mut ptr) };
267
+ assert_eq!(status, QTTY_OK);
268
+ assert!(!ptr.is_null());
269
+
270
+ let mut out = QttyQuantity::default();
271
+ let status2 = unsafe { qtty_quantity_from_json(ptr, &mut out) };
272
+ unsafe { qtty_string_free(ptr) };
273
+
274
+ assert_eq!(status2, QTTY_OK);
275
+ assert!((out.value - 1.5).abs() < 1e-12);
276
+ assert_eq!(out.unit, UnitId::Kilometer);
277
+ }
278
+
279
+ #[test]
280
+ fn test_from_json_value_invalid_input() {
281
+ let bad_json = std::ffi::CString::new("not a number").unwrap();
282
+ let mut out = QttyQuantity::default();
283
+
284
+ let status =
285
+ unsafe { qtty_quantity_from_json_value(UnitId::Meter, bad_json.as_ptr(), &mut out) };
286
+ assert_eq!(status, QTTY_ERR_INVALID_VALUE);
287
+ }
288
+
289
+ #[test]
290
+ fn test_string_free_null_is_noop() {
291
+ // Must not crash
292
+ unsafe { qtty_string_free(std::ptr::null_mut()) };
293
+ }
294
+ }
@@ -0,0 +1,310 @@
1
+ //! Helper functions and trait implementations for downstream crate integration.
2
+ //!
3
+ //! This module provides pre-implemented conversions between `qtty` types and [`QttyQuantity`],
4
+ //! making it easy for downstream Rust crates to integrate with the FFI layer.
5
+ //!
6
+ //! # Auto-generated conversions
7
+ //!
8
+ //! `From`/`TryFrom` implementations are auto-generated at build time from `units.csv` for
9
+ //! all units that have a corresponding Rust type mapping. This covers the vast majority of
10
+ //! units in the registry.
11
+ //!
12
+ //! Additional unit conversions can be implemented in downstream crates using the
13
+ //! [`impl_unit_ffi!`] macro.
14
+ //!
15
+ //! # Usage
16
+ //!
17
+ //! ## Converting to FFI format
18
+ //!
19
+ //! ```rust
20
+ //! use qtty::length::Meters;
21
+ //! use qtty_ffi::QttyQuantity;
22
+ //!
23
+ //! let meters = Meters::new(1000.0);
24
+ //! let ffi_qty: QttyQuantity = meters.into();
25
+ //! ```
26
+ //!
27
+ //! ## Converting from FFI format
28
+ //!
29
+ //! ```rust
30
+ //! use qtty::length::Kilometers;
31
+ //! use qtty_ffi::{QttyQuantity, UnitId};
32
+ //!
33
+ //! // A quantity received from FFI (could be in any compatible unit)
34
+ //! let ffi_qty = QttyQuantity::new(1000.0, UnitId::Meter);
35
+ //!
36
+ //! // Convert to Kilometers (automatic unit conversion happens)
37
+ //! let km: Kilometers = ffi_qty.try_into().unwrap();
38
+ //! assert!((km.value() - 1.0).abs() < 1e-12);
39
+ //! ```
40
+ //!
41
+ //! ## Error handling
42
+ //!
43
+ //! Conversion from [`QttyQuantity`] can fail if the dimensions are incompatible:
44
+ //!
45
+ //! ```rust
46
+ //! use qtty::length::Meters;
47
+ //! use qtty_ffi::{QttyQuantity, UnitId, QTTY_ERR_INCOMPATIBLE_DIM};
48
+ //!
49
+ //! let time_qty = QttyQuantity::new(60.0, UnitId::Second);
50
+ //! let result: Result<Meters, i32> = time_qty.try_into();
51
+ //! assert_eq!(result, Err(QTTY_ERR_INCOMPATIBLE_DIM));
52
+ //! ```
53
+
54
+ // Auto-generated From/TryFrom impls for all units with Rust type mappings.
55
+ // Generated by build.rs from units.csv.
56
+ include!(concat!(env!("OUT_DIR"), "/unit_conversions.rs"));
57
+
58
+ // =============================================================================
59
+ // Explicit Helper Functions (convenience API for common units)
60
+ // =============================================================================
61
+
62
+ use crate::QttyQuantity;
63
+
64
+ /// Converts `Meters` to an FFI-safe `QttyQuantity`.
65
+ #[inline]
66
+ pub fn meters_into_ffi(m: qtty::length::Meters) -> QttyQuantity {
67
+ m.into()
68
+ }
69
+
70
+ /// Attempts to convert a `QttyQuantity` to `Meters`.
71
+ ///
72
+ /// Returns an error if the quantity's unit is not length-compatible.
73
+ #[inline]
74
+ pub fn try_into_meters(q: QttyQuantity) -> Result<qtty::length::Meters, i32> {
75
+ q.try_into()
76
+ }
77
+
78
+ /// Converts `Kilometers` to an FFI-safe `QttyQuantity`.
79
+ #[inline]
80
+ pub fn kilometers_into_ffi(km: qtty::length::Kilometers) -> QttyQuantity {
81
+ km.into()
82
+ }
83
+
84
+ /// Attempts to convert a `QttyQuantity` to `Kilometers`.
85
+ ///
86
+ /// Returns an error if the quantity's unit is not length-compatible.
87
+ #[inline]
88
+ pub fn try_into_kilometers(q: QttyQuantity) -> Result<qtty::length::Kilometers, i32> {
89
+ q.try_into()
90
+ }
91
+
92
+ /// Converts `Seconds` to an FFI-safe `QttyQuantity`.
93
+ #[inline]
94
+ pub fn seconds_into_ffi(s: qtty::time::Seconds) -> QttyQuantity {
95
+ s.into()
96
+ }
97
+
98
+ /// Attempts to convert a `QttyQuantity` to `Seconds`.
99
+ ///
100
+ /// Returns an error if the quantity's unit is not time-compatible.
101
+ #[inline]
102
+ pub fn try_into_seconds(q: QttyQuantity) -> Result<qtty::time::Seconds, i32> {
103
+ q.try_into()
104
+ }
105
+
106
+ /// Converts `Minutes` to an FFI-safe `QttyQuantity`.
107
+ #[inline]
108
+ pub fn minutes_into_ffi(m: qtty::time::Minutes) -> QttyQuantity {
109
+ m.into()
110
+ }
111
+
112
+ /// Attempts to convert a `QttyQuantity` to `Minutes`.
113
+ ///
114
+ /// Returns an error if the quantity's unit is not time-compatible.
115
+ #[inline]
116
+ pub fn try_into_minutes(q: QttyQuantity) -> Result<qtty::time::Minutes, i32> {
117
+ q.try_into()
118
+ }
119
+
120
+ /// Converts `Hours` to an FFI-safe `QttyQuantity`.
121
+ #[inline]
122
+ pub fn hours_into_ffi(h: qtty::time::Hours) -> QttyQuantity {
123
+ h.into()
124
+ }
125
+
126
+ /// Attempts to convert a `QttyQuantity` to `Hours`.
127
+ ///
128
+ /// Returns an error if the quantity's unit is not time-compatible.
129
+ #[inline]
130
+ pub fn try_into_hours(q: QttyQuantity) -> Result<qtty::time::Hours, i32> {
131
+ q.try_into()
132
+ }
133
+
134
+ /// Converts `Days` to an FFI-safe `QttyQuantity`.
135
+ #[inline]
136
+ pub fn days_into_ffi(d: qtty::time::Days) -> QttyQuantity {
137
+ d.into()
138
+ }
139
+
140
+ /// Attempts to convert a `QttyQuantity` to `Days`.
141
+ ///
142
+ /// Returns an error if the quantity's unit is not time-compatible.
143
+ #[inline]
144
+ pub fn try_into_days(q: QttyQuantity) -> Result<qtty::time::Days, i32> {
145
+ q.try_into()
146
+ }
147
+
148
+ /// Converts `Radians` to an FFI-safe `QttyQuantity`.
149
+ #[inline]
150
+ pub fn radians_into_ffi(r: qtty::angular::Radians) -> QttyQuantity {
151
+ r.into()
152
+ }
153
+
154
+ /// Attempts to convert a `QttyQuantity` to `Radians`.
155
+ ///
156
+ /// Returns an error if the quantity's unit is not angle-compatible.
157
+ #[inline]
158
+ pub fn try_into_radians(q: QttyQuantity) -> Result<qtty::angular::Radians, i32> {
159
+ q.try_into()
160
+ }
161
+
162
+ /// Converts `Degrees` to an FFI-safe `QttyQuantity`.
163
+ #[inline]
164
+ pub fn degrees_into_ffi(d: qtty::angular::Degrees) -> QttyQuantity {
165
+ d.into()
166
+ }
167
+
168
+ /// Attempts to convert a `QttyQuantity` to `Degrees`.
169
+ ///
170
+ /// Returns an error if the quantity's unit is not angle-compatible.
171
+ #[inline]
172
+ pub fn try_into_degrees(q: QttyQuantity) -> Result<qtty::angular::Degrees, i32> {
173
+ q.try_into()
174
+ }
175
+
176
+ #[cfg(test)]
177
+ mod tests {
178
+ use super::*;
179
+ use crate::UnitId;
180
+ use crate::QTTY_ERR_INCOMPATIBLE_DIM;
181
+ use approx::assert_relative_eq;
182
+ use core::f64::consts::PI;
183
+
184
+ #[test]
185
+ fn test_meters_roundtrip() {
186
+ let original = qtty::length::Meters::new(42.5);
187
+ let ffi: QttyQuantity = original.into();
188
+ let back: qtty::length::Meters = ffi.try_into().unwrap();
189
+ assert_relative_eq!(back.value(), 42.5, epsilon = 1e-12);
190
+ }
191
+
192
+ #[test]
193
+ fn test_kilometers_roundtrip() {
194
+ let original = qtty::length::Kilometers::new(1.5);
195
+ let ffi: QttyQuantity = original.into();
196
+ let back: qtty::length::Kilometers = ffi.try_into().unwrap();
197
+ assert_relative_eq!(back.value(), 1.5, epsilon = 1e-12);
198
+ }
199
+
200
+ #[test]
201
+ fn test_meters_to_kilometers_via_ffi() {
202
+ let meters = qtty::length::Meters::new(1000.0);
203
+ let ffi: QttyQuantity = meters.into();
204
+ let km: qtty::length::Kilometers = ffi.try_into().unwrap();
205
+ assert_relative_eq!(km.value(), 1.0, epsilon = 1e-12);
206
+ }
207
+
208
+ #[test]
209
+ fn test_seconds_roundtrip() {
210
+ let original = qtty::time::Seconds::new(3600.0);
211
+ let ffi: QttyQuantity = original.into();
212
+ let back: qtty::time::Seconds = ffi.try_into().unwrap();
213
+ assert_relative_eq!(back.value(), 3600.0, epsilon = 1e-12);
214
+ }
215
+
216
+ #[test]
217
+ fn test_hours_to_seconds_via_ffi() {
218
+ let hours = qtty::time::Hours::new(1.0);
219
+ let ffi: QttyQuantity = hours.into();
220
+ let secs: qtty::time::Seconds = ffi.try_into().unwrap();
221
+ assert_relative_eq!(secs.value(), 3600.0, epsilon = 1e-12);
222
+ }
223
+
224
+ #[test]
225
+ fn test_degrees_to_radians_via_ffi() {
226
+ let degrees = qtty::angular::Degrees::new(180.0);
227
+ let ffi: QttyQuantity = degrees.into();
228
+ let radians: qtty::angular::Radians = ffi.try_into().unwrap();
229
+ assert_relative_eq!(radians.value(), PI, epsilon = 1e-12);
230
+ }
231
+
232
+ #[test]
233
+ fn test_incompatible_conversion_fails() {
234
+ let meters = qtty::length::Meters::new(100.0);
235
+ let ffi: QttyQuantity = meters.into();
236
+ let result: Result<qtty::time::Seconds, i32> = ffi.try_into();
237
+ assert_eq!(result, Err(QTTY_ERR_INCOMPATIBLE_DIM));
238
+ }
239
+
240
+ #[test]
241
+ fn test_explicit_helper_functions() {
242
+ let m = qtty::length::Meters::new(1000.0);
243
+ let ffi = meters_into_ffi(m);
244
+ let km = try_into_kilometers(ffi).unwrap();
245
+ assert_relative_eq!(km.value(), 1.0, epsilon = 1e-12);
246
+ }
247
+
248
+ #[test]
249
+ fn test_all_helper_functions_cover_unit_variants() {
250
+ use qtty::angular::{Degrees, Radians};
251
+ use qtty::length::{Kilometers, Meters};
252
+ use qtty::time::{Days, Hours, Minutes, Seconds};
253
+
254
+ let meters_qty = meters_into_ffi(Meters::new(12.0));
255
+ assert_eq!(meters_qty.unit, UnitId::Meter);
256
+ assert_relative_eq!(meters_qty.value, 12.0, epsilon = 1e-12);
257
+
258
+ let kilometers_qty = kilometers_into_ffi(Kilometers::new(3.0));
259
+ assert_eq!(kilometers_qty.unit, UnitId::Kilometer);
260
+ assert_relative_eq!(kilometers_qty.value, 3.0, epsilon = 1e-12);
261
+
262
+ let meters_from_kilometers = try_into_meters(kilometers_qty).unwrap();
263
+ assert_relative_eq!(meters_from_kilometers.value(), 3000.0, epsilon = 1e-12);
264
+
265
+ let kilometers_from_meters = try_into_kilometers(meters_qty).unwrap();
266
+ assert_relative_eq!(kilometers_from_meters.value(), 0.012, epsilon = 1e-12);
267
+
268
+ let seconds_qty = seconds_into_ffi(Seconds::new(90.0));
269
+ assert_eq!(seconds_qty.unit, UnitId::Second);
270
+ assert_relative_eq!(seconds_qty.value, 90.0, epsilon = 1e-12);
271
+
272
+ let minutes_qty = minutes_into_ffi(Minutes::new(2.5));
273
+ assert_eq!(minutes_qty.unit, UnitId::Minute);
274
+ assert_relative_eq!(minutes_qty.value, 2.5, epsilon = 1e-12);
275
+
276
+ let seconds_from_minutes = try_into_seconds(minutes_qty).unwrap();
277
+ assert_relative_eq!(seconds_from_minutes.value(), 150.0, epsilon = 1e-12);
278
+
279
+ let minutes_from_seconds = try_into_minutes(seconds_qty).unwrap();
280
+ assert_relative_eq!(minutes_from_seconds.value(), 1.5, epsilon = 1e-12);
281
+
282
+ let hours_qty = hours_into_ffi(Hours::new(4.0));
283
+ assert_eq!(hours_qty.unit, UnitId::Hour);
284
+ assert_relative_eq!(hours_qty.value, 4.0, epsilon = 1e-12);
285
+
286
+ let hours_from_minutes = try_into_hours(minutes_into_ffi(Minutes::new(180.0))).unwrap();
287
+ assert_relative_eq!(hours_from_minutes.value(), 3.0, epsilon = 1e-12);
288
+
289
+ let days_qty = days_into_ffi(Days::new(1.25));
290
+ assert_eq!(days_qty.unit, UnitId::Day);
291
+ assert_relative_eq!(days_qty.value, 1.25, epsilon = 1e-12);
292
+
293
+ let days_from_hours = try_into_days(hours_into_ffi(Hours::new(48.0))).unwrap();
294
+ assert_relative_eq!(days_from_hours.value(), 2.0, epsilon = 1e-12);
295
+
296
+ let radians_qty = radians_into_ffi(Radians::new(PI));
297
+ assert_eq!(radians_qty.unit, UnitId::Radian);
298
+ assert_relative_eq!(radians_qty.value, PI, epsilon = 1e-12);
299
+
300
+ let degrees_qty = degrees_into_ffi(Degrees::new(270.0));
301
+ assert_eq!(degrees_qty.unit, UnitId::Degree);
302
+ assert_relative_eq!(degrees_qty.value, 270.0, epsilon = 1e-12);
303
+
304
+ let radians_from_degrees = try_into_radians(degrees_qty).unwrap();
305
+ assert_relative_eq!(radians_from_degrees.value(), 1.5 * PI, epsilon = 1e-12);
306
+
307
+ let degrees_from_radians = try_into_degrees(radians_qty).unwrap();
308
+ assert_relative_eq!(degrees_from_radians.value(), 180.0, epsilon = 1e-12);
309
+ }
310
+ }