diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-21 15:49:24 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-21 15:49:37 +0900 |
| commit | fcef25f6ef36287a4984ffdaab39df84f5aceeba (patch) | |
| tree | 97ceb2098fbb5642e858929da9578bb41e277ada /crates/shirabe-php-shim/src/json.rs | |
| parent | f0f5f084c883dc4f5b6e61603e82cd1c2092fd9d (diff) | |
| download | php-shirabe-fcef25f6ef36287a4984ffdaab39df84f5aceeba.tar.gz php-shirabe-fcef25f6ef36287a4984ffdaab39df84f5aceeba.tar.zst php-shirabe-fcef25f6ef36287a4984ffdaab39df84f5aceeba.zip | |
refactor(php-shim): split lib.rs
Diffstat (limited to 'crates/shirabe-php-shim/src/json.rs')
| -rw-r--r-- | crates/shirabe-php-shim/src/json.rs | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/crates/shirabe-php-shim/src/json.rs b/crates/shirabe-php-shim/src/json.rs new file mode 100644 index 0000000..ed704f9 --- /dev/null +++ b/crates/shirabe-php-shim/src/json.rs @@ -0,0 +1,107 @@ +use crate::ArrayObject; +use crate::PhpMixed; +use indexmap::IndexMap; + +pub trait JsonSerializable { + fn json_serialize(&self) -> PhpMixed; +} + +#[derive(Debug)] +pub struct JsonObject { + data: IndexMap<String, PhpMixed>, +} + +pub const JSON_UNESCAPED_UNICODE: i64 = 256; +pub const JSON_UNESCAPED_SLASHES: i64 = 64; +pub const JSON_PRETTY_PRINT: i64 = 128; +pub const JSON_THROW_ON_ERROR: i64 = 4194304; +pub const JSON_INVALID_UTF8_IGNORE: i64 = 1048576; + +pub const JSON_ERROR_NONE: i64 = 0; +pub const JSON_ERROR_DEPTH: i64 = 1; +pub const JSON_ERROR_STATE_MISMATCH: i64 = 2; +pub const JSON_ERROR_CTRL_CHAR: i64 = 3; +pub const JSON_ERROR_UTF8: i64 = 5; + +pub fn json_last_error() -> i64 { + todo!() +} + +pub fn json_encode<T: serde::Serialize + ?Sized>(value: &T) -> Option<String> { + // PHP's json_encode() with no flags escapes slashes and non-ASCII characters. + json_encode_ex(value, 0) +} + +pub fn json_encode_ex<T: serde::Serialize + ?Sized>(value: &T, flags: i64) -> Option<String> { + // serde_json's compact output already matches PHP's `json_encode` with both + // JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE set: forward slashes and non-ASCII + // characters are emitted verbatim. The two flags below re-apply PHP's default escaping when + // they are absent. + // TODO(phase-c): other flags (e.g. JSON_PRETTY_PRINT, JSON_HEX_*, JSON_THROW_ON_ERROR) are not + // handled yet; add them when a call site needs them. + let mut s = serde_json::to_string(value).ok()?; + + if flags & JSON_UNESCAPED_SLASHES == 0 { + s = s.replace('/', "\\/"); + } + + if flags & JSON_UNESCAPED_UNICODE == 0 { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + if (c as u32) <= 0x7F { + out.push(c); + } else { + let mut buf = [0u16; 2]; + for unit in c.encode_utf16(&mut buf) { + out.push_str(&format!("\\u{:04x}", unit)); + } + } + } + s = out; + } + + Some(s) +} + +// PHP's two-argument `json_decode`: without JSON_THROW_ON_ERROR it never throws, +// returning null on malformed input. With `assoc` false, JSON objects decode to +// stdClass-equivalent ArrayObject values; with `assoc` true, to associative arrays. +pub fn json_decode(s: &str, assoc: bool) -> anyhow::Result<PhpMixed> { + match serde_json::from_str::<serde_json::Value>(s) { + Ok(value) => Ok(json_value_to_php_mixed(value, assoc)), + Err(_) => Ok(PhpMixed::Null), + } +} + +fn json_value_to_php_mixed(value: serde_json::Value, assoc: bool) -> PhpMixed { + match value { + serde_json::Value::Null => PhpMixed::Null, + serde_json::Value::Bool(b) => PhpMixed::Bool(b), + serde_json::Value::Number(n) => match n.as_i64() { + Some(i) => PhpMixed::Int(i), + // Integers beyond i64 and any fractional/exponent number decode to float, + // matching PHP's default (non-bigint) behaviour. + None => PhpMixed::Float(n.as_f64().unwrap_or(0.0)), + }, + serde_json::Value::String(s) => PhpMixed::String(s), + serde_json::Value::Array(items) => PhpMixed::List( + items + .into_iter() + .map(|item| json_value_to_php_mixed(item, assoc)) + .collect(), + ), + serde_json::Value::Object(entries) => { + let data: IndexMap<String, PhpMixed> = entries + .into_iter() + .map(|(k, v)| (k, json_value_to_php_mixed(v, assoc))) + .collect(); + if assoc { + PhpMixed::Array(data) + } else { + PhpMixed::Object(ArrayObject { + data: data.into_iter().collect(), + }) + } + } + } +} |
