mod newtypes;
mod version;
pub use newtypes::{ApiKey, ApiSecretKey, HostUrl, ShopDomain};
pub use version::ApiVersion;
use crate::auth::AuthScopes;
use crate::clients::ApiDeprecationInfo;
use crate::error::ConfigError;
use std::sync::Arc;
pub type DeprecationCallback = Arc<dyn Fn(&ApiDeprecationInfo) + Send + Sync>;
#[derive(Clone)]
pub struct ShopifyConfig {
api_key: ApiKey,
api_secret_key: ApiSecretKey,
old_api_secret_key: Option<ApiSecretKey>,
scopes: AuthScopes,
host: Option<HostUrl>,
api_version: ApiVersion,
is_embedded: bool,
user_agent_prefix: Option<String>,
deprecation_callback: Option<DeprecationCallback>,
}
impl std::fmt::Debug for ShopifyConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShopifyConfig")
.field("api_key", &self.api_key)
.field("api_secret_key", &self.api_secret_key)
.field("old_api_secret_key", &self.old_api_secret_key)
.field("scopes", &self.scopes)
.field("host", &self.host)
.field("api_version", &self.api_version)
.field("is_embedded", &self.is_embedded)
.field("user_agent_prefix", &self.user_agent_prefix)
.field(
"deprecation_callback",
&self.deprecation_callback.as_ref().map(|_| "<callback>"),
)
.finish()
}
}
impl ShopifyConfig {
#[must_use]
pub fn builder() -> ShopifyConfigBuilder {
ShopifyConfigBuilder::new()
}
#[must_use]
pub const fn api_key(&self) -> &ApiKey {
&self.api_key
}
#[must_use]
pub const fn api_secret_key(&self) -> &ApiSecretKey {
&self.api_secret_key
}
#[must_use]
pub const fn old_api_secret_key(&self) -> Option<&ApiSecretKey> {
self.old_api_secret_key.as_ref()
}
#[must_use]
pub const fn scopes(&self) -> &AuthScopes {
&self.scopes
}
#[must_use]
pub const fn host(&self) -> Option<&HostUrl> {
self.host.as_ref()
}
#[must_use]
pub const fn api_version(&self) -> &ApiVersion {
&self.api_version
}
#[must_use]
pub const fn is_embedded(&self) -> bool {
self.is_embedded
}
#[must_use]
pub fn user_agent_prefix(&self) -> Option<&str> {
self.user_agent_prefix.as_deref()
}
#[must_use]
pub fn deprecation_callback(&self) -> Option<&DeprecationCallback> {
self.deprecation_callback.as_ref()
}
}
const _: fn() = || {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ShopifyConfig>();
};
#[derive(Default)]
pub struct ShopifyConfigBuilder {
api_key: Option<ApiKey>,
api_secret_key: Option<ApiSecretKey>,
old_api_secret_key: Option<ApiSecretKey>,
scopes: Option<AuthScopes>,
host: Option<HostUrl>,
api_version: Option<ApiVersion>,
is_embedded: Option<bool>,
user_agent_prefix: Option<String>,
reject_deprecated_versions: bool,
deprecation_callback: Option<DeprecationCallback>,
}
impl std::fmt::Debug for ShopifyConfigBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShopifyConfigBuilder")
.field("api_key", &self.api_key)
.field("api_secret_key", &self.api_secret_key)
.field("old_api_secret_key", &self.old_api_secret_key)
.field("scopes", &self.scopes)
.field("host", &self.host)
.field("api_version", &self.api_version)
.field("is_embedded", &self.is_embedded)
.field("user_agent_prefix", &self.user_agent_prefix)
.field("reject_deprecated_versions", &self.reject_deprecated_versions)
.field(
"deprecation_callback",
&self.deprecation_callback.as_ref().map(|_| "<callback>"),
)
.finish()
}
}
impl ShopifyConfigBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn api_key(mut self, key: ApiKey) -> Self {
self.api_key = Some(key);
self
}
#[must_use]
pub fn api_secret_key(mut self, key: ApiSecretKey) -> Self {
self.api_secret_key = Some(key);
self
}
#[must_use]
pub fn old_api_secret_key(mut self, key: ApiSecretKey) -> Self {
self.old_api_secret_key = Some(key);
self
}
#[must_use]
pub fn scopes(mut self, scopes: AuthScopes) -> Self {
self.scopes = Some(scopes);
self
}
#[must_use]
pub fn host(mut self, host: HostUrl) -> Self {
self.host = Some(host);
self
}
#[must_use]
pub fn api_version(mut self, version: ApiVersion) -> Self {
self.api_version = Some(version);
self
}
#[must_use]
pub const fn is_embedded(mut self, embedded: bool) -> Self {
self.is_embedded = Some(embedded);
self
}
#[must_use]
pub fn user_agent_prefix(mut self, prefix: impl Into<String>) -> Self {
self.user_agent_prefix = Some(prefix.into());
self
}
#[must_use]
pub const fn reject_deprecated_versions(mut self, reject: bool) -> Self {
self.reject_deprecated_versions = reject;
self
}
#[must_use]
pub fn on_deprecation<F>(mut self, callback: F) -> Self
where
F: Fn(&ApiDeprecationInfo) + Send + Sync + 'static,
{
self.deprecation_callback = Some(Arc::new(callback));
self
}
pub fn build(self) -> Result<ShopifyConfig, ConfigError> {
let api_key = self
.api_key
.ok_or(ConfigError::MissingRequiredField { field: "api_key" })?;
let api_secret_key = self
.api_secret_key
.ok_or(ConfigError::MissingRequiredField {
field: "api_secret_key",
})?;
let api_version = self.api_version.unwrap_or_else(ApiVersion::latest);
if api_version.is_deprecated() {
if self.reject_deprecated_versions {
return Err(ConfigError::DeprecatedApiVersion {
version: api_version.to_string(),
latest: ApiVersion::latest().to_string(),
});
}
tracing::warn!(
version = %api_version,
latest = %ApiVersion::latest(),
"Using deprecated API version '{}'. Please upgrade to '{}' or a newer supported version.",
api_version,
ApiVersion::latest()
);
}
Ok(ShopifyConfig {
api_key,
api_secret_key,
old_api_secret_key: self.old_api_secret_key,
scopes: self.scopes.unwrap_or_default(),
host: self.host,
api_version,
is_embedded: self.is_embedded.unwrap_or(true),
user_agent_prefix: self.user_agent_prefix,
deprecation_callback: self.deprecation_callback,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_requires_api_key() {
let result = ShopifyConfigBuilder::new()
.api_secret_key(ApiSecretKey::new("secret").unwrap())
.build();
assert!(matches!(
result,
Err(ConfigError::MissingRequiredField { field: "api_key" })
));
}
#[test]
fn test_builder_requires_api_secret_key() {
let result = ShopifyConfigBuilder::new()
.api_key(ApiKey::new("key").unwrap())
.build();
assert!(matches!(
result,
Err(ConfigError::MissingRequiredField {
field: "api_secret_key"
})
));
}
#[test]
fn test_builder_provides_sensible_defaults() {
let config = ShopifyConfig::builder()
.api_key(ApiKey::new("key").unwrap())
.api_secret_key(ApiSecretKey::new("secret").unwrap())
.build()
.unwrap();
assert_eq!(config.api_version(), &ApiVersion::latest());
assert!(config.is_embedded());
assert!(config.scopes().is_empty());
assert!(config.host().is_none());
assert!(config.user_agent_prefix().is_none());
assert!(config.old_api_secret_key().is_none());
}
#[test]
fn test_config_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ShopifyConfig>();
}
#[test]
fn test_config_is_clone_and_debug() {
let config = ShopifyConfig::builder()
.api_key(ApiKey::new("key").unwrap())
.api_secret_key(ApiSecretKey::new("secret").unwrap())
.build()
.unwrap();
let cloned = config.clone();
assert_eq!(cloned.api_key(), config.api_key());
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("ShopifyConfig"));
}
#[test]
fn test_builder_with_all_optional_fields() {
let scopes: AuthScopes = "read_products,write_orders".parse().unwrap();
let host = HostUrl::new("https://myapp.example.com").unwrap();
let config = ShopifyConfig::builder()
.api_key(ApiKey::new("key").unwrap())
.api_secret_key(ApiSecretKey::new("secret").unwrap())
.scopes(scopes.clone())
.host(host.clone())
.api_version(ApiVersion::V2024_10)
.is_embedded(false)
.user_agent_prefix("MyApp/1.0")
.build()
.unwrap();
assert_eq!(config.api_version(), &ApiVersion::V2024_10);
assert!(!config.is_embedded());
assert_eq!(config.host(), Some(&host));
assert_eq!(config.user_agent_prefix(), Some("MyApp/1.0"));
}
#[test]
fn test_old_api_secret_key_configuration() {
let config = ShopifyConfig::builder()
.api_key(ApiKey::new("key").unwrap())
.api_secret_key(ApiSecretKey::new("new-secret").unwrap())
.old_api_secret_key(ApiSecretKey::new("old-secret").unwrap())
.build()
.unwrap();
assert!(config.old_api_secret_key().is_some());
assert_eq!(config.old_api_secret_key().unwrap().as_ref(), "old-secret");
}
#[test]
fn test_old_api_secret_key_is_optional() {
let config = ShopifyConfig::builder()
.api_key(ApiKey::new("key").unwrap())
.api_secret_key(ApiSecretKey::new("secret").unwrap())
.build()
.unwrap();
assert!(config.old_api_secret_key().is_none());
}
#[test]
fn test_build_allows_deprecated_version_by_default() {
let result = ShopifyConfig::builder()
.api_key(ApiKey::new("key").unwrap())
.api_secret_key(ApiSecretKey::new("secret").unwrap())
.api_version(ApiVersion::V2024_01)
.build();
assert!(result.is_ok());
assert_eq!(result.unwrap().api_version(), &ApiVersion::V2024_01);
}
#[test]
fn test_build_fails_when_reject_deprecated() {
let result = ShopifyConfig::builder()
.api_key(ApiKey::new("key").unwrap())
.api_secret_key(ApiSecretKey::new("secret").unwrap())
.api_version(ApiVersion::V2024_01)
.reject_deprecated_versions(true)
.build();
assert!(matches!(
result,
Err(ConfigError::DeprecatedApiVersion { version, latest })
if version == "2024-01" && latest == ApiVersion::latest().to_string()
));
}
#[test]
fn test_build_succeeds_with_supported_version_when_reject_deprecated() {
let result = ShopifyConfig::builder()
.api_key(ApiKey::new("key").unwrap())
.api_secret_key(ApiSecretKey::new("secret").unwrap())
.api_version(ApiVersion::V2025_10)
.reject_deprecated_versions(true)
.build();
assert!(result.is_ok());
}
#[test]
fn test_build_succeeds_with_unstable_when_reject_deprecated() {
let result = ShopifyConfig::builder()
.api_key(ApiKey::new("key").unwrap())
.api_secret_key(ApiSecretKey::new("secret").unwrap())
.api_version(ApiVersion::Unstable)
.reject_deprecated_versions(true)
.build();
assert!(result.is_ok());
}
}