From 2f22a841d3ac5688bd1e047322ac8b6d26223bdf Mon Sep 17 00:00:00 2001 From: gngpp Date: Tue, 21 Jul 2026 12:13:19 +0800 Subject: [PATCH] refactor(enum): centralize Ruby value methods --- lib/wreq.rb | 1 + lib/wreq_ruby/emulate.rb | 7 ++- src/cookie.rs | 31 +++--------- src/emulate.rs | 49 +++++-------------- src/http.rs | 80 +++++++++--------------------- src/macros.rs | 94 +++++++++++++++--------------------- test/value_semantics_test.rb | 25 +++++++--- 7 files changed, 103 insertions(+), 184 deletions(-) diff --git a/lib/wreq.rb b/lib/wreq.rb index 5e5ee21..4f23bc9 100644 --- a/lib/wreq.rb +++ b/lib/wreq.rb @@ -9,6 +9,7 @@ # Load type hint definitions require_relative "wreq_ruby/http" +require_relative "wreq_ruby/emulate" require_relative "wreq_ruby/client" require_relative "wreq_ruby/response" require_relative "wreq_ruby/body" diff --git a/lib/wreq_ruby/emulate.rb b/lib/wreq_ruby/emulate.rb index d0e4672..027ace3 100644 --- a/lib/wreq_ruby/emulate.rb +++ b/lib/wreq_ruby/emulate.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +# Profile and platform constants mirror the native enum variant names. +# standard:disable Naming/ConstantName + module Wreq # Browser and client fingerprint profile enumeration backed by Rust. # @@ -279,7 +282,7 @@ def hash class Emulation # Native fields and methods are set by the extension. # This stub is for documentation only. - unless method_defined?(:new) + unless singleton_methods(false).include?(:new) # @param profile [Wreq::Profile, nil] Fingerprint profile to emulate # @param platform [Wreq::Platform, nil] Operating system platform to emulate # @param http2 [Boolean, nil] Whether HTTP/2 emulation is enabled; defaults @@ -294,3 +297,5 @@ def self.new(**options) end end end + +# standard:enable Naming/ConstantName diff --git a/src/cookie.rs b/src/cookie.rs index 192575f..14da8ea 100644 --- a/src/cookie.rs +++ b/src/cookie.rs @@ -13,38 +13,19 @@ use crate::{ gvl, }; +// Defines constant registration, `into_ffi`/`from_ffi`, and handlers for +// Ruby's `to_s`, `to_sym`, `==`, `eql?`, and `hash` methods. define_ruby_enum!( /// The Cookie SameSite attribute. - const, SameSite, "Wreq::SameSite", cookie::SameSite, - Strict, - Lax, - None, + symbols: + Strict => "strict", + Lax => "lax", + None => "none", ); -impl SameSite { - /// SameSite attribute as a string. - pub fn to_s(&self) -> &'static str { - match self { - SameSite::Strict => "Strict", - SameSite::Lax => "Lax", - SameSite::None => "None", - } - } - - /// SameSite attribute as a lowercase Ruby symbol. - pub fn to_sym(ruby: &Ruby, rb_self: &Self) -> magnus::Symbol { - let name = match *rb_self { - SameSite::Strict => "strict", - SameSite::Lax => "lax", - SameSite::None => "none", - }; - ruby.to_symbol(name) - } -} - /// A single HTTP cookie. #[derive(Clone)] #[magnus::wrap(class = "Wreq::Cookie", free_immediately, size)] diff --git a/src/emulate.rs b/src/emulate.rs index 8feaa1f..c1c20ac 100644 --- a/src/emulate.rs +++ b/src/emulate.rs @@ -1,8 +1,5 @@ use ::serde::Deserialize; -use magnus::{ - Error, Module, Object, RModule, Ruby, Value, function, method, - typed_data::{Inspect, Obj}, -}; +use magnus::{Error, Module, Object, RModule, Ruby, Value, function, method, typed_data::Obj}; use crate::options::{NativeOption, Options}; @@ -41,9 +38,10 @@ impl Builder { } } +// Defines constant registration, `into_ffi`/`from_ffi`, and handlers for +// Ruby's `to_s`, `==`, `eql?`, and `hash` methods. define_ruby_enum!( /// An emulation profile. - const, Profile, "Wreq::Profile", wreq_util::Profile, @@ -187,17 +185,19 @@ define_ruby_enum!( Opera131, ); +// Defines constant registration, `into_ffi`/`from_ffi`, and handlers for +// Ruby's `to_s`, `to_sym`, `==`, `eql?`, and `hash` methods. define_ruby_enum!( /// An emulation profile for OS. - const, Platform, "Wreq::Platform", wreq_util::Platform, - Windows, - MacOS, - Linux, - Android, - IOS, + symbols: + Windows => "windows", + MacOS => "macos", + Linux => "linux", + Android => "android", + IOS => "ios", ); /// A struct to represent the `EmulationOption` class. @@ -205,33 +205,6 @@ define_ruby_enum!( #[magnus::wrap(class = "Wreq::Emulation", free_immediately, size)] pub struct Emulation(pub wreq_util::Emulation); -// ===== impl Profile ===== - -impl Profile { - pub fn to_s(&self) -> String { - self.into_ffi().inspect() - } -} - -// ===== impl Platform ===== - -impl Platform { - pub fn to_s(&self) -> String { - self.into_ffi().inspect() - } - - pub fn to_sym(ruby: &Ruby, rb_self: &Self) -> magnus::Symbol { - let name = match *rb_self { - Platform::Windows => "windows", - Platform::MacOS => "macos", - Platform::Linux => "linux", - Platform::Android => "android", - Platform::IOS => "ios", - }; - ruby.to_symbol(name) - } -} - // ===== impl Emulation ===== impl Emulation { diff --git a/src/http.rs b/src/http.rs index 8d63a12..d5f8376 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,81 +1,45 @@ -use magnus::{Error, Module, RModule, Ruby, TryConvert, Value, method, typed_data::Inspect}; +use magnus::{Error, Module, RModule, Ruby, TryConvert, Value, method}; +// Defines constant registration, `into_ffi`/`from_ffi`, and handlers for +// Ruby's `to_s`, `==`, `eql?`, and `hash` methods. +// `wreq::Version` only exposes protocol strings through `Debug`, so static +// labels avoid allocating an intermediate `String`. define_ruby_enum!( /// An HTTP version. - const, Version, "Wreq::Version", wreq::Version, - HTTP_09, - HTTP_10, - HTTP_11, - HTTP_2, - HTTP_3, + strings: + HTTP_09 => "HTTP/0.9", + HTTP_10 => "HTTP/1.0", + HTTP_11 => "HTTP/1.1", + HTTP_2 => "HTTP/2.0", + HTTP_3 => "HTTP/3.0", ); +// Defines constant registration, `into_ffi`/`from_ffi`, and handlers for +// Ruby's `to_s`, `to_sym`, `==`, `eql?`, and `hash` methods. define_ruby_enum!( /// An HTTP method. Method, "Wreq::Method", wreq::Method, - GET, - HEAD, - POST, - PUT, - DELETE, - OPTIONS, - TRACE, - PATCH, + symbols: + GET => "get", + HEAD => "head", + POST => "post", + PUT => "put", + DELETE => "delete", + OPTIONS => "options", + TRACE => "trace", + PATCH => "patch", ); -impl Method { - /// HTTP method token as a string. - #[inline] - pub fn to_s(&self) -> &'static str { - match self { - Method::GET => "GET", - Method::HEAD => "HEAD", - Method::POST => "POST", - Method::PUT => "PUT", - Method::DELETE => "DELETE", - Method::OPTIONS => "OPTIONS", - Method::TRACE => "TRACE", - Method::PATCH => "PATCH", - } - } - - /// HTTP method as a lowercase Ruby symbol. - #[inline] - pub fn to_sym(ruby: &Ruby, rb_self: &Self) -> magnus::Symbol { - let name = match *rb_self { - Method::GET => "get", - Method::HEAD => "head", - Method::POST => "post", - Method::PUT => "put", - Method::DELETE => "delete", - Method::OPTIONS => "options", - Method::TRACE => "trace", - Method::PATCH => "patch", - }; - ruby.to_symbol(name) - } -} - /// HTTP status code. #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[magnus::wrap(class = "Wreq::StatusCode", free_immediately, size)] pub struct StatusCode(pub wreq::StatusCode); -// ===== impl Version ===== - -impl Version { - /// Convert version to string. - #[inline] - pub fn to_s(&self) -> String { - self.into_ffi().inspect() - } -} - impl TryConvert for Version { fn try_convert(value: magnus::Value) -> Result { <&Version>::try_convert(value).cloned() diff --git a/src/macros.rs b/src/macros.rs index 9ca1540..02d3f5d 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -54,45 +54,63 @@ macro_rules! extract_native_option { } macro_rules! define_ruby_enum { - ($(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $($variant:ident),* $(,)?) => { - define_ruby_enum!($(#[$meta])* $enum_type, $ruby_class, $ffi_type, $( ($variant, $variant) ),*); + ($(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, strings: $($variant:ident => $display:expr),* $(,)?) => { + define_ruby_enum!(@impl $(#[$meta])* $enum_type, $ruby_class, $ffi_type, [$(($variant, $display)),*], []); + }; + + ($(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, symbols: $($variant:ident => $symbol:expr),* $(,)?) => { + define_ruby_enum!(@impl $(#[$meta])* $enum_type, $ruby_class, $ffi_type, [$(($variant, stringify!($variant))),*], [$($variant => $symbol),*]); }; - ($(#[$meta:meta])* const, $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $($variant:ident),* $(,)?) => { - define_ruby_enum!($(#[$meta])* const, $enum_type, $ruby_class, $ffi_type, $( ($variant, $variant) ),*); + ($(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $($variant:ident),* $(,)?) => { + define_ruby_enum!(@impl $(#[$meta])* $enum_type, $ruby_class, $ffi_type, [$(($variant, stringify!($variant))),*], []); }; - ($(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $(($rust_variant:ident, $ffi_variant:ident)),* $(,)?) => { + (@impl $(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, [$(($variant:ident, $display:expr)),*], [$($symbol_variant:ident => $symbol:expr),*]) => { $(#[$meta])* #[magnus::wrap(class = $ruby_class, free_immediately, size)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[allow(non_camel_case_types)] #[allow(clippy::upper_case_acronyms)] pub enum $enum_type { - $($rust_variant),* + $($variant),* } impl $enum_type { + /// Return the static text exposed through Ruby's `to_s`. + #[inline] + pub const fn to_s(&self) -> &'static str { + match self { + $(<$enum_type>::$variant => $display,)* + } + } + + define_ruby_enum!(@to_sym $enum_type, [$($symbol_variant => $symbol),*]); + + /// Convert this Ruby wrapper into its native enum value. pub fn into_ffi(self) -> $ffi_type { match self { - $(<$enum_type>::$rust_variant => <$ffi_type>::$ffi_variant,)* + $(<$enum_type>::$variant => <$ffi_type>::$variant,)* } } + /// Convert a known native enum value into its Ruby wrapper. #[allow(dead_code)] pub fn from_ffi(ffi: $ffi_type) -> Self { #[allow(unreachable_patterns)] match ffi { - $(<$ffi_type>::$ffi_variant => <$enum_type>::$rust_variant,)* + $(<$ffi_type>::$variant => <$enum_type>::$variant,)* _ => unreachable!(), } } + /// Register every enum variant as a constant on the Ruby class. pub fn define_constants(class: magnus::RClass) -> Result<(), magnus::Error> { - $(class.const_set(stringify!($rust_variant), <$enum_type>::$rust_variant)?;)* + $(class.const_set(stringify!($variant), <$enum_type>::$variant)?;)* Ok(()) } + /// Compare two wrapped enum values for Ruby's `==`. pub fn equals(&self, other: magnus::Value) -> bool { use magnus::TryConvert; <&$enum_type>::try_convert(other) @@ -100,10 +118,12 @@ macro_rules! define_ruby_enum { .unwrap_or(false) } + /// Compare two wrapped enum values for Ruby's `eql?`. pub fn is_eql(&self, other: magnus::Value) -> bool { self.equals(other) } + /// Return a hash consistent with Ruby's `eql?` contract. pub fn hash_value(&self) -> u64 { use std::hash::{Hash, Hasher}; let mut hasher = std::collections::hash_map::DefaultHasher::new(); @@ -113,54 +133,16 @@ macro_rules! define_ruby_enum { } }; - ($(#[$meta:meta])* const, $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $(($rust_variant:ident, $ffi_variant:ident)),* $(,)?) => { - $(#[$meta])* - #[magnus::wrap(class = $ruby_class, free_immediately, size)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - #[allow(non_camel_case_types)] - #[allow(clippy::upper_case_acronyms)] - pub enum $enum_type { - $($rust_variant),* - } - - impl $enum_type { - pub const fn into_ffi(self) -> $ffi_type { - match self { - $(<$enum_type>::$rust_variant => <$ffi_type>::$ffi_variant,)* - } - } + (@to_sym $enum_type:ident, []) => {}; - #[allow(dead_code)] - pub const fn from_ffi(ffi: $ffi_type) -> Self { - #[allow(unreachable_patterns)] - match ffi { - $(<$ffi_type>::$ffi_variant => <$enum_type>::$rust_variant,)* - _ => unreachable!(), - } - } - - pub fn define_constants(class: magnus::RClass) -> Result<(), magnus::Error> { - $(class.const_set(stringify!($rust_variant), <$enum_type>::$rust_variant)?;)* - Ok(()) - } - - pub fn equals(&self, other: magnus::Value) -> bool { - use magnus::TryConvert; - <&$enum_type>::try_convert(other) - .map(|other| *self == *other) - .unwrap_or(false) - } - - pub fn is_eql(&self, other: magnus::Value) -> bool { - self.equals(other) - } - - pub fn hash_value(&self) -> u64 { - use std::hash::{Hash, Hasher}; - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - self.hash(&mut hasher); - hasher.finish() - } + (@to_sym $enum_type:ident, [$($variant:ident => $symbol:expr),+]) => { + /// Return the configured Ruby symbol for this enum value. + #[inline] + pub fn to_sym(ruby: &magnus::Ruby, rb_self: &Self) -> magnus::Symbol { + let name = match *rb_self { + $(<$enum_type>::$variant => $symbol,)+ + }; + ruby.to_symbol(name) } }; } diff --git a/test/value_semantics_test.rb b/test/value_semantics_test.rb index 6acd872..78edc27 100644 --- a/test/value_semantics_test.rb +++ b/test/value_semantics_test.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "test_helper" -require "set" class ValueSemanticsTest < Minitest::Test # ---- StatusCode ---- @@ -42,7 +41,7 @@ def test_status_code_hash_consistent def test_status_code_as_hash_key status = @response.status - h = { status => "created" } + h = {status => "created"} assert_equal "created", h[@response.status] end @@ -73,6 +72,11 @@ def test_response_code_still_returns_integer def test_version_equality assert_equal Wreq::Version::HTTP_11, Wreq::Version::HTTP_11 + assert_equal "HTTP/0.9", Wreq::Version::HTTP_09.to_s + assert_equal "HTTP/1.0", Wreq::Version::HTTP_10.to_s + assert_equal "HTTP/1.1", Wreq::Version::HTTP_11.to_s + assert_equal "HTTP/2.0", Wreq::Version::HTTP_2.to_s + assert_equal "HTTP/3.0", Wreq::Version::HTTP_3.to_s end def test_version_eql @@ -92,7 +96,7 @@ def test_version_different_values_not_equal def test_version_as_hash_key v = Wreq::Version::HTTP_11 - h = { v => "http1.1" } + h = {v => "http1.1"} assert_equal "http1.1", h[Wreq::Version::HTTP_11] end @@ -143,7 +147,7 @@ def test_method_hash_consistent end def test_method_as_hash_key - h = { Wreq::Method::GET => "get it" } + h = {Wreq::Method::GET => "get it"} assert_equal "get it", h[Wreq::Method::GET] assert_nil h[Wreq::Method::POST] end @@ -177,6 +181,9 @@ def test_same_site_eql_and_hash def test_profile_equality assert_equal Wreq::Profile::Chrome134, Wreq::Profile::Chrome134 refute_equal Wreq::Profile::Chrome134, Wreq::Profile::Chrome135 + assert_equal "Chrome134", Wreq::Profile::Chrome134.to_s + assert_equal "SafariIos17_4_1", Wreq::Profile::SafariIos17_4_1.to_s + assert_equal "OkHttp4_12", Wreq::Profile::OkHttp4_12.to_s end def test_profile_eql_and_hash @@ -185,7 +192,7 @@ def test_profile_eql_and_hash end def test_profile_as_hash_key - h = { Wreq::Profile::Chrome134 => "chrome" } + h = {Wreq::Profile::Chrome134 => "chrome"} assert_equal "chrome", h[Wreq::Profile::Chrome134] assert_nil h[Wreq::Profile::Chrome135] end @@ -203,6 +210,12 @@ def test_platform_eql_and_hash end def test_platform_to_sym + assert_equal "Windows", Wreq::Platform::Windows.to_s + assert_equal "MacOS", Wreq::Platform::MacOS.to_s + assert_equal "Linux", Wreq::Platform::Linux.to_s + assert_equal "Android", Wreq::Platform::Android.to_s + assert_equal "IOS", Wreq::Platform::IOS.to_s + assert_equal :windows, Wreq::Platform::Windows.to_sym assert_equal :macos, Wreq::Platform::MacOS.to_sym assert_equal :linux, Wreq::Platform::Linux.to_sym @@ -222,4 +235,4 @@ def test_cross_type_eql_false refute Wreq::Method::GET.eql?(Wreq::Version::HTTP_11) refute Wreq::SameSite::Lax.eql?(Wreq::Method::GET) end -end \ No newline at end of file +end