From a61dc772071a3becbc46fc33936303749b19a615 Mon Sep 17 00:00:00 2001 From: subotac <73706465+subotac@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:13:02 +0300 Subject: [PATCH 1/5] fix(core): deserialize duration config values from strings --- core/core/src/raw/time.rs | 46 +++++++++++++++++++++++ core/services/cloudflare-kv/src/config.rs | 12 ++++++ core/services/memcached/src/config.rs | 12 ++++++ core/services/redis/src/config.rs | 12 ++++++ 4 files changed, 82 insertions(+) diff --git a/core/core/src/raw/time.rs b/core/core/src/raw/time.rs index f1a3e3c4dc6d..b3e66b2312af 100644 --- a/core/core/src/raw/time.rs +++ b/core/core/src/raw/time.rs @@ -19,6 +19,8 @@ use crate::*; use jiff::SignedDuration; +use serde::Deserialize; +use serde::Deserializer; use std::fmt; use std::ops::{Add, AddAssign, Sub, SubAssign}; use std::str::FromStr; @@ -29,6 +31,13 @@ pub use std::time::{Instant, SystemTime}; #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] pub use web_time::{Instant, SystemTime}; +#[derive(Deserialize)] +#[serde(untagged)] +enum ConfigDuration { + Duration(Duration), + String(String), +} + /// An instant in time represented as the number of nanoseconds since the Unix epoch. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Timestamp(jiff::Timestamp); @@ -271,10 +280,34 @@ pub fn signed_to_duration(value: &str) -> Result { }) } +/// Deserialize an optional [`Duration`] from either its serde representation or +/// a string accepted by [`signed_to_duration`]. +pub fn deserialize_option_duration<'de, D>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, +{ + Option::::deserialize(deserializer)? + .map(|value| match value { + ConfigDuration::Duration(value) => Ok(value), + ConfigDuration::String(value) => { + signed_to_duration(&value).map_err(serde::de::Error::custom) + } + }) + .transpose() +} + #[cfg(test)] mod tests { use super::*; + #[derive(Deserialize)] + struct TestDurationConfig { + #[serde(default, deserialize_with = "deserialize_option_duration")] + duration: Option, + } + fn test_time() -> Timestamp { Timestamp("2022-03-01T08:12:34Z".parse().unwrap()) } @@ -304,4 +337,17 @@ mod tests { let v = Timestamp::parse_rfc2822(s).unwrap(); assert_eq!("Sat, 29 Oct 1994 19:43:31 GMT", v.format_http_date()); } + + #[test] + fn test_deserialize_option_duration() { + let friendly: TestDurationConfig = + serde_json::from_str(r#"{"duration":"1500ms"}"#).unwrap(); + assert_eq!(friendly.duration, Some(Duration::from_millis(1500))); + + let structured: TestDurationConfig = + serde_json::from_str(r#"{"duration":{"secs":5,"nanos":7}}"#).unwrap(); + assert_eq!(structured.duration, Some(Duration::new(5, 7))); + + assert!(serde_json::from_str::(r#"{"duration":"5"}"#).is_err()); + } } diff --git a/core/services/cloudflare-kv/src/config.rs b/core/services/cloudflare-kv/src/config.rs index cad44d7ea3e3..ceaad61708b4 100644 --- a/core/services/cloudflare-kv/src/config.rs +++ b/core/services/cloudflare-kv/src/config.rs @@ -34,6 +34,10 @@ pub struct CloudflareKvConfig { /// The namespace ID. Used as URI path parameter. pub namespace_id: Option, /// The default ttl for write operations. + /// + /// String configurations accept ISO-8601 (for example, `PT5M`) and friendly + /// (for example, `5m`) duration formats. + #[serde(default, deserialize_with = "deserialize_option_duration")] pub default_ttl: Option, /// Root within this backend. @@ -125,4 +129,12 @@ mod tests { assert!(CloudflareKvConfig::from_uri(&uri).is_err()); } + + #[test] + fn from_iter_parses_default_ttl() { + let cfg = CloudflareKvConfig::from_iter([("default_ttl".to_string(), "PT1M".to_string())]) + .unwrap(); + + assert_eq!(cfg.default_ttl, Some(Duration::from_secs(60))); + } } diff --git a/core/services/memcached/src/config.rs b/core/services/memcached/src/config.rs index df8ab16b0c45..b14dc7fcc5d8 100644 --- a/core/services/memcached/src/config.rs +++ b/core/services/memcached/src/config.rs @@ -44,6 +44,10 @@ pub struct MemcachedConfig { /// Memcached password, optional. pub password: Option, /// The default ttl for put operations. + /// + /// String configurations accept ISO-8601 (for example, `PT5M`) and friendly + /// (for example, `5m`) duration formats. + #[serde(default, deserialize_with = "deserialize_option_duration")] pub default_ttl: Option, /// The maximum number of connections allowed. /// @@ -101,4 +105,12 @@ mod tests { assert_eq!(cfg.root.as_deref(), Some("app/session")); Ok(()) } + + #[test] + fn from_iter_parses_default_ttl() -> Result<()> { + let cfg = MemcachedConfig::from_iter([("default_ttl".to_string(), "1500ms".to_string())])?; + + assert_eq!(cfg.default_ttl, Some(Duration::from_millis(1500))); + Ok(()) + } } diff --git a/core/services/redis/src/config.rs b/core/services/redis/src/config.rs index e617be4990a2..8ead8c459ca6 100644 --- a/core/services/redis/src/config.rs +++ b/core/services/redis/src/config.rs @@ -59,6 +59,10 @@ pub struct RedisConfig { /// default is db 0 pub db: i64, /// The default ttl for put operations. + /// + /// String configurations accept ISO-8601 (for example, `PT5M`) and friendly + /// (for example, `5m`) duration formats. + #[serde(default, deserialize_with = "deserialize_option_duration")] pub default_ttl: Option, } @@ -156,6 +160,14 @@ mod tests { Ok(()) } + #[test] + fn from_iter_parses_default_ttl() -> Result<()> { + let cfg = RedisConfig::from_iter([("default_ttl".to_string(), "5s".to_string())])?; + + assert_eq!(cfg.default_ttl, Some(Duration::from_secs(5))); + Ok(()) + } + #[test] fn test_redis_builder_interface() { // Test that RedisBuilder still works with the new implementation From cc2163bedf63160628a4313e1933ad4d89181b6b Mon Sep 17 00:00:00 2001 From: subotac <73706465+subotac@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:33:17 +0300 Subject: [PATCH 2/5] chore: update generated duration config docs --- .../org/apache/opendal/ServiceConfig.java | 6 ++++ bindings/python/python/opendal/config.py | 6 ++-- core/core/src/raw/time.rs | 1 + website/data/services.json | 32 +++++++++---------- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/bindings/java/src/main/java/org/apache/opendal/ServiceConfig.java b/bindings/java/src/main/java/org/apache/opendal/ServiceConfig.java index d545a3ee5fcc..0449c3daa5e1 100644 --- a/bindings/java/src/main/java/org/apache/opendal/ServiceConfig.java +++ b/bindings/java/src/main/java/org/apache/opendal/ServiceConfig.java @@ -549,6 +549,8 @@ class CloudflareKv implements ServiceConfig { public final String apiToken; /** *

The default ttl for write operations.

+ *

String configurations accept ISO-8601 (for example, PT5M) and friendly + * (for example, 5m) duration formats.

