PageSourceSearch

https://contributing-docs.pages.dev/assets/js/2050771e.b615ee21.js

js contributing-docs.pages.dev collected 2026-10-03 06:41:05 UTC 15,351 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunk_bitwarden_contributing_docs=globalThis.webpackChunk_bitwarden_contributing_docs||[]).push([[4246],{41620(e,t,n){n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>c});const i=JSON.parse('{"id":"architecture/sdk/client-patterns","title":"Client patterns","description":"Clients group the SDK API surface into domain-specific bundles. For a step-by-step walkthrough of","source":"@site/docs/architecture/sdk/client-patterns.md","sourceDirName":"architecture/sdk","slug":"/architecture/sdk/client-patterns","permalink":"/architecture/sdk/client-patterns","draft":false,"unlisted":false,"editUrl":"https://github.com/bitwarden/contributing-docs/tree/main/docs/architecture/sdk/client-patterns.md","tags":[],"version":"current","sidebarPosition":3,"frontMatter":{"sidebar_position":3},"sidebar":"architecture","previous":{"title":"Crate structure","permalink":"/architecture/sdk/crate-structure"},"next":{"title":"Data models","permalink":"/architecture/sdk/data-models"}}');var r=n(74848),s=n(28453);const o={sidebar_position:3},a="Client patterns",l={},c=[{value:"<code>FromClient</code> and dependency injection",id:"fromclient-and-dependency-injection",level:2},{value:"WASM support",id:"wasm-support",level:3},{value:"UniFFI wrappers",id:"uniffi-wrappers",level:3},{value:"Why the wrappers exist",id:"why-the-wrappers-exist",level:4},{value:"Error conversion",id:"error-conversion",level:4},{value:"Extension traits",id:"extension-traits",level:2},{value:"File organization",id:"file-organization",level:2},{value:"Single file",id:"single-file",level:3},{value:"Per-method files or subdirectories",id:"per-method-files-or-subdirectories",level:3},{value:"Testing",id:"testing",level:2},{value:"Writing test cases",id:"writing-test-cases",level:3}];function d(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.header,{children:(0,r.jsx)(t.h1,{id:"client-patterns",children:"Client patterns"})}),"\n",(0,r.jsxs)(t.p,{children:["Clients group the SDK API surface into domain-specific bundles. For a step-by-step walkthrough of\ncreating a client from scratch and wiring it into the application interface, see\n",(0,r.jsx)(t.a,{href:"/architecture/sdk/adding-functionality",children:"Adding new functionality"}),"."]}),"\n",(0,r.jsxs)(t.h2,{id:"fromclient-and-dependency-injection",children:[(0,r.jsx)(t.code,{children:"FromClient"})," and dependency injection"]}),"\n",(0,r.jsxs)(t.p,{children:["Every client struct declares its dependencies as fields and derives ",(0,r.jsx)(t.code,{children:"FromClient"})," to have them\nautomatically populated from the SDK ",(0,r.jsx)(t.code,{children:"Client"}),". The macro generates a ",(0,r.jsx)(t.code,{children:"from_client"})," method that\nextracts each field using the ",(0,r.jsx)(t.code,{children:"FromClientPart"})," trait \u2014 client structs never call ",(0,r.jsx)(t.code,{children:"Client"})," methods\ndirectly to obtain their dependencies."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-rust",children:"#[derive(FromClient)]\npub struct FoldersClient {\n    pub(crate) key_store: KeyStore<KeySlotIds>,\n    pub(crate) api_configurations: Arc<ApiConfigurations>,\n    pub(crate) repository: Option<Arc<dyn Repository<Folder>>>,\n}\n"})}),"\n",(0,r.jsx)(t.p,{children:"Some of the available dependency types that can be extracted are:"}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsxs)(t.li,{children:[(0,r.jsx)(t.code,{children:"KeyStore<KeySlotIds>"})," \u2014 access to the cryptographic key store"]}),"\n",(0,r.jsxs)(t.li,{children:[(0,r.jsx)(t.code,{children:"Arc<ApiConfigurations>"})," \u2014 HTTP API client configuration"]}),"\n",(0,r.jsxs)(t.li,{children:[(0,r.jsx)(t.code,{children:"Option<Arc<dyn Repository<T>>>"})," \u2014 state repository for a given domain type"]}),"\n"]}),"\n",(0,r.jsxs)(t.p,{children:["This design also makes clients straightforward to test: because dependencies are plain struct\nfields, tests can construct clients directly with test doubles instead of spinning up a full SDK\n",(0,r.jsx)(t.code,{children:"Client"}),". See ",(0,r.jsx)(t.a,{href:"#testing",children:"Testing"})," below."]}),"\n",(0,r.jsx)(t.h3,{id:"wasm-support",children:"WASM support"}),"\n",(0,r.jsxs)(t.p,{children:["If the client will be exposed over WASM, annotate both the struct and its ",(0,r.jsx)(t.code,{children:"impl"})," blocks with:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-rust",children:'#[cfg_attr(feature = "wasm", wasm_bindgen)]\n'})}),"\n",(0,r.jsx)(t.h3,{id:"uniffi-wrappers",children:"UniFFI wrappers"}),"\n",(0,r.jsxs)(t.p,{children:["Expose mobile clients through thin wrapper structs in the ",(0,r.jsx)(t.code,{children:"bitwarden-uniffi"})," crate. Each wrapper is\na ",(0,r.jsx)(t.code,{children:"uniffi::Object"})," that holds the client it delegates to:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-rust",children:"#[derive(uniffi::Object)]\npub struct FoldersClient(pub(crate) bitwarden_vault::FoldersClient);\n\n#[uniffi::export]\nimpl FoldersClient {\n    pub async fn get(&self, folder_id: FolderId) -> Result<FolderView> {\n        Ok(self.0.get(folder_id).await?)\n    }\n}\n"})}),"\n",(0,r.jsxs)(t.p,{children:["The wrapper mirrors the structure of the Rust client hierarchy: parent wrappers exp
1ose child\nwrappers through accessor methods, just like the application interface clients do. ",(0,r.jsx)(t.code,{children:"VaultClient"}),"\nwraps ",(0,r.jsx)(t.code,{children:"bitwarden_vault::VaultClient"}),", and its ",(0,r.jsx)(t.code,{children:"folders()"})," accessor returns the ",(0,r.jsx)(t.code,{children:"FoldersClient"}),"\nwrapper shown above."]}),"\n",(0,r.jsx)(t.h4,{id:"why-the-wrappers-exist",children:"Why the wrappers exist"}),"\n",(0,r.jsxs)(t.p,{children:["UniFFI reports a failed argument conversion by downcasting to the error type declared in the\nexported function's signature, so every method on the mobile surface has to return the same error\ntype, ",(0,r.jsx)(t.code,{children:"BitwardenError"}),". Anything else leaves bad input reaching the client as an undeclared internal\nerror rather than a typed one. ",(0,r.jsx)(t.code,{children:"BitwardenError"})," is defined in ",(0,r.jsx)(t.code,{children:"bitwarden-uniffi"}),", which depends on\nthe feature crates and not the other way round, so a feature crate cannot name it and cannot host\nthe export. That is what forces the wrapper layer."]}),"\n",(0,r.jsxs)(t.p,{children:["The wrapper layer is a temporary workaround for a UniFFI limitation, tracked\n",(0,r.jsx)(t.a,{href:"https://github.com/mozilla/uniffi-rs/issues/2416",children:"upstream"}),". Lifting it would let feature crates export over UniFFI the way they\nalready do over WASM. The full mechanism and the rules that follow from it are documented in\n",(0,r.jsx)(t.a,{href:"https://github.com/bitwarden/sdk-internal/tree/main/crates/bitwarden-uniffi-error",children:(0,r.jsx)(t.code,{children:"bitwarden-uniffi-error"})}),"."]}),"\n",(0,r.jsx)(t.h4,{id:"error-conversion",children:"Error conversion"}),"\n",(0,r.jsxs)(t.p,{children:["UniFFI wrappers use a crate-level ",(0,r.jsx)(t.code,{children:"Result<T>"})," type alias that maps errors to ",(0,r.jsx)(t.code,{children:"BitwardenError"}),". Use\nthe ",(0,r.jsx)(t.code,{children:"?"})," operator in wrapper methods to convert domain-specific errors through the\n",(0,r.jsx)(t.code,{children:"From<E> for BitwardenError"})," implementations."]}),"\n",(0,r.jsxs)(t.p,{children:["When introducing a new error type, add a variant for it in\n",(0,r.jsx)(t.a,{href:"https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-uniffi/src/error.rs",children:(0,r.jsx)(t.code,{children:"bitwarden-uniffi/src/error.rs"})})," and implement the ",(0,r.jsx)(t.code,{children:"From"})," conversion so it can be\npropagated with ",(0,r.jsx)(t.code,{children:"?"}),"."]}),"\n",(0,r.jsx)(t.h2,{id:"extension-traits",children:"Extension traits"}),"\n",(0,r.jsxs)(t.p,{children:["Feature crates connect to the SDK ",(0,r.jsx)(t.code,{children:"Client"})," through extension traits. This keeps feature code\ndecoupled from ",(0,r.jsx)(t.code,{children:"Client"})," \u2014 the trait is defined in the feature crate, not in ",(0,r.jsx)(t.code,{children:"bitwarden-core"}),"."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-rust",children:"pub trait VaultClientExt {\n    fn vault(&self) -> VaultClient;\n}\n\nimpl VaultClientExt for Client {\n    fn vault(&self) -> VaultClient {\n        VaultClient::new(self.clone())\n    }\n}\n"})}),"\n",(0,r.jsxs)(t.p,{children:["The application interface (e.g. ",(0,r.jsx)(t.code,{children:"PasswordManagerClient"}),") imports the extension trait and calls it to\nexpose the feature to consumers."]}),"\n",(0,r.jsx)(t.h2,{id:"file-organization",children:"File organization"}),"\n",(0,r.jsx)(t.p,{children:"Start with everything in a single file. Split when the file grows past ~500 lines (including tests)."}),"\n",(0,r.jsx)(t.h3,{id:"single-file",children:"Single file"}),"\n",(0,r.jsxs)(t.p,{children:["Define the client struct, its initialization, and all method ",(0,r.jsx)(t.code,{children:"impl"})," blocks including tests in one\nfile. This minimizes indirection and keeps related code easy to discover. Prefer this for smaller\ndomains."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"domain_client.rs\n\u251c\u2500\u2500 DomainClient struct definition\n\u2514\u2500\u2500 impl DomainClient { methods and tests }\n"})}),"\n",(0,r.jsx)(t.h3,{id:"per-method-files-or-subdirectories",children:"Per-method files or subdirectories"}),"\n",(0,r.jsxs)(t.p,{children:["When the single file becomes unwieldy, keep the client struct in its own file and give each method\nits own file. Eac
1h file contains the ",(0,r.jsx)(t.code,{children:"impl DomainClient"})," block for that method, its DTOs, error\ntypes, and its tests."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"domain/\n\u251c\u2500\u2500 domain_client.rs     # DomainClient struct definition and initialization\n\u251c\u2500\u2500 mod.rs\n\u251c\u2500\u2500 method_name.rs       # impl DomainClient { fn method_name() } + tests\n\u2514\u2500\u2500 other_method.rs      # impl DomainClient { fn other_method() } + tests\n"})}),"\n",(0,r.jsx)(t.p,{children:"If a method needs many or large supporting types (request/response structs, error enums), promote it\nto a subdirectory:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"domain/\n\u251c\u2500\u2500 domain_client.rs     # DomainClient struct definition and initialization\n\u251c\u2500\u2500 mod.rs\n\u2514\u2500\u2500 method_name/\n    \u251c\u2500\u2500 mod.rs           # impl DomainClient { fn method_name() } + tests\n    \u2514\u2500\u2500 request.rs       # supporting types\n"})}),"\n",(0,r.jsxs)(t.admonition,{title:"Anti-pattern: thin passthroughs",type:"warning",children:[(0,r.jsx)(t.p,{children:"Do not delegate method bodies to free functions. This splits the implementation away from the API\nsurface, makes the client harder to navigate, and obscures what the method actually does in\ngenerated documentation."}),(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-rust",children:"impl LoginClient {\n    // Bad \u2014 the real logic lives somewhere else.\n    pub async fn login_with_password(&self, data: LoginData) -> Result<()> {\n        login_with_password(self.client, data).await\n    }\n}\n"})}),(0,r.jsx)(t.p,{children:"Instead, implement the logic directly in the method body."})]}),"\n",(0,r.jsx)(t.h2,{id:"testing",children:"Testing"}),"\n",(0,r.jsxs)(t.p,{children:["Because client structs declare their dependencies as fields, they can be constructed directly in\ntests without spinning up a full SDK ",(0,r.jsx)(t.code,{children:"Client"}),". Inject test doubles for each dependency to isolate\nthe code under test."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-rust",children:"fn create_test_client() -> FoldersClient {\n    let key_store =\n        create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());\n    let repository = Arc::new(MemoryRepository::<Folder>::default());\n\n    FoldersClient {\n        key_store,\n        api_configurations: Arc::new(ApiConfigurations::from_api_client(\n            ApiClient::new_mocked(|_| {}),\n        )),\n        repository: Some(repository),\n    }\n}\n"})}),"\n",(0,r.jsx)(t.p,{children:"Key points:"}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsxs)(t.li,{children:[(0,r.jsx)(t.strong,{children:"Key store"})," \u2014 use ",(0,r.jsx)(t.code,{children:"create_test_crypto_with_user_key"})," to set up a key store with a test key."]}),"\n",(0,r.jsxs)(t.li,{children:[(0,r.jsx)(t.strong,{children:"API configurations"})," \u2014 use ",(0,r.jsx)(t.code,{children:"ApiClient::new_mocked"})," to create a mock HTTP client. The closure\nreceives requests and can return custom responses."]}),"\n",(0,r.jsxs)(t.li,{children:[(0,r.jsx)(t.strong,{children:"Repositories"})," \u2014 use ",(0,r.jsx)(t.code,{children:"MemoryRepository"})," as an in-memory test double for state repositories.\nPopulate it with test data before exercising the client method."]}),"\n"]}),"\n",(0,r.jsx)(t.h3,{id:"writing-test-cases",children:"Writing test cases"}),"\n",(0,r.jsx)(t.p,{children:"Each public client method should have tests that cover the expected behavior and important error\npaths. Construct the client, set up any required state, call the method, and assert the result."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-rust",children:'#[tokio::test]\nasync fn test_get_folder() {\n    let client = create_test_client();\n    let folder_id = FolderId::new(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"));\n\n    // Populate the repository with test data\n    let folder = client.key_store.encrypt(FolderView {\n        id: Some(folder_id),\
1n        name: "Test Folder".to_string(),\n        revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),\n    }).unwrap();\n\n    client.repository.as_ref().unwrap()\n        .set(folder_id, folder).await.unwrap();\n\n    // Exercise the method and verify\n    let result = client.get(folder_id).await.unwrap();\n    assert_eq!(result.name, "Test Folder");\n}\n\n#[tokio::test]\nasync fn test_get_folder_not_found() {\n    let client = create_test_client();\n    let folder_id = FolderId::new(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"));\n\n    let result = client.get(folder_id).await;\n    assert!(matches!(result.unwrap_err(), GetFolderError::ItemNotFound(_)));\n}\n'})})]})}function h(e={}){const{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},28453(e,t,n){n.d(t,{R:()=>o,x:()=>a});var i=n(96540);const r={},s=i.createContext(r);function o(e){const t=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.