apollo-rust-client 0.7.0

A Rust client for Apollo configuration center
Documentation
# SDD Interface Design Viewpoint

This document details the external and internal interfaces of the `apollo-rust-client`, detailing how it binds to JS/WASM runtimes, communicates with Apollo Servers, and coordinates internally.

---

## 1. External Interfaces

### 1.1 WebAssembly & JavaScript Bindings (`wasm-bindgen`)

The client exports standard JavaScript classes to enable integration within Node.js and browser environments. The generated bindings interface with standard Promise structures.

#### Class `ClientConfig` (JavaScript API)
- **Constructor**:
  ```javascript
  constructor(appId: string, configServer: string, cluster: string);
  ```
- **Properties**:
  - `secret: string | null` (Getter/Setter)
  - `label: string | null` (Getter/Setter)
  - `ip: string | null` (Getter/Setter)
  - `allowInsecureHttps: boolean | null` (Getter/Setter)

#### Class `Client` (JavaScript API)
- **Constructor**:
  ```javascript
  constructor(config: ClientConfig);
  ```
- **Methods**:
  - `start(): Promise<void>`: Spawns the async listener refresh loop (WASM).
  - `stop(): Promise<void>`: Stops the background loop.
  - `namespace(namespace: string): Promise<any>`: Retrieves the Javascript representation of the `Namespace` variant (`Properties` class, raw JSON object, YAML string, or raw text string).
  - `add_listener(namespace: string, callback: (data: any, error: string | null) => void): Promise<void>`: Registers an observer callback.

#### Class `Properties` (JavaScript API)
- **Methods**:
  - `get_string(key: string): string | null` (synchronous)
  - `get_int(key: string): number | null` (synchronous)
  - `get_float(key: string): number | null` (synchronous)
  - `get_bool(key: string): boolean | null` (synchronous)

> [!WARNING]
> **Manual Memory Management**: JS runtimes must call `.free()` on all instances of `ClientConfig`, `Client`, and `Properties` (when returned from `namespace()`) created from WASM once they are out of scope. If ignored, the underlying memory allocated in the WebAssembly heap will leak. Other returned namespace formats (such as raw JSON objects, YAML strings, or raw text) are standard JavaScript values/objects managed automatically by JavaScript garbage collection and do not need to be freed manually.

---

### 1.2 Apollo HTTP Integration Protocol

The library retrieves configurations by directly polling Apollo Server JSON endpoints.

- **Request Type**: HTTP `GET`
- **Request URL Layout**:
  ```
  {config_server}/configfiles/json/{app_id}/{cluster}/{namespace}
  ```
- **Query Parameters**:
  - `ip` (Optional): Appended as `?ip={ip_val}` for targeted grayscale releases.
  - `label` (Optional): Appended as `?label={label_val}` for target canary rules.

#### Request Headers & Authentication Signature
If `secret` authentication is configured, the client generates custom HMAC-SHA1 headers on each request:

| Header | Description / Value Format |
| :--- | :--- |
| `timestamp` | Current Unix time in milliseconds (e.g., `1576478257344`). |
| `Authorization` | Signature format: `Apollo {app_id}:{signature}`. |

#### Signature Algorithm
The HMAC-SHA1 signature is constructed as follows:
1. Parse the request URL to extract the path and query string:
   `path_and_query = "/configs/{app_id}/{cluster}/{namespace}?ip={ip}"`
2. Formulate the signing input string:
   `input = "{timestamp_millis}\n{path_and_query}"`
3. Generate the HMAC-SHA1 digest using the `secret` key.
4. Encode the resulting digest byte array as standard **Base64** text.

```rust
// Signature generation signature (Internal)
fn sign(timestamp: i64, url: &str, secret: &str) -> Result<String, Error>;
```

### 1.3 Testing Mock Server (Docker Compose / WireMock)
To decouple the test suite from external, remote Apollo servers (such as the now retired CTrip Apollo community demo server), the client repository includes a standard local Docker Mock Server setup using WireMock.

- **Docker Service Orchestration**: Defined in the [docker-compose.yml]../docker-compose.yml in the root directory.
- **WireMock Stub Mappings**: Stored as JSON definitions under [tests/wiremock/mappings/]../tests/wiremock/mappings/.
- **Dynamic Config Routing**: Test clients read the server destination from the `APOLLO_TEST_SERVER` environment variable, falling back to `http://localhost:8080` (where the local WireMock Docker container is mapped).
- **Self-Managed Test Lifecycle**: The [scripts/test.sh]../scripts/test.sh script automatically starts the WireMock Docker container before testing (`docker compose up -d`), waits for the server to be fully responsive, executes the test suite, and reliably tears down all containers on completion using a shell `trap` handler. This keeps the GitHub Actions workflow extremely simple and requires zero manual container management by local developers.

---

## 2. Internal Interfaces

The coordination between internal modules is asynchronous and thread-safe.

```mermaid
sequenceDiagram
    participant User as Consumer Code
    participant Client as Client Instance
    participant Registry as Namespace Registry (HashMap)
    participant Cache as Cache Instance
    participant Net as Apollo Server HTTP

    User->>Client: namespace("application.json")
    Client->>Registry: Acquire RwLock Write
    alt Cache does not exist
        Registry->>Cache: Create new Cache(Config, Namespace)
        Registry->>Registry: Insert into HashMap
    end
    Registry-->>Client: Return Arc<Cache>
    Client->>Cache: get_value()
    Cache->>Cache: Read in-memory cache
    alt Cache is Empty/Stale
        Cache->>Cache: Lock memory writer
        Cache->>Net: GET /configfiles/json/...
        Net-->>Cache: JSON Response
        Cache->>Cache: Write persistent cache (Native file / WASM localStorage)
        Cache->>Cache: Update in-memory cache
        Cache-->>User: Trigger Registered Observers (Events)
        Cache->>Cache: Unlock memory writer
    end
    Cache-->>Client: Return serde_json::Value
    Client-->>User: Return parsed Namespace (JSON enum)
```

### 2.1 Format Detection Flow
When converting a raw configuration response, `namespace::get_namespace` analyzes the name string:
1. Splitting the string on dot characters (`.`).
2. If no extensions are found, format returns `NamespaceType::Properties`.
3. If extensions exist, it parses the trailing substring:
   - `"json"` $\rightarrow$ `NamespaceType::Json`
   - `"yaml" | "yml"` $\rightarrow$ `NamespaceType::Yaml`
   - `"xml"` $\rightarrow$ `NamespaceType::Xml` (currently unsupported)
   - Other extension $\rightarrow$ `NamespaceType::Text` (default fallback)