diff --git a/.github/workflows/sdk-spec.yml b/.github/workflows/sdk-spec.yml new file mode 100644 index 0000000..1f19dbb --- /dev/null +++ b/.github/workflows/sdk-spec.yml @@ -0,0 +1,17 @@ +name: SDK spec + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + sdk-spec: + # Keep the ref in `uses:` and `spec-ref` in sync. + uses: Flagsmith/sdk-spec/.github/workflows/run-spec.yml@v0.3.0 + with: + spec-ref: v0.3.0 + language: rust diff --git a/Cargo.toml b/Cargo.toml index 4e4476d..d7e7ea8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ readme = "README.md" categories = ["config", "api-bindings"] keywords = ["Flagsmith", "feature-flag", "remote-config"] rust-version = "1.86.0" +# The SDK spec steps are a separate crate used only in CI. +exclude = ["/sdk-spec"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/renovate.json b/renovate.json index 5464e80..b674be3 100644 --- a/renovate.json +++ b/renovate.json @@ -3,6 +3,15 @@ "extends": ["config:recommended", ":semanticCommitScopeDisabled"], "ignorePresets": [":semanticPrefixFixDepsChoreOthers"], "semanticCommitType": "deps", + "customManagers": [ + { + "customType": "regex", + "managerFilePatterns": ["/^\\.github/workflows/sdk-spec\\.yml$/"], + "matchStrings": ["spec-ref: (?v[\\d.]+)"], + "depNameTemplate": "Flagsmith/sdk-spec", + "datasourceTemplate": "github-tags" + } + ], "packageRules": [ { "matchUpdateTypes": [ @@ -21,6 +30,11 @@ { "matchPackageNames": ["flagsmith-flag-engine"], "enabled": true + }, + { + "matchPackageNames": ["Flagsmith/sdk-spec"], + "groupName": "SDK spec", + "enabled": true } ] } diff --git a/sdk-spec/.gitignore b/sdk-spec/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/sdk-spec/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/sdk-spec/Cargo.toml b/sdk-spec/Cargo.toml new file mode 100644 index 0000000..73cb089 --- /dev/null +++ b/sdk-spec/Cargo.toml @@ -0,0 +1,14 @@ +# The steps file as a crate, built by the Rust harness of the Flagsmith SDK behavioural spec. +[package] +name = "sdk-spec" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +path = "steps.rs" + +[dependencies] +flagsmith = { path = ".." } +flagsmith-flag-engine = "0.6" +serde = { version = "1.0", features = ["derive"] } diff --git a/sdk-spec/Dockerfile b/sdk-spec/Dockerfile new file mode 100644 index 0000000..658ca09 --- /dev/null +++ b/sdk-spec/Dockerfile @@ -0,0 +1,23 @@ +# Runs this SDK, built from source, under the Flagsmith SDK behavioural spec. +# docker build -f sdk-spec/Dockerfile --build-context harness=/harness/rust . +FROM docker.io/library/rust:1.86-slim-bookworm AS build + +WORKDIR /sdk +COPY Cargo.toml Cargo.lock README.md ./ +COPY src ./src +COPY sdk-spec/Cargo.toml sdk-spec/steps.rs ./sdk-spec/ + +# The harness builds the steps file in, from the SDK checkout at /sdk. +COPY --from=harness . /harness +# Resolve dependencies to the versions the SDK is tested with. +COPY Cargo.lock /harness/Cargo.lock +RUN cargo build --release --manifest-path /harness/Cargo.toml + +FROM docker.io/library/debian:bookworm-slim +# The SDK's HTTP client fails to build without system CA certificates. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* +COPY --from=build /harness/target/release/sdk-spec-harness /usr/local/bin/ + +ENTRYPOINT ["sdk-spec-harness"] diff --git a/sdk-spec/steps.rs b/sdk-spec/steps.rs new file mode 100644 index 0000000..52321ce --- /dev/null +++ b/sdk-spec/steps.rs @@ -0,0 +1,59 @@ +//! Steps for the Flagsmith SDK behavioural spec: https://github.com/Flagsmith/sdk-spec +//! +//! Each function implements the Gherkin step in the comment above it. + +use std::collections::HashMap; + +use flagsmith::error::Error; +use flagsmith::flagsmith::models::{Flags, SDKTrait}; +use flagsmith::{Flagsmith, FlagsmithOptions}; +use flagsmith_flag_engine::types::FlagsmithValue; +use serde::Serialize; + +#[derive(Serialize)] +pub struct SpecFlag { + feature: String, + enabled: bool, + value: FlagsmithValue, +} + +fn as_spec_flags(flags: Flags) -> Vec { + flags + .all_flags() + .into_iter() + .map(|flag| SpecFlag { + feature: flag.feature_name, + enabled: flag.enabled, + value: flag.value, + }) + .collect() +} + +// Create the SDK client +pub fn create_client(environment_key: String, api_url: String) -> Flagsmith { + let options = FlagsmithOptions { + api_url, + ..Default::default() + }; + Flagsmith::new(environment_key, options) +} + +// When the application gets the environment flags +pub fn get_environment_flags(flagsmith: &Flagsmith) -> Result, Error> { + flagsmith.get_environment_flags().map(as_spec_flags) +} + +// When the application gets the flags for identity "{identifier}" with traits: +pub fn get_identity_flags( + flagsmith: &Flagsmith, + identifier: String, + traits: HashMap, +) -> Result, Error> { + let traits = traits + .into_iter() + .map(|(key, value)| SDKTrait::new(key, value)) + .collect(); + flagsmith + .get_identity_flags(&identifier, Some(traits), None) + .map(as_spec_flags) +}