use serde_json::{Map, Value};
pub(crate) mod keys {
pub(crate) const AFTER_CREATE_ORGANIZATION_URL: &str = "afterCreateOrganizationUrl";
pub(crate) const AFTER_JOIN_WAITLIST_URL: &str = "afterJoinWaitlistUrl";
pub(crate) const AFTER_MULTI_SESSION_SINGLE_SIGN_OUT_URL: &str =
"afterMultiSessionSingleSignOutUrl";
pub(crate) const AFTER_SELECT_ORGANIZATION_URL: &str = "afterSelectOrganizationUrl";
pub(crate) const AFTER_SIGN_OUT_URL: &str = "afterSignOutUrl";
pub(crate) const AFTER_SWITCH_SESSION_URL: &str = "afterSwitchSessionUrl";
pub(crate) const ALLOWED_REDIRECT_ORIGINS: &str = "allowedRedirectOrigins";
pub(crate) const ALLOWED_REDIRECT_PROTOCOLS: &str = "allowedRedirectProtocols";
pub(crate) const APPEARANCE: &str = "appearance";
pub(crate) const CREATE_ORGANIZATION_URL: &str = "createOrganizationUrl";
pub(crate) const DEFAULT_OPEN: &str = "defaultOpen";
pub(crate) const DOMAIN: &str = "domain";
pub(crate) const FALLBACK_REDIRECT_URL: &str = "fallbackRedirectUrl";
pub(crate) const FORCE_REDIRECT_URL: &str = "forceRedirectUrl";
pub(crate) const INITIAL_VALUES: &str = "initialValues";
pub(crate) const IS_SATELLITE: &str = "isSatellite";
pub(crate) const LEEWAY_IN_SECONDS: &str = "leewayInSeconds";
pub(crate) const LOCALIZATION: &str = "localization";
pub(crate) const ORGANIZATION_ID: &str = "organizationId";
pub(crate) const ORGANIZATION_PROFILE_URL: &str = "organizationProfileUrl";
pub(crate) const PATH: &str = "path";
pub(crate) const PREFETCH_UI: &str = "prefetchUI";
pub(crate) const PROXY_URL: &str = "proxyUrl";
pub(crate) const REDIRECT_URL: &str = "redirectUrl";
pub(crate) const REDIRECT_URL_COMPLETE: &str = "redirectUrlComplete";
pub(crate) const ROUTING: &str = "routing";
pub(crate) const SATELLITE_AUTO_SYNC: &str = "satelliteAutoSync";
pub(crate) const SESSION_ID: &str = "sessionId";
pub(crate) const SHOW_NAME: &str = "showName";
pub(crate) const SIGN_IN_FALLBACK_REDIRECT_URL: &str = "signInFallbackRedirectUrl";
pub(crate) const SIGN_IN_FORCE_REDIRECT_URL: &str = "signInForceRedirectUrl";
pub(crate) const SIGN_IN_URL: &str = "signInUrl";
pub(crate) const SIGN_UP_FALLBACK_REDIRECT_URL: &str = "signUpFallbackRedirectUrl";
pub(crate) const SIGN_UP_FORCE_REDIRECT_URL: &str = "signUpForceRedirectUrl";
pub(crate) const SIGN_UP_URL: &str = "signUpUrl";
pub(crate) const SKIP_CACHE: &str = "skipCache";
pub(crate) const TASK_URLS: &str = "taskUrls";
pub(crate) const TEMPLATE: &str = "template";
pub(crate) const TRANSFERABLE: &str = "transferable";
pub(crate) const USER_PROFILE_MODE: &str = "userProfileMode";
pub(crate) const USER_PROFILE_PROPS: &str = "userProfileProps";
pub(crate) const USER_PROFILE_URL: &str = "userProfileUrl";
pub(crate) const WAITLIST_URL: &str = "waitlistUrl";
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use = "builders are consuming; assign or chain the returned value"]
pub struct JsonOptions {
value: Value,
}
impl Default for JsonOptions {
fn default() -> Self {
Self::new()
}
}
impl JsonOptions {
pub fn new() -> Self {
Self {
value: Value::Object(Map::new()),
}
}
pub fn from_value(value: Value) -> Self {
Self { value }
}
pub fn option(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
if !self.value.is_object() {
self.value = Value::Object(Map::new());
}
if let Value::Object(map) = &mut self.value {
map.insert(key.into(), value.into());
}
self
}
#[must_use = "into_value returns the built options; it does not send them anywhere"]
pub fn into_value(self) -> Value {
self.value
}
}
impl From<JsonOptions> for Value {
fn from(options: JsonOptions) -> Self {
options.into_value()
}
}
impl From<&JsonOptions> for Value {
fn from(options: &JsonOptions) -> Self {
options.value.clone()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Routing {
Hash,
Path,
}
impl Routing {
pub fn as_str(self) -> &'static str {
match self {
Self::Hash => "hash",
Self::Path => "path",
}
}
}
impl From<Routing> for Value {
fn from(routing: Routing) -> Self {
Value::String(routing.as_str().into())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UserProfileMode {
Modal,
Navigation,
}
impl UserProfileMode {
pub fn as_str(self) -> &'static str {
match self {
Self::Modal => "modal",
Self::Navigation => "navigation",
}
}
}
impl From<UserProfileMode> for Value {
fn from(mode: UserProfileMode) -> Self {
Value::String(mode.as_str().into())
}
}
macro_rules! option_wrapper {
($name:ident, $doc:literal) => {
#[doc = $doc]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[must_use = "builders are consuming; assign or chain the returned value"]
pub struct $name {
inner: JsonOptions,
}
impl $name {
pub fn new() -> Self {
Self {
inner: JsonOptions::new(),
}
}
pub fn from_value(value: Value) -> Self {
Self {
inner: JsonOptions::from_value(value),
}
}
pub fn option(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.inner = self.inner.option(key, value);
self
}
#[must_use = "into_value returns the built options; it does not send them anywhere"]
pub fn into_value(self) -> Value {
self.inner.into_value()
}
}
impl From<$name> for Value {
fn from(options: $name) -> Self {
options.into_value()
}
}
impl From<&$name> for Value {
fn from(options: &$name) -> Self {
options.inner.value.clone()
}
}
impl From<Value> for $name {
fn from(value: Value) -> Self {
Self::from_value(value)
}
}
};
}
option_wrapper!(
ClerkOptions,
"Options forwarded to `Clerk.load(...)` by `ClerkProvider`."
);
option_wrapper!(SignInOptions, "Options for sign-in UI and modal flows.");
option_wrapper!(SignUpOptions, "Options for sign-up UI and modal flows.");
option_wrapper!(UserButtonOptions, "Options for the Clerk user button.");
option_wrapper!(UserProfileOptions, "Options for the Clerk user profile UI.");
option_wrapper!(
CreateOrganizationOptions,
"Options for the Clerk create-organization UI."
);
option_wrapper!(
OrganizationProfileOptions,
"Options for the Clerk organization profile UI."
);
option_wrapper!(
OrganizationSwitcherOptions,
"Options for the Clerk organization switcher UI."
);
option_wrapper!(
OrganizationListOptions,
"Options for the Clerk organization list UI."
);
option_wrapper!(WaitlistOptions, "Options for the Clerk waitlist UI.");
option_wrapper!(
TaskSetupMFAOptions,
"Options for the Clerk task setup-MFA UI."
);
option_wrapper!(RedirectOptions, "Options for Clerk redirect helpers.");
option_wrapper!(SignOutOptions, "Options for `Clerk.signOut(...)`.");
option_wrapper!(GetTokenOptions, "Options for `session.getToken(...)`.");
macro_rules! option_setters {
($owner:ident { $($rest:tt)* }) => {
impl $owner {
option_setters!(@items $($rest)*);
}
};
(@items) => {};
(@items $(#[$doc:meta])* string $name:ident / $maybe:ident => $key:path; $($rest:tt)*) => {
$(#[$doc])*
pub fn $name(self, value: impl Into<String>) -> Self {
self.option($key, value.into())
}
#[allow(dead_code)]
pub(crate) fn $maybe(self, value: Option<String>) -> Self {
match value {
Some(value) => self.$name(value),
None => self,
}
}
option_setters!(@items $($rest)*);
};
(@items $(#[$doc:meta])* bool $name:ident / $maybe:ident => $key:path; $($rest:tt)*) => {
$(#[$doc])*
pub fn $name(self, value: bool) -> Self {
self.option($key, value)
}
#[allow(dead_code)]
pub(crate) fn $maybe(self, value: Option<bool>) -> Self {
match value {
Some(value) => self.$name(value),
None => self,
}
}
option_setters!(@items $($rest)*);
};
(@items $(#[$doc:meta])* u64 $name:ident / $maybe:ident => $key:path; $($rest:tt)*) => {
$(#[$doc])*
pub fn $name(self, value: u64) -> Self {
self.option($key, value)
}
#[allow(dead_code)]
pub(crate) fn $maybe(self, value: Option<u64>) -> Self {
match value {
Some(value) => self.$name(value),
None => self,
}
}
option_setters!(@items $($rest)*);
};
(@items $(#[$doc:meta])* value $name:ident / $maybe:ident => $key:path; $($rest:tt)*) => {
$(#[$doc])*
pub fn $name(self, value: Value) -> Self {
self.option($key, value)
}
#[allow(dead_code)]
pub(crate) fn $maybe(self, value: Option<Value>) -> Self {
match value {
Some(value) => self.$name(value),
None => self,
}
}
option_setters!(@items $($rest)*);
};
(@items $(#[$doc:meta])* list $name:ident / $maybe:ident => $key:path; $($rest:tt)*) => {
$(#[$doc])*
pub fn $name(self, values: impl IntoIterator<Item = impl Into<String>>) -> Self {
let values: Vec<String> = values.into_iter().map(Into::into).collect();
self.option($key, serde_json::json!(values))
}
#[allow(dead_code)]
pub(crate) fn $maybe(self, values: Option<Vec<String>>) -> Self {
match values {
Some(values) => self.$name(values),
None => self,
}
}
option_setters!(@items $($rest)*);
};
(@items $(#[$doc:meta])* enum($ty:ty) $name:ident / $maybe:ident => $key:path; $($rest:tt)*) => {
$(#[$doc])*
pub fn $name(self, value: $ty) -> Self {
self.option($key, value)
}
#[allow(dead_code)]
pub(crate) fn $maybe(self, value: Option<$ty>) -> Self {
match value {
Some(value) => self.$name(value),
None => self,
}
}
option_setters!(@items $($rest)*);
};
}
option_setters!(ClerkOptions {
string sign_in_url / maybe_sign_in_url => keys::SIGN_IN_URL;
string sign_up_url / maybe_sign_up_url => keys::SIGN_UP_URL;
string sign_in_fallback_redirect_url / maybe_sign_in_fallback_redirect_url => keys::SIGN_IN_FALLBACK_REDIRECT_URL;
string sign_in_force_redirect_url / maybe_sign_in_force_redirect_url => keys::SIGN_IN_FORCE_REDIRECT_URL;
string sign_up_fallback_redirect_url / maybe_sign_up_fallback_redirect_url => keys::SIGN_UP_FALLBACK_REDIRECT_URL;
string sign_up_force_redirect_url / maybe_sign_up_force_redirect_url => keys::SIGN_UP_FORCE_REDIRECT_URL;
string after_sign_out_url / maybe_after_sign_out_url => keys::AFTER_SIGN_OUT_URL;
string after_multi_session_single_sign_out_url / maybe_after_multi_session_single_sign_out_url => keys::AFTER_MULTI_SESSION_SINGLE_SIGN_OUT_URL;
string after_switch_session_url / maybe_after_switch_session_url => keys::AFTER_SWITCH_SESSION_URL;
string waitlist_url / maybe_waitlist_url => keys::WAITLIST_URL;
string user_profile_url / maybe_user_profile_url => keys::USER_PROFILE_URL;
string organization_profile_url / maybe_organization_profile_url => keys::ORGANIZATION_PROFILE_URL;
string create_organization_url / maybe_create_organization_url => keys::CREATE_ORGANIZATION_URL;
string proxy_url / maybe_proxy_url => keys::PROXY_URL;
string domain / maybe_domain => keys::DOMAIN;
bool is_satellite / maybe_is_satellite => keys::IS_SATELLITE;
bool satellite_auto_sync / maybe_satellite_auto_sync => keys::SATELLITE_AUTO_SYNC;
bool prefetch_ui / maybe_prefetch_ui => keys::PREFETCH_UI;
list allowed_redirect_origins / maybe_allowed_redirect_origins => keys::ALLOWED_REDIRECT_ORIGINS;
list allowed_redirect_protocols / maybe_allowed_redirect_protocols => keys::ALLOWED_REDIRECT_PROTOCOLS;
value appearance / maybe_appearance => keys::APPEARANCE;
value localization / maybe_localization => keys::LOCALIZATION;
value task_urls / maybe_task_urls => keys::TASK_URLS;
});
option_setters!(SignInOptions {
enum(Routing) routing / maybe_routing => keys::ROUTING;
string path / maybe_path => keys::PATH;
string sign_up_url / maybe_sign_up_url => keys::SIGN_UP_URL;
string waitlist_url / maybe_waitlist_url => keys::WAITLIST_URL;
string force_redirect_url / maybe_force_redirect_url => keys::FORCE_REDIRECT_URL;
string fallback_redirect_url / maybe_fallback_redirect_url => keys::FALLBACK_REDIRECT_URL;
string sign_up_force_redirect_url / maybe_sign_up_force_redirect_url => keys::SIGN_UP_FORCE_REDIRECT_URL;
string sign_up_fallback_redirect_url / maybe_sign_up_fallback_redirect_url => keys::SIGN_UP_FALLBACK_REDIRECT_URL;
value initial_values / maybe_initial_values => keys::INITIAL_VALUES;
bool transferable / maybe_transferable => keys::TRANSFERABLE;
value appearance / maybe_appearance => keys::APPEARANCE;
});
option_setters!(SignUpOptions {
enum(Routing) routing / maybe_routing => keys::ROUTING;
string path / maybe_path => keys::PATH;
string sign_in_url / maybe_sign_in_url => keys::SIGN_IN_URL;
string waitlist_url / maybe_waitlist_url => keys::WAITLIST_URL;
string force_redirect_url / maybe_force_redirect_url => keys::FORCE_REDIRECT_URL;
string fallback_redirect_url / maybe_fallback_redirect_url => keys::FALLBACK_REDIRECT_URL;
string sign_in_force_redirect_url / maybe_sign_in_force_redirect_url => keys::SIGN_IN_FORCE_REDIRECT_URL;
string sign_in_fallback_redirect_url / maybe_sign_in_fallback_redirect_url => keys::SIGN_IN_FALLBACK_REDIRECT_URL;
value initial_values / maybe_initial_values => keys::INITIAL_VALUES;
value appearance / maybe_appearance => keys::APPEARANCE;
});
option_setters!(UserButtonOptions {
string after_sign_out_url / maybe_after_sign_out_url => keys::AFTER_SIGN_OUT_URL;
string after_switch_session_url / maybe_after_switch_session_url => keys::AFTER_SWITCH_SESSION_URL;
string sign_in_url / maybe_sign_in_url => keys::SIGN_IN_URL;
bool show_name / maybe_show_name => keys::SHOW_NAME;
bool default_open / maybe_default_open => keys::DEFAULT_OPEN;
enum(UserProfileMode) user_profile_mode / maybe_user_profile_mode => keys::USER_PROFILE_MODE;
string user_profile_url / maybe_user_profile_url => keys::USER_PROFILE_URL;
value user_profile_props / maybe_user_profile_props => keys::USER_PROFILE_PROPS;
value appearance / maybe_appearance => keys::APPEARANCE;
});
option_setters!(UserProfileOptions {
enum(Routing) routing / maybe_routing => keys::ROUTING;
string path / maybe_path => keys::PATH;
value appearance / maybe_appearance => keys::APPEARANCE;
});
option_setters!(CreateOrganizationOptions {
enum(Routing) routing / maybe_routing => keys::ROUTING;
string path / maybe_path => keys::PATH;
string after_create_organization_url / maybe_after_create_organization_url => keys::AFTER_CREATE_ORGANIZATION_URL;
value appearance / maybe_appearance => keys::APPEARANCE;
});
option_setters!(OrganizationProfileOptions {
enum(Routing) routing / maybe_routing => keys::ROUTING;
string path / maybe_path => keys::PATH;
value appearance / maybe_appearance => keys::APPEARANCE;
});
option_setters!(OrganizationSwitcherOptions {
string create_organization_url / maybe_create_organization_url => keys::CREATE_ORGANIZATION_URL;
string after_create_organization_url / maybe_after_create_organization_url => keys::AFTER_CREATE_ORGANIZATION_URL;
string organization_profile_url / maybe_organization_profile_url => keys::ORGANIZATION_PROFILE_URL;
value appearance / maybe_appearance => keys::APPEARANCE;
});
option_setters!(OrganizationListOptions {
string after_create_organization_url / maybe_after_create_organization_url => keys::AFTER_CREATE_ORGANIZATION_URL;
string after_select_organization_url / maybe_after_select_organization_url => keys::AFTER_SELECT_ORGANIZATION_URL;
value appearance / maybe_appearance => keys::APPEARANCE;
});
option_setters!(WaitlistOptions {
string after_join_waitlist_url / maybe_after_join_waitlist_url => keys::AFTER_JOIN_WAITLIST_URL;
value appearance / maybe_appearance => keys::APPEARANCE;
});
option_setters!(TaskSetupMFAOptions {
string redirect_url_complete / maybe_redirect_url_complete => keys::REDIRECT_URL_COMPLETE;
value appearance / maybe_appearance => keys::APPEARANCE;
});
option_setters!(RedirectOptions {
string force_redirect_url / maybe_force_redirect_url => keys::FORCE_REDIRECT_URL;
string fallback_redirect_url / maybe_fallback_redirect_url => keys::FALLBACK_REDIRECT_URL;
string sign_up_force_redirect_url / maybe_sign_up_force_redirect_url => keys::SIGN_UP_FORCE_REDIRECT_URL;
string sign_up_fallback_redirect_url / maybe_sign_up_fallback_redirect_url => keys::SIGN_UP_FALLBACK_REDIRECT_URL;
string sign_in_force_redirect_url / maybe_sign_in_force_redirect_url => keys::SIGN_IN_FORCE_REDIRECT_URL;
string sign_in_fallback_redirect_url / maybe_sign_in_fallback_redirect_url => keys::SIGN_IN_FALLBACK_REDIRECT_URL;
});
option_setters!(SignOutOptions {
string redirect_url / maybe_redirect_url => keys::REDIRECT_URL;
string session_id / maybe_session_id => keys::SESSION_ID;
});
option_setters!(GetTokenOptions {
string template / maybe_template => keys::TEMPLATE;
string organization_id / maybe_organization_id => keys::ORGANIZATION_ID;
u64 leeway_in_seconds / maybe_leeway_in_seconds => keys::LEEWAY_IN_SECONDS;
bool skip_cache / maybe_skip_cache => keys::SKIP_CACHE;
});
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn clerk_options_emit_clerk_keys() {
let value = ClerkOptions::new()
.sign_in_url("/si")
.sign_up_url("/su")
.sign_in_fallback_redirect_url("/sifb")
.sign_in_force_redirect_url("/sif")
.sign_up_fallback_redirect_url("/sufb")
.sign_up_force_redirect_url("/suf")
.after_sign_out_url("/aso")
.after_multi_session_single_sign_out_url("/amsso")
.after_switch_session_url("/ass")
.waitlist_url("/wl")
.user_profile_url("/up")
.organization_profile_url("/op")
.create_organization_url("/co")
.proxy_url("/proxy")
.domain("clerk.example.com")
.is_satellite(true)
.satellite_auto_sync(false)
.prefetch_ui(true)
.allowed_redirect_origins(["https://a.example"])
.allowed_redirect_protocols(["https"])
.appearance(json!({"variables": {}}))
.localization(json!({"locale": "en-US"}))
.into_value();
assert_eq!(
value,
json!({
"signInUrl": "/si",
"signUpUrl": "/su",
"signInFallbackRedirectUrl": "/sifb",
"signInForceRedirectUrl": "/sif",
"signUpFallbackRedirectUrl": "/sufb",
"signUpForceRedirectUrl": "/suf",
"afterSignOutUrl": "/aso",
"afterMultiSessionSingleSignOutUrl": "/amsso",
"afterSwitchSessionUrl": "/ass",
"waitlistUrl": "/wl",
"userProfileUrl": "/up",
"organizationProfileUrl": "/op",
"createOrganizationUrl": "/co",
"proxyUrl": "/proxy",
"domain": "clerk.example.com",
"isSatellite": true,
"satelliteAutoSync": false,
"prefetchUI": true,
"allowedRedirectOrigins": ["https://a.example"],
"allowedRedirectProtocols": ["https"],
"appearance": {"variables": {}},
"localization": {"locale": "en-US"},
})
);
}
#[test]
fn clerk_options_emit_session_task_urls() {
let value = ClerkOptions::new()
.task_urls(json!({ "setup-mfa": "/onboarding/mfa" }))
.into_value();
assert_eq!(
value,
json!({ "taskUrls": { "setup-mfa": "/onboarding/mfa" } })
);
}
#[test]
fn sign_in_options_emit_clerk_keys() {
let value = SignInOptions::new()
.routing(Routing::Path)
.path("/sign-in")
.sign_up_url("/su")
.waitlist_url("/wl")
.force_redirect_url("/f")
.fallback_redirect_url("/fb")
.sign_up_force_redirect_url("/suf")
.sign_up_fallback_redirect_url("/sufb")
.initial_values(json!({"emailAddress": "a@example.com"}))
.transferable(true)
.appearance(json!({"variables": {}}))
.into_value();
assert_eq!(
value,
json!({
"routing": "path",
"path": "/sign-in",
"signUpUrl": "/su",
"waitlistUrl": "/wl",
"forceRedirectUrl": "/f",
"fallbackRedirectUrl": "/fb",
"signUpForceRedirectUrl": "/suf",
"signUpFallbackRedirectUrl": "/sufb",
"initialValues": {"emailAddress": "a@example.com"},
"transferable": true,
"appearance": {"variables": {}},
})
);
}
#[test]
fn sign_up_options_emit_clerk_keys() {
let value = SignUpOptions::new()
.routing(Routing::Hash)
.path("/sign-up")
.sign_in_url("/si")
.waitlist_url("/wl")
.force_redirect_url("/f")
.fallback_redirect_url("/fb")
.sign_in_force_redirect_url("/sif")
.sign_in_fallback_redirect_url("/sifb")
.initial_values(json!({"username": "a"}))
.appearance(json!({}))
.into_value();
assert_eq!(
value,
json!({
"routing": "hash",
"path": "/sign-up",
"signInUrl": "/si",
"waitlistUrl": "/wl",
"forceRedirectUrl": "/f",
"fallbackRedirectUrl": "/fb",
"signInForceRedirectUrl": "/sif",
"signInFallbackRedirectUrl": "/sifb",
"initialValues": {"username": "a"},
"appearance": {},
})
);
}
#[test]
fn user_button_options_emit_clerk_keys() {
let value = UserButtonOptions::new()
.after_sign_out_url("/aso")
.after_switch_session_url("/ass")
.sign_in_url("/si")
.show_name(true)
.default_open(false)
.user_profile_mode(UserProfileMode::Navigation)
.user_profile_url("/up")
.user_profile_props(json!({"appearance": {}}))
.appearance(json!({}))
.into_value();
assert_eq!(
value,
json!({
"afterSignOutUrl": "/aso",
"afterSwitchSessionUrl": "/ass",
"signInUrl": "/si",
"showName": true,
"defaultOpen": false,
"userProfileMode": "navigation",
"userProfileUrl": "/up",
"userProfileProps": {"appearance": {}},
"appearance": {},
})
);
}
#[test]
fn widget_options_emit_clerk_keys() {
assert_eq!(
UserProfileOptions::new()
.routing(Routing::Path)
.path("/profile")
.appearance(json!({}))
.into_value(),
json!({"routing": "path", "path": "/profile", "appearance": {}})
);
assert_eq!(
CreateOrganizationOptions::new()
.routing(Routing::Hash)
.path("/create-org")
.after_create_organization_url("/org")
.appearance(json!({}))
.into_value(),
json!({
"routing": "hash",
"path": "/create-org",
"afterCreateOrganizationUrl": "/org",
"appearance": {},
})
);
assert_eq!(
OrganizationProfileOptions::new()
.routing(Routing::Path)
.path("/org-profile")
.appearance(json!({}))
.into_value(),
json!({"routing": "path", "path": "/org-profile", "appearance": {}})
);
assert_eq!(
OrganizationSwitcherOptions::new()
.create_organization_url("/co")
.after_create_organization_url("/aco")
.organization_profile_url("/op")
.appearance(json!({}))
.into_value(),
json!({
"createOrganizationUrl": "/co",
"afterCreateOrganizationUrl": "/aco",
"organizationProfileUrl": "/op",
"appearance": {},
})
);
assert_eq!(
OrganizationListOptions::new()
.after_create_organization_url("/aco")
.after_select_organization_url("/aso")
.appearance(json!({}))
.into_value(),
json!({
"afterCreateOrganizationUrl": "/aco",
"afterSelectOrganizationUrl": "/aso",
"appearance": {},
})
);
assert_eq!(
WaitlistOptions::new()
.after_join_waitlist_url("/ajw")
.appearance(json!({}))
.into_value(),
json!({"afterJoinWaitlistUrl": "/ajw", "appearance": {}})
);
}
#[test]
fn task_setup_mfa_options_emit_clerk_keys() {
assert_eq!(
TaskSetupMFAOptions::new()
.redirect_url_complete("/onboarding/done")
.appearance(json!({}))
.into_value(),
json!({ "redirectUrlComplete": "/onboarding/done", "appearance": {} })
);
}
#[test]
fn action_options_emit_clerk_keys() {
assert_eq!(
RedirectOptions::new()
.force_redirect_url("/f")
.fallback_redirect_url("/fb")
.sign_up_force_redirect_url("/suf")
.sign_up_fallback_redirect_url("/sufb")
.sign_in_force_redirect_url("/sif")
.sign_in_fallback_redirect_url("/sifb")
.into_value(),
json!({
"forceRedirectUrl": "/f",
"fallbackRedirectUrl": "/fb",
"signUpForceRedirectUrl": "/suf",
"signUpFallbackRedirectUrl": "/sufb",
"signInForceRedirectUrl": "/sif",
"signInFallbackRedirectUrl": "/sifb",
})
);
assert_eq!(
SignOutOptions::new()
.redirect_url("/after")
.session_id("sess_1")
.into_value(),
json!({"redirectUrl": "/after", "sessionId": "sess_1"})
);
assert_eq!(
GetTokenOptions::new()
.template("supabase")
.organization_id("org_1")
.leeway_in_seconds(10)
.skip_cache(true)
.into_value(),
json!({
"template": "supabase",
"organizationId": "org_1",
"leewayInSeconds": 10,
"skipCache": true,
})
);
}
#[test]
fn maybe_setters_with_none_leave_options_untouched() {
let value = SignInOptions::new()
.maybe_routing(None)
.maybe_path(None)
.maybe_sign_up_url(None)
.maybe_initial_values(None)
.maybe_transferable(None)
.into_value();
assert_eq!(value, json!({}));
let untouched = SignInOptions::from_value(json!({"signUpUrl": "/kept"}))
.maybe_sign_up_url(None)
.into_value();
assert_eq!(untouched, json!({"signUpUrl": "/kept"}));
}
#[test]
fn builders_accept_a_raw_value_via_from() {
let value = json!({ "signUpUrl": "/kept", "path": "/p" });
assert_eq!(
SignInOptions::from(value.clone()).into_value(),
SignInOptions::from_value(value).into_value(),
);
}
#[test]
fn explicit_props_win_over_raw_options() {
let value = SignInOptions::from_value(json!({"signUpUrl": "/old", "path": "/kept"}))
.sign_up_url("/new")
.into_value();
assert_eq!(value, json!({"signUpUrl": "/new", "path": "/kept"}));
}
#[test]
fn raw_null_options_stay_null_when_no_setter_runs() {
assert_eq!(
SignInOptions::from_value(serde_json::Value::Null)
.maybe_sign_up_url(None)
.into_value(),
serde_json::Value::Null
);
assert_eq!(
SignInOptions::from_value(serde_json::Value::Null)
.sign_up_url("/su")
.into_value(),
json!({"signUpUrl": "/su"})
);
}
}