*/ public final Duration defaultTtl; /** @@ -1894,6 +1896,8 @@ class Memcached implements ServiceConfig { public final Long connectionPoolMaxSize; /** *

The default ttl for put operations.

+ *

String configurations accept ISO-8601 (for example, PT5M) and friendly + * (for example, 5m) duration formats.

*/ public final Duration defaultTtl; /** @@ -2817,6 +2821,8 @@ class Redis implements ServiceConfig { public final long db; /** *

The default ttl for put operations.

+ *

String configurations accept ISO-8601 (for example, PT5M) and friendly + * (for example, 5m) duration formats.

*/ public final Duration defaultTtl; /** diff --git a/bindings/python/python/opendal/config.py b/bindings/python/python/opendal/config.py index 54a4b7a6315a..c772c002e321 100644 --- a/bindings/python/python/opendal/config.py +++ b/bindings/python/python/opendal/config.py @@ -193,7 +193,7 @@ class CloudflareKvConfig(TypedDict): api_token: NotRequired[str] """The token used to authenticate with CloudFlare.""" default_ttl: NotRequired[str] - """The default ttl for write operations. A human readable duration string, e.g. "5s" (see https://docs.rs/humantime/latest/humantime/fn.parse_duration.html).""" + """The default ttl for write operations. String configurations accept ISO-8601 (for example, `PT5M`) and friendly (for example, `5m`) duration formats. A human readable duration string, e.g. "5s" (see https://docs.rs/humantime/latest/humantime/fn.parse_duration.html).""" namespace_id: NotRequired[str] """The namespace ID. Used as URI path parameter.""" root: NotRequired[str | os.PathLike[str]] @@ -457,7 +457,7 @@ class MemcachedConfig(TypedDict): connection_pool_max_size: NotRequired[int] """The maximum number of connections allowed. default is 10""" default_ttl: NotRequired[str] - """The default ttl for put operations. A human readable duration string, e.g. "5s" (see https://docs.rs/humantime/latest/humantime/fn.parse_duration.html).""" + """The default ttl for put operations. String configurations accept ISO-8601 (for example, `PT5M`) and friendly (for example, `5m`) duration formats. A human readable duration string, e.g. "5s" (see https://docs.rs/humantime/latest/humantime/fn.parse_duration.html).""" endpoint: NotRequired[str] """network address of the memcached service. For example: "tcp://localhost:11211\"""" password: NotRequired[str] @@ -689,7 +689,7 @@ class RedisConfig(TypedDict): db: Required[int] """the number of DBs redis can take is unlimited default is db 0""" default_ttl: NotRequired[str] - """The default ttl for put operations. A human readable duration string, e.g. "5s" (see https://docs.rs/humantime/latest/humantime/fn.parse_duration.html).""" + """The default ttl for put operations. String configurations accept ISO-8601 (for example, `PT5M`) and friendly (for example, `5m`) duration formats. A human readable duration string, e.g. "5s" (see https://docs.rs/humantime/latest/humantime/fn.parse_duration.html).""" endpoint: NotRequired[str] """network address of the Redis service. Can be "tcp://127.0.0.1:6379", e.g. default is "tcp://127.0.0.1:6379\"""" password: NotRequired[str] diff --git a/core/core/src/raw/time.rs b/core/core/src/raw/time.rs index b3e66b2312af..ed09a1d4d862 100644 --- a/core/core/src/raw/time.rs +++ b/core/core/src/raw/time.rs @@ -35,6 +35,7 @@ pub use web_time::{Instant, SystemTime}; #[serde(untagged)] enum ConfigDuration { Duration(Duration), + // String-backed config paths cannot provide Duration's structured serde representation. String(String), } diff --git a/website/data/services.json b/website/data/services.json index f9600f47118f..6fb277d43401 100644 --- a/website/data/services.json +++ b/website/data/services.json @@ -678,7 +678,7 @@ "type": "duration", "required": false, "group": "General", - "comments": "The default ttl for write operations." + "comments": "The default ttl for write operations.\n\nString configurations accept ISO-8601 (for example, `PT5M`) and friendly\n(for example, `5m`) duration formats." }, { "name": "root", @@ -693,19 +693,19 @@ "binding": "rust", "language": "rust", "minimal": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"cloudflare-kv\", [\n])?;", - "full": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"cloudflare-kv\", [\n // The token used to authenticate with CloudFlare.\n // (\"api_token\".to_string(), \"...\".to_string()),\n\n // The account ID used to authenticate with CloudFlare. Used as URI path parameter.\n // (\"account_id\".to_string(), \"...\".to_string()),\n\n // The namespace ID. Used as URI path parameter.\n // (\"namespace_id\".to_string(), \"...\".to_string()),\n\n // The default ttl for write operations.\n // (\"default_ttl\".to_string(), \"10s\".to_string()),\n\n // Root within this backend.\n // (\"root\".to_string(), \"...\".to_string()),\n])?;" + "full": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"cloudflare-kv\", [\n // The token used to authenticate with CloudFlare.\n // (\"api_token\".to_string(), \"...\".to_string()),\n\n // The account ID used to authenticate with CloudFlare. Used as URI path parameter.\n // (\"account_id\".to_string(), \"...\".to_string()),\n\n // The namespace ID. Used as URI path parameter.\n // (\"namespace_id\".to_string(), \"...\".to_string()),\n\n // The default ttl for write operations.\n //\n // String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n // (for example, `5m`) duration formats.\n // (\"default_ttl\".to_string(), \"10s\".to_string()),\n\n // Root within this backend.\n // (\"root\".to_string(), \"...\".to_string()),\n])?;" }, { "binding": "python", "language": "python", "minimal": "import opendal\n\noperator = opendal.Operator(\n \"cloudflare-kv\",\n)", - "full": "import opendal\n\noperator = opendal.Operator(\n \"cloudflare-kv\",\n # The token used to authenticate with CloudFlare.\n # api_token=\"...\",\n\n # The account ID used to authenticate with CloudFlare. Used as URI path parameter.\n # account_id=\"...\",\n\n # The namespace ID. Used as URI path parameter.\n # namespace_id=\"...\",\n\n # The default ttl for write operations.\n # default_ttl=\"10s\",\n\n # Root within this backend.\n # root=\"...\",\n)" + "full": "import opendal\n\noperator = opendal.Operator(\n \"cloudflare-kv\",\n # The token used to authenticate with CloudFlare.\n # api_token=\"...\",\n\n # The account ID used to authenticate with CloudFlare. Used as URI path parameter.\n # account_id=\"...\",\n\n # The namespace ID. Used as URI path parameter.\n # namespace_id=\"...\",\n\n # The default ttl for write operations.\n #\n # String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n # (for example, `5m`) duration formats.\n # default_ttl=\"10s\",\n\n # Root within this backend.\n # root=\"...\",\n)" }, { "binding": "go", "language": "go", "minimal": "import (\n\t\"github.com/apache/opendal-go-services/cloudflare-kv\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(cloudflare-kv.Scheme, opendal.OperatorOptions{\n})", - "full": "import (\n\t\"github.com/apache/opendal-go-services/cloudflare-kv\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(cloudflare-kv.Scheme, opendal.OperatorOptions{\n\t// The token used to authenticate with CloudFlare.\n\t// \"api_token\": \"...\",\n\n\t// The account ID used to authenticate with CloudFlare. Used as URI path parameter.\n\t// \"account_id\": \"...\",\n\n\t// The namespace ID. Used as URI path parameter.\n\t// \"namespace_id\": \"...\",\n\n\t// The default ttl for write operations.\n\t// \"default_ttl\": \"10s\",\n\n\t// Root within this backend.\n\t// \"root\": \"...\",\n})" + "full": "import (\n\t\"github.com/apache/opendal-go-services/cloudflare-kv\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(cloudflare-kv.Scheme, opendal.OperatorOptions{\n\t// The token used to authenticate with CloudFlare.\n\t// \"api_token\": \"...\",\n\n\t// The account ID used to authenticate with CloudFlare. Used as URI path parameter.\n\t// \"account_id\": \"...\",\n\n\t// The namespace ID. Used as URI path parameter.\n\t// \"namespace_id\": \"...\",\n\n\t// The default ttl for write operations.\n\t//\n\t// String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n\t// (for example, `5m`) duration formats.\n\t// \"default_ttl\": \"10s\",\n\n\t// Root within this backend.\n\t// \"root\": \"...\",\n})" } ] }, @@ -2603,7 +2603,7 @@ "type": "duration", "required": false, "group": "General", - "comments": "The default ttl for put operations." + "comments": "The default ttl for put operations.\n\nString configurations accept ISO-8601 (for example, `PT5M`) and friendly\n(for example, `5m`) duration formats." }, { "name": "connection_pool_max_size", @@ -2618,31 +2618,31 @@ "binding": "rust", "language": "rust", "minimal": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"memcached\", [\n])?;", - "full": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"memcached\", [\n // network address of the memcached service.\n //\n // For example: \"tcp://localhost:11211\"\n // (\"endpoint\".to_string(), \"...\".to_string()),\n\n // the working directory of the service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // (\"root\".to_string(), \"...\".to_string()),\n\n // Memcached username, optional.\n // (\"username\".to_string(), \"...\".to_string()),\n\n // Memcached password, optional.\n // (\"password\".to_string(), \"...\".to_string()),\n\n // The default ttl for put operations.\n // (\"default_ttl\".to_string(), \"10s\".to_string()),\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // (\"connection_pool_max_size\".to_string(), \"1000\".to_string()),\n])?;" + "full": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"memcached\", [\n // network address of the memcached service.\n //\n // For example: \"tcp://localhost:11211\"\n // (\"endpoint\".to_string(), \"...\".to_string()),\n\n // the working directory of the service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // (\"root\".to_string(), \"...\".to_string()),\n\n // Memcached username, optional.\n // (\"username\".to_string(), \"...\".to_string()),\n\n // Memcached password, optional.\n // (\"password\".to_string(), \"...\".to_string()),\n\n // The default ttl for put operations.\n //\n // String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n // (for example, `5m`) duration formats.\n // (\"default_ttl\".to_string(), \"10s\".to_string()),\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // (\"connection_pool_max_size\".to_string(), \"1000\".to_string()),\n])?;" }, { "binding": "java", "language": "java", "minimal": "import java.util.HashMap;\nimport java.util.Map;\nimport org.apache.opendal.Operator;\n\nMap config = new HashMap<>();\nOperator operator = Operator.of(\"memcached\", config);", - "full": "import java.util.HashMap;\nimport java.util.Map;\nimport org.apache.opendal.Operator;\n\nMap config = new HashMap<>();\n// network address of the memcached service.\n//\n// For example: \"tcp://localhost:11211\"\n// config.put(\"endpoint\", \"...\");\n\n// the working directory of the service. Can be \"/path/to/dir\"\n//\n// default is \"/\"\n// config.put(\"root\", \"...\");\n\n// Memcached username, optional.\n// config.put(\"username\", \"...\");\n\n// Memcached password, optional.\n// config.put(\"password\", \"...\");\n\n// The default ttl for put operations.\n// config.put(\"default_ttl\", \"10s\");\n\n// The maximum number of connections allowed.\n//\n// default is 10\n// config.put(\"connection_pool_max_size\", \"1000\");\nOperator operator = Operator.of(\"memcached\", config);" + "full": "import java.util.HashMap;\nimport java.util.Map;\nimport org.apache.opendal.Operator;\n\nMap config = new HashMap<>();\n// network address of the memcached service.\n//\n// For example: \"tcp://localhost:11211\"\n// config.put(\"endpoint\", \"...\");\n\n// the working directory of the service. Can be \"/path/to/dir\"\n//\n// default is \"/\"\n// config.put(\"root\", \"...\");\n\n// Memcached username, optional.\n// config.put(\"username\", \"...\");\n\n// Memcached password, optional.\n// config.put(\"password\", \"...\");\n\n// The default ttl for put operations.\n//\n// String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n// (for example, `5m`) duration formats.\n// config.put(\"default_ttl\", \"10s\");\n\n// The maximum number of connections allowed.\n//\n// default is 10\n// config.put(\"connection_pool_max_size\", \"1000\");\nOperator operator = Operator.of(\"memcached\", config);" }, { "binding": "python", "language": "python", "minimal": "import opendal\n\noperator = opendal.Operator(\n \"memcached\",\n)", - "full": "import opendal\n\noperator = opendal.Operator(\n \"memcached\",\n # network address of the memcached service.\n #\n # For example: \"tcp://localhost:11211\"\n # endpoint=\"...\",\n\n # the working directory of the service. Can be \"/path/to/dir\"\n #\n # default is \"/\"\n # root=\"...\",\n\n # Memcached username, optional.\n # username=\"...\",\n\n # Memcached password, optional.\n # password=\"...\",\n\n # The default ttl for put operations.\n # default_ttl=\"10s\",\n\n # The maximum number of connections allowed.\n #\n # default is 10\n # connection_pool_max_size=\"1000\",\n)" + "full": "import opendal\n\noperator = opendal.Operator(\n \"memcached\",\n # network address of the memcached service.\n #\n # For example: \"tcp://localhost:11211\"\n # endpoint=\"...\",\n\n # the working directory of the service. Can be \"/path/to/dir\"\n #\n # default is \"/\"\n # root=\"...\",\n\n # Memcached username, optional.\n # username=\"...\",\n\n # Memcached password, optional.\n # password=\"...\",\n\n # The default ttl for put operations.\n #\n # String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n # (for example, `5m`) duration formats.\n # default_ttl=\"10s\",\n\n # The maximum number of connections allowed.\n #\n # default is 10\n # connection_pool_max_size=\"1000\",\n)" }, { "binding": "nodejs", "language": "javascript", "minimal": "import { Operator } from \"opendal\";\n\nconst operator = new Operator(\"memcached\", {\n});", - "full": "import { Operator } from \"opendal\";\n\nconst operator = new Operator(\"memcached\", {\n // network address of the memcached service.\n //\n // For example: \"tcp://localhost:11211\"\n // endpoint: \"...\",\n\n // the working directory of the service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // root: \"...\",\n\n // Memcached username, optional.\n // username: \"...\",\n\n // Memcached password, optional.\n // password: \"...\",\n\n // The default ttl for put operations.\n // default_ttl: \"10s\",\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // connection_pool_max_size: \"1000\",\n});" + "full": "import { Operator } from \"opendal\";\n\nconst operator = new Operator(\"memcached\", {\n // network address of the memcached service.\n //\n // For example: \"tcp://localhost:11211\"\n // endpoint: \"...\",\n\n // the working directory of the service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // root: \"...\",\n\n // Memcached username, optional.\n // username: \"...\",\n\n // Memcached password, optional.\n // password: \"...\",\n\n // The default ttl for put operations.\n //\n // String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n // (for example, `5m`) duration formats.\n // default_ttl: \"10s\",\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // connection_pool_max_size: \"1000\",\n});" }, { "binding": "go", "language": "go", "minimal": "import (\n\t\"github.com/apache/opendal-go-services/memcached\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(memcached.Scheme, opendal.OperatorOptions{\n})", - "full": "import (\n\t\"github.com/apache/opendal-go-services/memcached\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(memcached.Scheme, opendal.OperatorOptions{\n\t// network address of the memcached service.\n\t//\n\t// For example: \"tcp://localhost:11211\"\n\t// \"endpoint\": \"...\",\n\n\t// the working directory of the service. Can be \"/path/to/dir\"\n\t//\n\t// default is \"/\"\n\t// \"root\": \"...\",\n\n\t// Memcached username, optional.\n\t// \"username\": \"...\",\n\n\t// Memcached password, optional.\n\t// \"password\": \"...\",\n\n\t// The default ttl for put operations.\n\t// \"default_ttl\": \"10s\",\n\n\t// The maximum number of connections allowed.\n\t//\n\t// default is 10\n\t// \"connection_pool_max_size\": \"1000\",\n})" + "full": "import (\n\t\"github.com/apache/opendal-go-services/memcached\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(memcached.Scheme, opendal.OperatorOptions{\n\t// network address of the memcached service.\n\t//\n\t// For example: \"tcp://localhost:11211\"\n\t// \"endpoint\": \"...\",\n\n\t// the working directory of the service. Can be \"/path/to/dir\"\n\t//\n\t// default is \"/\"\n\t// \"root\": \"...\",\n\n\t// Memcached username, optional.\n\t// \"username\": \"...\",\n\n\t// Memcached password, optional.\n\t// \"password\": \"...\",\n\n\t// The default ttl for put operations.\n\t//\n\t// String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n\t// (for example, `5m`) duration formats.\n\t// \"default_ttl\": \"10s\",\n\n\t// The maximum number of connections allowed.\n\t//\n\t// default is 10\n\t// \"connection_pool_max_size\": \"1000\",\n})" } ] }, @@ -3797,7 +3797,7 @@ "type": "duration", "required": false, "group": "General", - "comments": "The default ttl for put operations." + "comments": "The default ttl for put operations.\n\nString configurations accept ISO-8601 (for example, `PT5M`) and friendly\n(for example, `5m`) duration formats." } ], "examples": [ @@ -3805,31 +3805,31 @@ "binding": "rust", "language": "rust", "minimal": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"redis\", [\n (\"db\".to_string(), \"1000\".to_string()),\n])?;", - "full": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"redis\", [\n // network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n //\n // default is \"tcp://127.0.0.1:6379\"\n // (\"endpoint\".to_string(), \"...\".to_string()),\n\n // network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n //\n // default is None\n // (\"cluster_endpoints\".to_string(), \"...\".to_string()),\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // (\"connection_pool_max_size\".to_string(), \"1000\".to_string()),\n\n // the username to connect redis service.\n //\n // default is None\n // (\"username\".to_string(), \"...\".to_string()),\n\n // the password for authentication\n //\n // default is None\n // (\"password\".to_string(), \"...\".to_string()),\n\n // the working directory of the Redis service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // (\"root\".to_string(), \"...\".to_string()),\n\n // the number of DBs redis can take is unlimited\n //\n // default is db 0\n (\"db\".to_string(), \"1000\".to_string()),\n\n // The default ttl for put operations.\n // (\"default_ttl\".to_string(), \"10s\".to_string()),\n])?;" + "full": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"redis\", [\n // network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n //\n // default is \"tcp://127.0.0.1:6379\"\n // (\"endpoint\".to_string(), \"...\".to_string()),\n\n // network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n //\n // default is None\n // (\"cluster_endpoints\".to_string(), \"...\".to_string()),\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // (\"connection_pool_max_size\".to_string(), \"1000\".to_string()),\n\n // the username to connect redis service.\n //\n // default is None\n // (\"username\".to_string(), \"...\".to_string()),\n\n // the password for authentication\n //\n // default is None\n // (\"password\".to_string(), \"...\".to_string()),\n\n // the working directory of the Redis service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // (\"root\".to_string(), \"...\".to_string()),\n\n // the number of DBs redis can take is unlimited\n //\n // default is db 0\n (\"db\".to_string(), \"1000\".to_string()),\n\n // The default ttl for put operations.\n //\n // String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n // (for example, `5m`) duration formats.\n // (\"default_ttl\".to_string(), \"10s\".to_string()),\n])?;" }, { "binding": "java", "language": "java", "minimal": "import java.util.HashMap;\nimport java.util.Map;\nimport org.apache.opendal.Operator;\n\nMap config = new HashMap<>();\nconfig.put(\"db\", \"1000\");\nOperator operator = Operator.of(\"redis\", config);", - "full": "import java.util.HashMap;\nimport java.util.Map;\nimport org.apache.opendal.Operator;\n\nMap config = new HashMap<>();\n// network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n//\n// default is \"tcp://127.0.0.1:6379\"\n// config.put(\"endpoint\", \"...\");\n\n// network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n//\n// default is None\n// config.put(\"cluster_endpoints\", \"...\");\n\n// The maximum number of connections allowed.\n//\n// default is 10\n// config.put(\"connection_pool_max_size\", \"1000\");\n\n// the username to connect redis service.\n//\n// default is None\n// config.put(\"username\", \"...\");\n\n// the password for authentication\n//\n// default is None\n// config.put(\"password\", \"...\");\n\n// the working directory of the Redis service. Can be \"/path/to/dir\"\n//\n// default is \"/\"\n// config.put(\"root\", \"...\");\n\n// the number of DBs redis can take is unlimited\n//\n// default is db 0\nconfig.put(\"db\", \"1000\");\n\n// The default ttl for put operations.\n// config.put(\"default_ttl\", \"10s\");\nOperator operator = Operator.of(\"redis\", config);" + "full": "import java.util.HashMap;\nimport java.util.Map;\nimport org.apache.opendal.Operator;\n\nMap config = new HashMap<>();\n// network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n//\n// default is \"tcp://127.0.0.1:6379\"\n// config.put(\"endpoint\", \"...\");\n\n// network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n//\n// default is None\n// config.put(\"cluster_endpoints\", \"...\");\n\n// The maximum number of connections allowed.\n//\n// default is 10\n// config.put(\"connection_pool_max_size\", \"1000\");\n\n// the username to connect redis service.\n//\n// default is None\n// config.put(\"username\", \"...\");\n\n// the password for authentication\n//\n// default is None\n// config.put(\"password\", \"...\");\n\n// the working directory of the Redis service. Can be \"/path/to/dir\"\n//\n// default is \"/\"\n// config.put(\"root\", \"...\");\n\n// the number of DBs redis can take is unlimited\n//\n// default is db 0\nconfig.put(\"db\", \"1000\");\n\n// The default ttl for put operations.\n//\n// String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n// (for example, `5m`) duration formats.\n// config.put(\"default_ttl\", \"10s\");\nOperator operator = Operator.of(\"redis\", config);" }, { "binding": "python", "language": "python", "minimal": "import opendal\n\noperator = opendal.Operator(\n \"redis\",\n db=\"1000\",\n)", - "full": "import opendal\n\noperator = opendal.Operator(\n \"redis\",\n # network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n #\n # default is \"tcp://127.0.0.1:6379\"\n # endpoint=\"...\",\n\n # network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n #\n # default is None\n # cluster_endpoints=\"...\",\n\n # The maximum number of connections allowed.\n #\n # default is 10\n # connection_pool_max_size=\"1000\",\n\n # the username to connect redis service.\n #\n # default is None\n # username=\"...\",\n\n # the password for authentication\n #\n # default is None\n # password=\"...\",\n\n # the working directory of the Redis service. Can be \"/path/to/dir\"\n #\n # default is \"/\"\n # root=\"...\",\n\n # the number of DBs redis can take is unlimited\n #\n # default is db 0\n db=\"1000\",\n\n # The default ttl for put operations.\n # default_ttl=\"10s\",\n)" + "full": "import opendal\n\noperator = opendal.Operator(\n \"redis\",\n # network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n #\n # default is \"tcp://127.0.0.1:6379\"\n # endpoint=\"...\",\n\n # network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n #\n # default is None\n # cluster_endpoints=\"...\",\n\n # The maximum number of connections allowed.\n #\n # default is 10\n # connection_pool_max_size=\"1000\",\n\n # the username to connect redis service.\n #\n # default is None\n # username=\"...\",\n\n # the password for authentication\n #\n # default is None\n # password=\"...\",\n\n # the working directory of the Redis service. Can be \"/path/to/dir\"\n #\n # default is \"/\"\n # root=\"...\",\n\n # the number of DBs redis can take is unlimited\n #\n # default is db 0\n db=\"1000\",\n\n # The default ttl for put operations.\n #\n # String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n # (for example, `5m`) duration formats.\n # default_ttl=\"10s\",\n)" }, { "binding": "nodejs", "language": "javascript", "minimal": "import { Operator } from \"opendal\";\n\nconst operator = new Operator(\"redis\", {\n db: \"1000\",\n});", - "full": "import { Operator } from \"opendal\";\n\nconst operator = new Operator(\"redis\", {\n // network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n //\n // default is \"tcp://127.0.0.1:6379\"\n // endpoint: \"...\",\n\n // network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n //\n // default is None\n // cluster_endpoints: \"...\",\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // connection_pool_max_size: \"1000\",\n\n // the username to connect redis service.\n //\n // default is None\n // username: \"...\",\n\n // the password for authentication\n //\n // default is None\n // password: \"...\",\n\n // the working directory of the Redis service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // root: \"...\",\n\n // the number of DBs redis can take is unlimited\n //\n // default is db 0\n db: \"1000\",\n\n // The default ttl for put operations.\n // default_ttl: \"10s\",\n});" + "full": "import { Operator } from \"opendal\";\n\nconst operator = new Operator(\"redis\", {\n // network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n //\n // default is \"tcp://127.0.0.1:6379\"\n // endpoint: \"...\",\n\n // network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n //\n // default is None\n // cluster_endpoints: \"...\",\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // connection_pool_max_size: \"1000\",\n\n // the username to connect redis service.\n //\n // default is None\n // username: \"...\",\n\n // the password for authentication\n //\n // default is None\n // password: \"...\",\n\n // the working directory of the Redis service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // root: \"...\",\n\n // the number of DBs redis can take is unlimited\n //\n // default is db 0\n db: \"1000\",\n\n // The default ttl for put operations.\n //\n // String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n // (for example, `5m`) duration formats.\n // default_ttl: \"10s\",\n});" }, { "binding": "go", "language": "go", "minimal": "import (\n\t\"github.com/apache/opendal-go-services/redis\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(redis.Scheme, opendal.OperatorOptions{\n\t\"db\": \"1000\",\n})", - "full": "import (\n\t\"github.com/apache/opendal-go-services/redis\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(redis.Scheme, opendal.OperatorOptions{\n\t// network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n\t//\n\t// default is \"tcp://127.0.0.1:6379\"\n\t// \"endpoint\": \"...\",\n\n\t// network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n\t//\n\t// default is None\n\t// \"cluster_endpoints\": \"...\",\n\n\t// The maximum number of connections allowed.\n\t//\n\t// default is 10\n\t// \"connection_pool_max_size\": \"1000\",\n\n\t// the username to connect redis service.\n\t//\n\t// default is None\n\t// \"username\": \"...\",\n\n\t// the password for authentication\n\t//\n\t// default is None\n\t// \"password\": \"...\",\n\n\t// the working directory of the Redis service. Can be \"/path/to/dir\"\n\t//\n\t// default is \"/\"\n\t// \"root\": \"...\",\n\n\t// the number of DBs redis can take is unlimited\n\t//\n\t// default is db 0\n\t\"db\": \"1000\",\n\n\t// The default ttl for put operations.\n\t// \"default_ttl\": \"10s\",\n})" + "full": "import (\n\t\"github.com/apache/opendal-go-services/redis\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(redis.Scheme, opendal.OperatorOptions{\n\t// network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n\t//\n\t// default is \"tcp://127.0.0.1:6379\"\n\t// \"endpoint\": \"...\",\n\n\t// network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n\t//\n\t// default is None\n\t// \"cluster_endpoints\": \"...\",\n\n\t// The maximum number of connections allowed.\n\t//\n\t// default is 10\n\t// \"connection_pool_max_size\": \"1000\",\n\n\t// the username to connect redis service.\n\t//\n\t// default is None\n\t// \"username\": \"...\",\n\n\t// the password for authentication\n\t//\n\t// default is None\n\t// \"password\": \"...\",\n\n\t// the working directory of the Redis service. Can be \"/path/to/dir\"\n\t//\n\t// default is \"/\"\n\t// \"root\": \"...\",\n\n\t// the number of DBs redis can take is unlimited\n\t//\n\t// default is db 0\n\t\"db\": \"1000\",\n\n\t// The default ttl for put operations.\n\t//\n\t// String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n\t// (for example, `5m`) duration formats.\n\t// \"default_ttl\": \"10s\",\n})" } ] }, From 5e982beaa80527e64a9e217bc7a0872d0362bdbd Mon Sep 17 00:00:00 2001 From: subotac <73706465+subotac@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:28:13 +0300 Subject: [PATCH 3/5] docs: keep duration config docs binding-neutral --- .../org/apache/opendal/ServiceConfig.java | 6 ---- bindings/python/python/opendal/config.py | 6 ++-- core/services/cloudflare-kv/src/config.rs | 3 -- core/services/memcached/src/config.rs | 3 -- core/services/redis/src/config.rs | 3 -- dev/src/generate/python.rs | 3 +- website/data/services.json | 32 +++++++++---------- 7 files changed, 20 insertions(+), 36 deletions(-) diff --git a/bindings/java/src/main/java/org/apache/opendal/ServiceConfig.java b/bindings/java/src/main/java/org/apache/opendal/ServiceConfig.java index 0449c3daa5e1..d545a3ee5fcc 100644 --- a/bindings/java/src/main/java/org/apache/opendal/ServiceConfig.java +++ b/bindings/java/src/main/java/org/apache/opendal/ServiceConfig.java @@ -549,8 +549,6 @@ class CloudflareKv implements ServiceConfig { public final String apiToken; /** *

The default ttl for write operations.

- *

String configurations accept ISO-8601 (for example, PT5M) and friendly - * (for example, 5m) duration formats.

*/ public final Duration defaultTtl; /** @@ -1896,8 +1894,6 @@ class Memcached implements ServiceConfig { public final Long connectionPoolMaxSize; /** *

The default ttl for put operations.

- *

String configurations accept ISO-8601 (for example, PT5M) and friendly - * (for example, 5m) duration formats.

*/ public final Duration defaultTtl; /** @@ -2821,8 +2817,6 @@ class Redis implements ServiceConfig { public final long db; /** *

The default ttl for put operations.

- *

String configurations accept ISO-8601 (for example, PT5M) and friendly - * (for example, 5m) duration formats.

*/ public final Duration defaultTtl; /** diff --git a/bindings/python/python/opendal/config.py b/bindings/python/python/opendal/config.py index c772c002e321..54a4b7a6315a 100644 --- a/bindings/python/python/opendal/config.py +++ b/bindings/python/python/opendal/config.py @@ -193,7 +193,7 @@ class CloudflareKvConfig(TypedDict): api_token: NotRequired[str] """The token used to authenticate with CloudFlare.""" default_ttl: NotRequired[str] - """The default ttl for write operations. String configurations accept ISO-8601 (for example, `PT5M`) and friendly (for example, `5m`) duration formats. A human readable duration string, e.g. "5s" (see https://docs.rs/humantime/latest/humantime/fn.parse_duration.html).""" + """The default ttl for write operations. A human readable duration string, e.g. "5s" (see https://docs.rs/humantime/latest/humantime/fn.parse_duration.html).""" namespace_id: NotRequired[str] """The namespace ID. Used as URI path parameter.""" root: NotRequired[str | os.PathLike[str]] @@ -457,7 +457,7 @@ class MemcachedConfig(TypedDict): connection_pool_max_size: NotRequired[int] """The maximum number of connections allowed. default is 10""" default_ttl: NotRequired[str] - """The default ttl for put operations. String configurations accept ISO-8601 (for example, `PT5M`) and friendly (for example, `5m`) duration formats. A human readable duration string, e.g. "5s" (see https://docs.rs/humantime/latest/humantime/fn.parse_duration.html).""" + """The default ttl for put operations. A human readable duration string, e.g. "5s" (see https://docs.rs/humantime/latest/humantime/fn.parse_duration.html).""" endpoint: NotRequired[str] """network address of the memcached service. For example: "tcp://localhost:11211\"""" password: NotRequired[str] @@ -689,7 +689,7 @@ class RedisConfig(TypedDict): db: Required[int] """the number of DBs redis can take is unlimited default is db 0""" default_ttl: NotRequired[str] - """The default ttl for put operations. String configurations accept ISO-8601 (for example, `PT5M`) and friendly (for example, `5m`) duration formats. A human readable duration string, e.g. "5s" (see https://docs.rs/humantime/latest/humantime/fn.parse_duration.html).""" + """The default ttl for put operations. A human readable duration string, e.g. "5s" (see https://docs.rs/humantime/latest/humantime/fn.parse_duration.html).""" endpoint: NotRequired[str] """network address of the Redis service. Can be "tcp://127.0.0.1:6379", e.g. default is "tcp://127.0.0.1:6379\"""" password: NotRequired[str] diff --git a/core/services/cloudflare-kv/src/config.rs b/core/services/cloudflare-kv/src/config.rs index ceaad61708b4..1668816a79a1 100644 --- a/core/services/cloudflare-kv/src/config.rs +++ b/core/services/cloudflare-kv/src/config.rs @@ -34,9 +34,6 @@ pub struct CloudflareKvConfig { /// The namespace ID. Used as URI path parameter. pub namespace_id: Option, /// The default ttl for write operations. - /// - /// String configurations accept ISO-8601 (for example, `PT5M`) and friendly - /// (for example, `5m`) duration formats. #[serde(default, deserialize_with = "deserialize_option_duration")] pub default_ttl: Option, diff --git a/core/services/memcached/src/config.rs b/core/services/memcached/src/config.rs index b14dc7fcc5d8..cf2e949e68e2 100644 --- a/core/services/memcached/src/config.rs +++ b/core/services/memcached/src/config.rs @@ -44,9 +44,6 @@ pub struct MemcachedConfig { /// Memcached password, optional. pub password: Option, /// The default ttl for put operations. - /// - /// String configurations accept ISO-8601 (for example, `PT5M`) and friendly - /// (for example, `5m`) duration formats. #[serde(default, deserialize_with = "deserialize_option_duration")] pub default_ttl: Option, /// The maximum number of connections allowed. diff --git a/core/services/redis/src/config.rs b/core/services/redis/src/config.rs index 8ead8c459ca6..03cbfacd729f 100644 --- a/core/services/redis/src/config.rs +++ b/core/services/redis/src/config.rs @@ -59,9 +59,6 @@ pub struct RedisConfig { /// default is db 0 pub db: i64, /// The default ttl for put operations. - /// - /// String configurations accept ISO-8601 (for example, `PT5M`) and friendly - /// (for example, `5m`) duration formats. #[serde(default, deserialize_with = "deserialize_option_duration")] pub default_ttl: Option, } diff --git a/dev/src/generate/python.rs b/dev/src/generate/python.rs index 0763c066a8bd..706c0735348a 100644 --- a/dev/src/generate/python.rs +++ b/dev/src/generate/python.rs @@ -259,8 +259,7 @@ fn is_path_like_field(name: &str) -> bool { fn make_config_field_type(field: ViaDeserialize) -> Result { Ok(match field.value { ConfigType::Bool => "bool".to_string(), - // Duration is typed `str` (a humantime string, e.g. "5s"). See #7887: - // core cannot yet deserialize Duration config fields from strings. + // Duration values cross the Python binding as strings, e.g. "5s". ConfigType::Duration => "str".to_string(), ConfigType::Usize | ConfigType::U64 diff --git a/website/data/services.json b/website/data/services.json index 6fb277d43401..f9600f47118f 100644 --- a/website/data/services.json +++ b/website/data/services.json @@ -678,7 +678,7 @@ "type": "duration", "required": false, "group": "General", - "comments": "The default ttl for write operations.\n\nString configurations accept ISO-8601 (for example, `PT5M`) and friendly\n(for example, `5m`) duration formats." + "comments": "The default ttl for write operations." }, { "name": "root", @@ -693,19 +693,19 @@ "binding": "rust", "language": "rust", "minimal": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"cloudflare-kv\", [\n])?;", - "full": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"cloudflare-kv\", [\n // The token used to authenticate with CloudFlare.\n // (\"api_token\".to_string(), \"...\".to_string()),\n\n // The account ID used to authenticate with CloudFlare. Used as URI path parameter.\n // (\"account_id\".to_string(), \"...\".to_string()),\n\n // The namespace ID. Used as URI path parameter.\n // (\"namespace_id\".to_string(), \"...\".to_string()),\n\n // The default ttl for write operations.\n //\n // String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n // (for example, `5m`) duration formats.\n // (\"default_ttl\".to_string(), \"10s\".to_string()),\n\n // Root within this backend.\n // (\"root\".to_string(), \"...\".to_string()),\n])?;" + "full": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"cloudflare-kv\", [\n // The token used to authenticate with CloudFlare.\n // (\"api_token\".to_string(), \"...\".to_string()),\n\n // The account ID used to authenticate with CloudFlare. Used as URI path parameter.\n // (\"account_id\".to_string(), \"...\".to_string()),\n\n // The namespace ID. Used as URI path parameter.\n // (\"namespace_id\".to_string(), \"...\".to_string()),\n\n // The default ttl for write operations.\n // (\"default_ttl\".to_string(), \"10s\".to_string()),\n\n // Root within this backend.\n // (\"root\".to_string(), \"...\".to_string()),\n])?;" }, { "binding": "python", "language": "python", "minimal": "import opendal\n\noperator = opendal.Operator(\n \"cloudflare-kv\",\n)", - "full": "import opendal\n\noperator = opendal.Operator(\n \"cloudflare-kv\",\n # The token used to authenticate with CloudFlare.\n # api_token=\"...\",\n\n # The account ID used to authenticate with CloudFlare. Used as URI path parameter.\n # account_id=\"...\",\n\n # The namespace ID. Used as URI path parameter.\n # namespace_id=\"...\",\n\n # The default ttl for write operations.\n #\n # String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n # (for example, `5m`) duration formats.\n # default_ttl=\"10s\",\n\n # Root within this backend.\n # root=\"...\",\n)" + "full": "import opendal\n\noperator = opendal.Operator(\n \"cloudflare-kv\",\n # The token used to authenticate with CloudFlare.\n # api_token=\"...\",\n\n # The account ID used to authenticate with CloudFlare. Used as URI path parameter.\n # account_id=\"...\",\n\n # The namespace ID. Used as URI path parameter.\n # namespace_id=\"...\",\n\n # The default ttl for write operations.\n # default_ttl=\"10s\",\n\n # Root within this backend.\n # root=\"...\",\n)" }, { "binding": "go", "language": "go", "minimal": "import (\n\t\"github.com/apache/opendal-go-services/cloudflare-kv\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(cloudflare-kv.Scheme, opendal.OperatorOptions{\n})", - "full": "import (\n\t\"github.com/apache/opendal-go-services/cloudflare-kv\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(cloudflare-kv.Scheme, opendal.OperatorOptions{\n\t// The token used to authenticate with CloudFlare.\n\t// \"api_token\": \"...\",\n\n\t// The account ID used to authenticate with CloudFlare. Used as URI path parameter.\n\t// \"account_id\": \"...\",\n\n\t// The namespace ID. Used as URI path parameter.\n\t// \"namespace_id\": \"...\",\n\n\t// The default ttl for write operations.\n\t//\n\t// String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n\t// (for example, `5m`) duration formats.\n\t// \"default_ttl\": \"10s\",\n\n\t// Root within this backend.\n\t// \"root\": \"...\",\n})" + "full": "import (\n\t\"github.com/apache/opendal-go-services/cloudflare-kv\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(cloudflare-kv.Scheme, opendal.OperatorOptions{\n\t// The token used to authenticate with CloudFlare.\n\t// \"api_token\": \"...\",\n\n\t// The account ID used to authenticate with CloudFlare. Used as URI path parameter.\n\t// \"account_id\": \"...\",\n\n\t// The namespace ID. Used as URI path parameter.\n\t// \"namespace_id\": \"...\",\n\n\t// The default ttl for write operations.\n\t// \"default_ttl\": \"10s\",\n\n\t// Root within this backend.\n\t// \"root\": \"...\",\n})" } ] }, @@ -2603,7 +2603,7 @@ "type": "duration", "required": false, "group": "General", - "comments": "The default ttl for put operations.\n\nString configurations accept ISO-8601 (for example, `PT5M`) and friendly\n(for example, `5m`) duration formats." + "comments": "The default ttl for put operations." }, { "name": "connection_pool_max_size", @@ -2618,31 +2618,31 @@ "binding": "rust", "language": "rust", "minimal": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"memcached\", [\n])?;", - "full": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"memcached\", [\n // network address of the memcached service.\n //\n // For example: \"tcp://localhost:11211\"\n // (\"endpoint\".to_string(), \"...\".to_string()),\n\n // the working directory of the service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // (\"root\".to_string(), \"...\".to_string()),\n\n // Memcached username, optional.\n // (\"username\".to_string(), \"...\".to_string()),\n\n // Memcached password, optional.\n // (\"password\".to_string(), \"...\".to_string()),\n\n // The default ttl for put operations.\n //\n // String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n // (for example, `5m`) duration formats.\n // (\"default_ttl\".to_string(), \"10s\".to_string()),\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // (\"connection_pool_max_size\".to_string(), \"1000\".to_string()),\n])?;" + "full": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"memcached\", [\n // network address of the memcached service.\n //\n // For example: \"tcp://localhost:11211\"\n // (\"endpoint\".to_string(), \"...\".to_string()),\n\n // the working directory of the service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // (\"root\".to_string(), \"...\".to_string()),\n\n // Memcached username, optional.\n // (\"username\".to_string(), \"...\".to_string()),\n\n // Memcached password, optional.\n // (\"password\".to_string(), \"...\".to_string()),\n\n // The default ttl for put operations.\n // (\"default_ttl\".to_string(), \"10s\".to_string()),\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // (\"connection_pool_max_size\".to_string(), \"1000\".to_string()),\n])?;" }, { "binding": "java", "language": "java", "minimal": "import java.util.HashMap;\nimport java.util.Map;\nimport org.apache.opendal.Operator;\n\nMap config = new HashMap<>();\nOperator operator = Operator.of(\"memcached\", config);", - "full": "import java.util.HashMap;\nimport java.util.Map;\nimport org.apache.opendal.Operator;\n\nMap config = new HashMap<>();\n// network address of the memcached service.\n//\n// For example: \"tcp://localhost:11211\"\n// config.put(\"endpoint\", \"...\");\n\n// the working directory of the service. Can be \"/path/to/dir\"\n//\n// default is \"/\"\n// config.put(\"root\", \"...\");\n\n// Memcached username, optional.\n// config.put(\"username\", \"...\");\n\n// Memcached password, optional.\n// config.put(\"password\", \"...\");\n\n// The default ttl for put operations.\n//\n// String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n// (for example, `5m`) duration formats.\n// config.put(\"default_ttl\", \"10s\");\n\n// The maximum number of connections allowed.\n//\n// default is 10\n// config.put(\"connection_pool_max_size\", \"1000\");\nOperator operator = Operator.of(\"memcached\", config);" + "full": "import java.util.HashMap;\nimport java.util.Map;\nimport org.apache.opendal.Operator;\n\nMap config = new HashMap<>();\n// network address of the memcached service.\n//\n// For example: \"tcp://localhost:11211\"\n// config.put(\"endpoint\", \"...\");\n\n// the working directory of the service. Can be \"/path/to/dir\"\n//\n// default is \"/\"\n// config.put(\"root\", \"...\");\n\n// Memcached username, optional.\n// config.put(\"username\", \"...\");\n\n// Memcached password, optional.\n// config.put(\"password\", \"...\");\n\n// The default ttl for put operations.\n// config.put(\"default_ttl\", \"10s\");\n\n// The maximum number of connections allowed.\n//\n// default is 10\n// config.put(\"connection_pool_max_size\", \"1000\");\nOperator operator = Operator.of(\"memcached\", config);" }, { "binding": "python", "language": "python", "minimal": "import opendal\n\noperator = opendal.Operator(\n \"memcached\",\n)", - "full": "import opendal\n\noperator = opendal.Operator(\n \"memcached\",\n # network address of the memcached service.\n #\n # For example: \"tcp://localhost:11211\"\n # endpoint=\"...\",\n\n # the working directory of the service. Can be \"/path/to/dir\"\n #\n # default is \"/\"\n # root=\"...\",\n\n # Memcached username, optional.\n # username=\"...\",\n\n # Memcached password, optional.\n # password=\"...\",\n\n # The default ttl for put operations.\n #\n # String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n # (for example, `5m`) duration formats.\n # default_ttl=\"10s\",\n\n # The maximum number of connections allowed.\n #\n # default is 10\n # connection_pool_max_size=\"1000\",\n)" + "full": "import opendal\n\noperator = opendal.Operator(\n \"memcached\",\n # network address of the memcached service.\n #\n # For example: \"tcp://localhost:11211\"\n # endpoint=\"...\",\n\n # the working directory of the service. Can be \"/path/to/dir\"\n #\n # default is \"/\"\n # root=\"...\",\n\n # Memcached username, optional.\n # username=\"...\",\n\n # Memcached password, optional.\n # password=\"...\",\n\n # The default ttl for put operations.\n # default_ttl=\"10s\",\n\n # The maximum number of connections allowed.\n #\n # default is 10\n # connection_pool_max_size=\"1000\",\n)" }, { "binding": "nodejs", "language": "javascript", "minimal": "import { Operator } from \"opendal\";\n\nconst operator = new Operator(\"memcached\", {\n});", - "full": "import { Operator } from \"opendal\";\n\nconst operator = new Operator(\"memcached\", {\n // network address of the memcached service.\n //\n // For example: \"tcp://localhost:11211\"\n // endpoint: \"...\",\n\n // the working directory of the service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // root: \"...\",\n\n // Memcached username, optional.\n // username: \"...\",\n\n // Memcached password, optional.\n // password: \"...\",\n\n // The default ttl for put operations.\n //\n // String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n // (for example, `5m`) duration formats.\n // default_ttl: \"10s\",\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // connection_pool_max_size: \"1000\",\n});" + "full": "import { Operator } from \"opendal\";\n\nconst operator = new Operator(\"memcached\", {\n // network address of the memcached service.\n //\n // For example: \"tcp://localhost:11211\"\n // endpoint: \"...\",\n\n // the working directory of the service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // root: \"...\",\n\n // Memcached username, optional.\n // username: \"...\",\n\n // Memcached password, optional.\n // password: \"...\",\n\n // The default ttl for put operations.\n // default_ttl: \"10s\",\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // connection_pool_max_size: \"1000\",\n});" }, { "binding": "go", "language": "go", "minimal": "import (\n\t\"github.com/apache/opendal-go-services/memcached\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(memcached.Scheme, opendal.OperatorOptions{\n})", - "full": "import (\n\t\"github.com/apache/opendal-go-services/memcached\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(memcached.Scheme, opendal.OperatorOptions{\n\t// network address of the memcached service.\n\t//\n\t// For example: \"tcp://localhost:11211\"\n\t// \"endpoint\": \"...\",\n\n\t// the working directory of the service. Can be \"/path/to/dir\"\n\t//\n\t// default is \"/\"\n\t// \"root\": \"...\",\n\n\t// Memcached username, optional.\n\t// \"username\": \"...\",\n\n\t// Memcached password, optional.\n\t// \"password\": \"...\",\n\n\t// The default ttl for put operations.\n\t//\n\t// String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n\t// (for example, `5m`) duration formats.\n\t// \"default_ttl\": \"10s\",\n\n\t// The maximum number of connections allowed.\n\t//\n\t// default is 10\n\t// \"connection_pool_max_size\": \"1000\",\n})" + "full": "import (\n\t\"github.com/apache/opendal-go-services/memcached\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(memcached.Scheme, opendal.OperatorOptions{\n\t// network address of the memcached service.\n\t//\n\t// For example: \"tcp://localhost:11211\"\n\t// \"endpoint\": \"...\",\n\n\t// the working directory of the service. Can be \"/path/to/dir\"\n\t//\n\t// default is \"/\"\n\t// \"root\": \"...\",\n\n\t// Memcached username, optional.\n\t// \"username\": \"...\",\n\n\t// Memcached password, optional.\n\t// \"password\": \"...\",\n\n\t// The default ttl for put operations.\n\t// \"default_ttl\": \"10s\",\n\n\t// The maximum number of connections allowed.\n\t//\n\t// default is 10\n\t// \"connection_pool_max_size\": \"1000\",\n})" } ] }, @@ -3797,7 +3797,7 @@ "type": "duration", "required": false, "group": "General", - "comments": "The default ttl for put operations.\n\nString configurations accept ISO-8601 (for example, `PT5M`) and friendly\n(for example, `5m`) duration formats." + "comments": "The default ttl for put operations." } ], "examples": [ @@ -3805,31 +3805,31 @@ "binding": "rust", "language": "rust", "minimal": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"redis\", [\n (\"db\".to_string(), \"1000\".to_string()),\n])?;", - "full": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"redis\", [\n // network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n //\n // default is \"tcp://127.0.0.1:6379\"\n // (\"endpoint\".to_string(), \"...\".to_string()),\n\n // network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n //\n // default is None\n // (\"cluster_endpoints\".to_string(), \"...\".to_string()),\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // (\"connection_pool_max_size\".to_string(), \"1000\".to_string()),\n\n // the username to connect redis service.\n //\n // default is None\n // (\"username\".to_string(), \"...\".to_string()),\n\n // the password for authentication\n //\n // default is None\n // (\"password\".to_string(), \"...\".to_string()),\n\n // the working directory of the Redis service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // (\"root\".to_string(), \"...\".to_string()),\n\n // the number of DBs redis can take is unlimited\n //\n // default is db 0\n (\"db\".to_string(), \"1000\".to_string()),\n\n // The default ttl for put operations.\n //\n // String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n // (for example, `5m`) duration formats.\n // (\"default_ttl\".to_string(), \"10s\".to_string()),\n])?;" + "full": "use opendal::Operator;\n\nlet operator = Operator::via_iter(\"redis\", [\n // network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n //\n // default is \"tcp://127.0.0.1:6379\"\n // (\"endpoint\".to_string(), \"...\".to_string()),\n\n // network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n //\n // default is None\n // (\"cluster_endpoints\".to_string(), \"...\".to_string()),\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // (\"connection_pool_max_size\".to_string(), \"1000\".to_string()),\n\n // the username to connect redis service.\n //\n // default is None\n // (\"username\".to_string(), \"...\".to_string()),\n\n // the password for authentication\n //\n // default is None\n // (\"password\".to_string(), \"...\".to_string()),\n\n // the working directory of the Redis service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // (\"root\".to_string(), \"...\".to_string()),\n\n // the number of DBs redis can take is unlimited\n //\n // default is db 0\n (\"db\".to_string(), \"1000\".to_string()),\n\n // The default ttl for put operations.\n // (\"default_ttl\".to_string(), \"10s\".to_string()),\n])?;" }, { "binding": "java", "language": "java", "minimal": "import java.util.HashMap;\nimport java.util.Map;\nimport org.apache.opendal.Operator;\n\nMap config = new HashMap<>();\nconfig.put(\"db\", \"1000\");\nOperator operator = Operator.of(\"redis\", config);", - "full": "import java.util.HashMap;\nimport java.util.Map;\nimport org.apache.opendal.Operator;\n\nMap config = new HashMap<>();\n// network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n//\n// default is \"tcp://127.0.0.1:6379\"\n// config.put(\"endpoint\", \"...\");\n\n// network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n//\n// default is None\n// config.put(\"cluster_endpoints\", \"...\");\n\n// The maximum number of connections allowed.\n//\n// default is 10\n// config.put(\"connection_pool_max_size\", \"1000\");\n\n// the username to connect redis service.\n//\n// default is None\n// config.put(\"username\", \"...\");\n\n// the password for authentication\n//\n// default is None\n// config.put(\"password\", \"...\");\n\n// the working directory of the Redis service. Can be \"/path/to/dir\"\n//\n// default is \"/\"\n// config.put(\"root\", \"...\");\n\n// the number of DBs redis can take is unlimited\n//\n// default is db 0\nconfig.put(\"db\", \"1000\");\n\n// The default ttl for put operations.\n//\n// String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n// (for example, `5m`) duration formats.\n// config.put(\"default_ttl\", \"10s\");\nOperator operator = Operator.of(\"redis\", config);" + "full": "import java.util.HashMap;\nimport java.util.Map;\nimport org.apache.opendal.Operator;\n\nMap config = new HashMap<>();\n// network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n//\n// default is \"tcp://127.0.0.1:6379\"\n// config.put(\"endpoint\", \"...\");\n\n// network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n//\n// default is None\n// config.put(\"cluster_endpoints\", \"...\");\n\n// The maximum number of connections allowed.\n//\n// default is 10\n// config.put(\"connection_pool_max_size\", \"1000\");\n\n// the username to connect redis service.\n//\n// default is None\n// config.put(\"username\", \"...\");\n\n// the password for authentication\n//\n// default is None\n// config.put(\"password\", \"...\");\n\n// the working directory of the Redis service. Can be \"/path/to/dir\"\n//\n// default is \"/\"\n// config.put(\"root\", \"...\");\n\n// the number of DBs redis can take is unlimited\n//\n// default is db 0\nconfig.put(\"db\", \"1000\");\n\n// The default ttl for put operations.\n// config.put(\"default_ttl\", \"10s\");\nOperator operator = Operator.of(\"redis\", config);" }, { "binding": "python", "language": "python", "minimal": "import opendal\n\noperator = opendal.Operator(\n \"redis\",\n db=\"1000\",\n)", - "full": "import opendal\n\noperator = opendal.Operator(\n \"redis\",\n # network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n #\n # default is \"tcp://127.0.0.1:6379\"\n # endpoint=\"...\",\n\n # network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n #\n # default is None\n # cluster_endpoints=\"...\",\n\n # The maximum number of connections allowed.\n #\n # default is 10\n # connection_pool_max_size=\"1000\",\n\n # the username to connect redis service.\n #\n # default is None\n # username=\"...\",\n\n # the password for authentication\n #\n # default is None\n # password=\"...\",\n\n # the working directory of the Redis service. Can be \"/path/to/dir\"\n #\n # default is \"/\"\n # root=\"...\",\n\n # the number of DBs redis can take is unlimited\n #\n # default is db 0\n db=\"1000\",\n\n # The default ttl for put operations.\n #\n # String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n # (for example, `5m`) duration formats.\n # default_ttl=\"10s\",\n)" + "full": "import opendal\n\noperator = opendal.Operator(\n \"redis\",\n # network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n #\n # default is \"tcp://127.0.0.1:6379\"\n # endpoint=\"...\",\n\n # network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n #\n # default is None\n # cluster_endpoints=\"...\",\n\n # The maximum number of connections allowed.\n #\n # default is 10\n # connection_pool_max_size=\"1000\",\n\n # the username to connect redis service.\n #\n # default is None\n # username=\"...\",\n\n # the password for authentication\n #\n # default is None\n # password=\"...\",\n\n # the working directory of the Redis service. Can be \"/path/to/dir\"\n #\n # default is \"/\"\n # root=\"...\",\n\n # the number of DBs redis can take is unlimited\n #\n # default is db 0\n db=\"1000\",\n\n # The default ttl for put operations.\n # default_ttl=\"10s\",\n)" }, { "binding": "nodejs", "language": "javascript", "minimal": "import { Operator } from \"opendal\";\n\nconst operator = new Operator(\"redis\", {\n db: \"1000\",\n});", - "full": "import { Operator } from \"opendal\";\n\nconst operator = new Operator(\"redis\", {\n // network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n //\n // default is \"tcp://127.0.0.1:6379\"\n // endpoint: \"...\",\n\n // network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n //\n // default is None\n // cluster_endpoints: \"...\",\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // connection_pool_max_size: \"1000\",\n\n // the username to connect redis service.\n //\n // default is None\n // username: \"...\",\n\n // the password for authentication\n //\n // default is None\n // password: \"...\",\n\n // the working directory of the Redis service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // root: \"...\",\n\n // the number of DBs redis can take is unlimited\n //\n // default is db 0\n db: \"1000\",\n\n // The default ttl for put operations.\n //\n // String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n // (for example, `5m`) duration formats.\n // default_ttl: \"10s\",\n});" + "full": "import { Operator } from \"opendal\";\n\nconst operator = new Operator(\"redis\", {\n // network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n //\n // default is \"tcp://127.0.0.1:6379\"\n // endpoint: \"...\",\n\n // network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n //\n // default is None\n // cluster_endpoints: \"...\",\n\n // The maximum number of connections allowed.\n //\n // default is 10\n // connection_pool_max_size: \"1000\",\n\n // the username to connect redis service.\n //\n // default is None\n // username: \"...\",\n\n // the password for authentication\n //\n // default is None\n // password: \"...\",\n\n // the working directory of the Redis service. Can be \"/path/to/dir\"\n //\n // default is \"/\"\n // root: \"...\",\n\n // the number of DBs redis can take is unlimited\n //\n // default is db 0\n db: \"1000\",\n\n // The default ttl for put operations.\n // default_ttl: \"10s\",\n});" }, { "binding": "go", "language": "go", "minimal": "import (\n\t\"github.com/apache/opendal-go-services/redis\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(redis.Scheme, opendal.OperatorOptions{\n\t\"db\": \"1000\",\n})", - "full": "import (\n\t\"github.com/apache/opendal-go-services/redis\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(redis.Scheme, opendal.OperatorOptions{\n\t// network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n\t//\n\t// default is \"tcp://127.0.0.1:6379\"\n\t// \"endpoint\": \"...\",\n\n\t// network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n\t//\n\t// default is None\n\t// \"cluster_endpoints\": \"...\",\n\n\t// The maximum number of connections allowed.\n\t//\n\t// default is 10\n\t// \"connection_pool_max_size\": \"1000\",\n\n\t// the username to connect redis service.\n\t//\n\t// default is None\n\t// \"username\": \"...\",\n\n\t// the password for authentication\n\t//\n\t// default is None\n\t// \"password\": \"...\",\n\n\t// the working directory of the Redis service. Can be \"/path/to/dir\"\n\t//\n\t// default is \"/\"\n\t// \"root\": \"...\",\n\n\t// the number of DBs redis can take is unlimited\n\t//\n\t// default is db 0\n\t\"db\": \"1000\",\n\n\t// The default ttl for put operations.\n\t//\n\t// String configurations accept ISO-8601 (for example, `PT5M`) and friendly\n\t// (for example, `5m`) duration formats.\n\t// \"default_ttl\": \"10s\",\n})" + "full": "import (\n\t\"github.com/apache/opendal-go-services/redis\"\n\topendal \"github.com/apache/opendal/bindings/go\"\n)\n\noperator, err := opendal.NewOperator(redis.Scheme, opendal.OperatorOptions{\n\t// network address of the Redis service. Can be \"tcp://127.0.0.1:6379\", e.g.\n\t//\n\t// default is \"tcp://127.0.0.1:6379\"\n\t// \"endpoint\": \"...\",\n\n\t// network address of the Redis cluster service. Can be \"tcp://127.0.0.1:6379,tcp://127.0.0.1:6380,tcp://127.0.0.1:6381\", e.g.\n\t//\n\t// default is None\n\t// \"cluster_endpoints\": \"...\",\n\n\t// The maximum number of connections allowed.\n\t//\n\t// default is 10\n\t// \"connection_pool_max_size\": \"1000\",\n\n\t// the username to connect redis service.\n\t//\n\t// default is None\n\t// \"username\": \"...\",\n\n\t// the password for authentication\n\t//\n\t// default is None\n\t// \"password\": \"...\",\n\n\t// the working directory of the Redis service. Can be \"/path/to/dir\"\n\t//\n\t// default is \"/\"\n\t// \"root\": \"...\",\n\n\t// the number of DBs redis can take is unlimited\n\t//\n\t// default is db 0\n\t\"db\": \"1000\",\n\n\t// The default ttl for put operations.\n\t// \"default_ttl\": \"10s\",\n})" } ] }, From fed4f0e5728750f85c478532ac79eadcd613220a Mon Sep 17 00:00:00 2001 From: subotac <73706465+subotac@users.noreply.github.com> Date: Fri, 31 Jul 2026 20:26:23 +0300 Subject: [PATCH 4/5] fix(services): use signed durations for default TTLs --- core/core/src/raw/time.rs | 57 ++++------------------ core/services/cloudflare-kv/src/backend.rs | 15 ++++-- core/services/cloudflare-kv/src/config.rs | 10 ++-- core/services/memcached/src/backend.rs | 13 ++++- core/services/memcached/src/config.rs | 10 ++-- core/services/redis/src/backend.rs | 15 ++++-- core/services/redis/src/config.rs | 10 ++-- dev/src/generate/parser.rs | 17 ++++++- 8 files changed, 78 insertions(+), 69 deletions(-) diff --git a/core/core/src/raw/time.rs b/core/core/src/raw/time.rs index ed09a1d4d862..7cc1c957ab82 100644 --- a/core/core/src/raw/time.rs +++ b/core/core/src/raw/time.rs @@ -18,9 +18,7 @@ //! Time related utils. use crate::*; -use jiff::SignedDuration; -use serde::Deserialize; -use serde::Deserializer; +pub use jiff::SignedDuration; use std::fmt; use std::ops::{Add, AddAssign, Sub, SubAssign}; use std::str::FromStr; @@ -31,14 +29,6 @@ pub use std::time::{Instant, SystemTime}; #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] pub use web_time::{Instant, SystemTime}; -#[derive(Deserialize)] -#[serde(untagged)] -enum ConfigDuration { - Duration(Duration), - // String-backed config paths cannot provide Duration's structured serde representation. - String(String), -} - /// An instant in time represented as the number of nanoseconds since the Unix epoch. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Timestamp(jiff::Timestamp); @@ -264,7 +254,6 @@ impl SubAssign for Timestamp { } } -/// Convert an unsigned [`Duration`] into a jiff [`SignedDuration`]. /// Parse a duration encoded either as ISO-8601 (e.g. `PT5M`) or friendly (e.g. `5m`). #[inline] pub fn signed_to_duration(value: &str) -> Result { @@ -272,7 +261,13 @@ pub fn signed_to_duration(value: &str) -> Result { Error::new(ErrorKind::ConfigInvalid, "failed to parse duration").set_source(err) })?; - Duration::try_from(signed).map_err(|err| { + signed_duration_to_duration(signed) +} + +/// Convert a jiff [`SignedDuration`] into an unsigned [`Duration`]. +#[inline] +pub fn signed_duration_to_duration(value: SignedDuration) -> Result { + Duration::try_from(value).map_err(|err| { Error::new( ErrorKind::ConfigInvalid, "duration must not be negative or overflow", @@ -281,34 +276,10 @@ pub fn signed_to_duration(value: &str) -> Result { }) } -/// Deserialize an optional [`Duration`] from either its serde representation or -/// a string accepted by [`signed_to_duration`]. -pub fn deserialize_option_duration<'de, D>( - deserializer: D, -) -> std::result::Result, D::Error> -where - D: Deserializer<'de>, -{ - Option::::deserialize(deserializer)? - .map(|value| match value { - ConfigDuration::Duration(value) => Ok(value), - ConfigDuration::String(value) => { - signed_to_duration(&value).map_err(serde::de::Error::custom) - } - }) - .transpose() -} - #[cfg(test)] mod tests { use super::*; - #[derive(Deserialize)] - struct TestDurationConfig { - #[serde(default, deserialize_with = "deserialize_option_duration")] - duration: Option, - } - fn test_time() -> Timestamp { Timestamp("2022-03-01T08:12:34Z".parse().unwrap()) } @@ -340,15 +311,7 @@ mod tests { } #[test] - fn test_deserialize_option_duration() { - let friendly: TestDurationConfig = - serde_json::from_str(r#"{"duration":"1500ms"}"#).unwrap(); - assert_eq!(friendly.duration, Some(Duration::from_millis(1500))); - - let structured: TestDurationConfig = - serde_json::from_str(r#"{"duration":{"secs":5,"nanos":7}}"#).unwrap(); - assert_eq!(structured.duration, Some(Duration::new(5, 7))); - - assert!(serde_json::from_str::(r#"{"duration":"5"}"#).is_err()); + fn test_signed_duration_to_duration_rejects_negative_values() { + assert!(signed_duration_to_duration(SignedDuration::from_secs(-1)).is_err()); } } diff --git a/core/services/cloudflare-kv/src/backend.rs b/core/services/cloudflare-kv/src/backend.rs index a900fb6036fc..bd536df6ab39 100644 --- a/core/services/cloudflare-kv/src/backend.rs +++ b/core/services/cloudflare-kv/src/backend.rs @@ -37,6 +37,7 @@ use super::writer::CloudflareWriter; #[derive(Default)] pub struct CloudflareKvBuilder { pub(super) config: CloudflareKvConfig, + pub(super) default_ttl: Option, } impl Debug for CloudflareKvBuilder { @@ -76,7 +77,7 @@ impl CloudflareKvBuilder { /// /// If set, we will specify `EX` for write operations. pub fn default_ttl(mut self, ttl: Duration) -> Self { - self.config.default_ttl = Some(ttl); + self.default_ttl = Some(ttl); self } @@ -96,6 +97,14 @@ impl Builder for CloudflareKvBuilder { type Config = CloudflareKvConfig; fn build(self) -> Result { + let default_ttl = match self.default_ttl { + Some(ttl) => Some(ttl), + None => self + .config + .default_ttl + .map(signed_duration_to_duration) + .transpose()?, + }; let api_token = match &self.config.api_token { Some(api_token) => format_authorization_by_bearer(api_token)?, None => { @@ -121,7 +130,7 @@ impl Builder for CloudflareKvBuilder { }; // Validate default TTL is at least 60 seconds if specified - if let Some(ttl) = self.config.default_ttl + if let Some(ttl) = default_ttl && ttl < Duration::from_secs(60) { return Err(Error::new( @@ -143,7 +152,7 @@ impl Builder for CloudflareKvBuilder { api_token, account_id, namespace_id, - expiration_ttl: self.config.default_ttl, + expiration_ttl: default_ttl, info: ServiceInfo::new(CLOUDFLARE_KV_SCHEME, &root, ""), capability: Capability { create_dir: true, diff --git a/core/services/cloudflare-kv/src/config.rs b/core/services/cloudflare-kv/src/config.rs index 1668816a79a1..7f5f2bae8f3d 100644 --- a/core/services/cloudflare-kv/src/config.rs +++ b/core/services/cloudflare-kv/src/config.rs @@ -34,8 +34,7 @@ pub struct CloudflareKvConfig { /// The namespace ID. Used as URI path parameter. pub namespace_id: Option, /// The default ttl for write operations. - #[serde(default, deserialize_with = "deserialize_option_duration")] - pub default_ttl: Option, + pub default_ttl: Option, /// Root within this backend. pub root: Option, @@ -95,7 +94,10 @@ impl opendal_core::Configurator for CloudflareKvConfig { } fn into_builder(self) -> Self::Builder { - CloudflareKvBuilder { config: self } + CloudflareKvBuilder { + config: self, + default_ttl: None, + } } } @@ -132,6 +134,6 @@ mod tests { let cfg = CloudflareKvConfig::from_iter([("default_ttl".to_string(), "PT1M".to_string())]) .unwrap(); - assert_eq!(cfg.default_ttl, Some(Duration::from_secs(60))); + assert_eq!(cfg.default_ttl, Some(SignedDuration::from_mins(1))); } } diff --git a/core/services/memcached/src/backend.rs b/core/services/memcached/src/backend.rs index 7d6e4a2d85b7..2a259892fe9b 100644 --- a/core/services/memcached/src/backend.rs +++ b/core/services/memcached/src/backend.rs @@ -34,6 +34,7 @@ use super::writer::MemcachedWriter; #[derive(Debug, Default)] pub struct MemcachedBuilder { pub(super) config: MemcachedConfig, + pub(super) default_ttl: Option, } impl MemcachedBuilder { @@ -74,7 +75,7 @@ impl MemcachedBuilder { /// Set the default ttl for memcached services. pub fn default_ttl(mut self, ttl: Duration) -> Self { - self.config.default_ttl = Some(ttl); + self.default_ttl = Some(ttl); self } @@ -97,6 +98,14 @@ impl Builder for MemcachedBuilder { type Config = MemcachedConfig; fn build(self) -> Result { + let default_ttl = match self.default_ttl { + Some(ttl) => Some(ttl), + None => self + .config + .default_ttl + .map(signed_duration_to_duration) + .transpose()?, + }; let endpoint_raw = self.config.endpoint.clone().ok_or_else(|| { Error::new(ErrorKind::ConfigInvalid, "endpoint is empty") .with_context("service", MEMCACHED_SCHEME) @@ -171,7 +180,7 @@ impl Builder for MemcachedBuilder { endpoint, self.config.username, self.config.password, - self.config.default_ttl, + default_ttl, self.config.connection_pool_max_size, )) .with_normalized_root(root)) diff --git a/core/services/memcached/src/config.rs b/core/services/memcached/src/config.rs index cf2e949e68e2..4cc2ad2b81b9 100644 --- a/core/services/memcached/src/config.rs +++ b/core/services/memcached/src/config.rs @@ -44,8 +44,7 @@ pub struct MemcachedConfig { /// Memcached password, optional. pub password: Option, /// The default ttl for put operations. - #[serde(default, deserialize_with = "deserialize_option_duration")] - pub default_ttl: Option, + pub default_ttl: Option, /// The maximum number of connections allowed. /// /// default is 10 @@ -82,7 +81,10 @@ impl Configurator for MemcachedConfig { } fn into_builder(self) -> Self::Builder { - MemcachedBuilder { config: self } + MemcachedBuilder { + config: self, + default_ttl: None, + } } } @@ -107,7 +109,7 @@ mod tests { fn from_iter_parses_default_ttl() -> Result<()> { let cfg = MemcachedConfig::from_iter([("default_ttl".to_string(), "1500ms".to_string())])?; - assert_eq!(cfg.default_ttl, Some(Duration::from_millis(1500))); + assert_eq!(cfg.default_ttl, Some(SignedDuration::from_millis(1500))); Ok(()) } } diff --git a/core/services/redis/src/backend.rs b/core/services/redis/src/backend.rs index 67aa3896d37c..a86665159ef5 100644 --- a/core/services/redis/src/backend.rs +++ b/core/services/redis/src/backend.rs @@ -44,6 +44,7 @@ const DEFAULT_REDIS_PORT: u16 = 6379; #[derive(Debug, Default)] pub struct RedisBuilder { pub(super) config: RedisConfig, + pub(super) default_ttl: Option, } impl RedisBuilder { @@ -108,7 +109,7 @@ impl RedisBuilder { /// /// If set, we will specify `EX` for write operations. pub fn default_ttl(mut self, ttl: Duration) -> Self { - self.config.default_ttl = Some(ttl); + self.default_ttl = Some(ttl); self } @@ -144,6 +145,14 @@ impl Builder for RedisBuilder { type Config = RedisConfig; fn build(self) -> Result { + let default_ttl = match self.default_ttl { + Some(ttl) => Some(ttl), + None => self + .config + .default_ttl + .map(signed_duration_to_duration) + .transpose()?, + }; let root = normalize_root( self.config .root @@ -170,7 +179,7 @@ impl Builder for RedisBuilder { endpoints, None, Some(client), - self.config.default_ttl, + default_ttl, self.config.connection_pool_max_size, )) .with_normalized_root(root)) @@ -194,7 +203,7 @@ impl Builder for RedisBuilder { endpoint, Some(client), None, - self.config.default_ttl, + default_ttl, self.config.connection_pool_max_size, )) .with_normalized_root(root)) diff --git a/core/services/redis/src/config.rs b/core/services/redis/src/config.rs index 03cbfacd729f..4f488dac8e9a 100644 --- a/core/services/redis/src/config.rs +++ b/core/services/redis/src/config.rs @@ -59,8 +59,7 @@ pub struct RedisConfig { /// default is db 0 pub db: i64, /// The default ttl for put operations. - #[serde(default, deserialize_with = "deserialize_option_duration")] - pub default_ttl: Option, + pub default_ttl: Option, } impl Debug for RedisConfig { @@ -121,7 +120,10 @@ impl Configurator for RedisConfig { } fn into_builder(self) -> Self::Builder { - RedisBuilder { config: self } + RedisBuilder { + config: self, + default_ttl: None, + } } } @@ -161,7 +163,7 @@ mod tests { fn from_iter_parses_default_ttl() -> Result<()> { let cfg = RedisConfig::from_iter([("default_ttl".to_string(), "5s".to_string())])?; - assert_eq!(cfg.default_ttl, Some(Duration::from_secs(5))); + assert_eq!(cfg.default_ttl, Some(SignedDuration::from_secs(5))); Ok(()) } diff --git a/dev/src/generate/parser.rs b/dev/src/generate/parser.rs index f46eaf697bd3..df200daaa084 100644 --- a/dev/src/generate/parser.rs +++ b/dev/src/generate/parser.rs @@ -83,7 +83,7 @@ pub enum ConfigType { Bool, /// Mapping to rust's `String` String, - /// Mapping to rust's `Duration` + /// Mapping to rust's `Duration` and `SignedDuration` Duration, /// Mapping to rust's `usize` @@ -113,7 +113,7 @@ impl FromStr for ConfigType { Ok(match s { "bool" => ConfigType::Bool, "String" => ConfigType::String, - "Duration" => ConfigType::Duration, + "Duration" | "SignedDuration" => ConfigType::Duration, "usize" => ConfigType::Usize, "u64" => ConfigType::U64, @@ -514,6 +514,19 @@ mod tests { example: None, }, ), + ( + "default_ttl: Option", + Config { + name: "default_ttl".to_string(), + value: ConfigType::Duration, + optional: true, + deprecated: None, + comments: "".to_string(), + group: None, + default_value: None, + example: None, + }, + ), ]; for (input, expected) in cases { From 779aa7c8eb01b318c0b1f70992b77fbd19d0df7a Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 5 Aug 2026 06:00:59 +0800 Subject: [PATCH 5/5] fine tune uses Signed-off-by: tison --- core/core/src/raw/time.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/core/src/raw/time.rs b/core/core/src/raw/time.rs index 7cc1c957ab82..4ff95f4463a8 100644 --- a/core/core/src/raw/time.rs +++ b/core/core/src/raw/time.rs @@ -18,11 +18,12 @@ //! Time related utils. use crate::*; -pub use jiff::SignedDuration; + use std::fmt; use std::ops::{Add, AddAssign, Sub, SubAssign}; use std::str::FromStr; +pub use jiff::SignedDuration; pub use std::time::{Duration, UNIX_EPOCH}; #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] pub use std::time::{Instant, SystemTime};