diff --git a/CLAUDE.md b/CLAUDE.md index 5c548904..d8cb2c85 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,71 +2,50 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Project Overview +## Overview -OpenFeature Ruby SDK — implements the [OpenFeature specification](https://openfeature.dev) (v0.8.0) for vendor-agnostic feature flag management. Published as the `openfeature-sdk` gem. Pure Ruby, no runtime dependencies. Requires Ruby >= 3.1. +This is the official OpenFeature SDK for Ruby — an implementation of the [OpenFeature specification](https://openfeature.dev) providing a vendor-agnostic API for feature flag evaluation. Published as the `openfeature-sdk` gem. Requires Ruby >= 3.1. ## Commands -```bash -# Install dependencies -bundle install +- **Run all tests:** `bundle exec rspec` +- **Run a single test file:** `bundle exec rspec spec/open_feature/sdk/client_spec.rb` +- **Run a specific test by line:** `bundle exec rspec spec/open_feature/sdk/client_spec.rb:43` +- **Lint:** `bundle exec standardrb` +- **Lint with autofix:** `bundle exec standardrb --fix` +- **Default rake (tests + lint):** `bundle exec rake` -# Run full test suite + linting (default rake task) -bundle exec rake +Note: Linting uses [Standard Ruby](https://github.com/standardrb/standard) (configured via the `standard` gem), which enforces double-quoted strings and its own opinionated style. There is no `.rubocop.yml` — Standard manages RuboCop configuration internally. Do not use `bundle exec rubocop` directly as a stale RuboCop server may apply different rules; always use `bundle exec standardrb`. -# Run tests only -bundle exec rspec - -# Run a single test file -bundle exec rspec spec/open_feature/sdk/client_spec.rb - -# Run a specific test by line number -bundle exec rspec spec/open_feature/sdk/client_spec.rb:40 - -# Lint (StandardRB with performance plugin) -bundle exec rake standard +## Architecture -# Auto-fix lint issues -bundle exec standardrb --fix -``` +### Entry point and API singleton -## Architecture +`OpenFeature::SDK` (in `lib/open_feature/sdk.rb`) delegates all method calls to `API.instance` via `method_missing`. `API` is a Singleton that holds a `Configuration` object and builds `Client` instances. -Entry point: `require 'open_feature/sdk'` — the `OpenFeature::SDK` module delegates all method calls to `API.instance` (Singleton) via `method_missing`. +### Provider duck type -### Core Components +Providers are not subclasses — they follow a duck type interface. Any object implementing `fetch_boolean_value`, `fetch_string_value`, `fetch_number_value`, `fetch_integer_value`, `fetch_float_value`, and `fetch_object_value` (all accepting `flag_key:`, `default_value:`, `evaluation_context:`) works as a provider. Each method must return a `ResolutionDetails` struct. Two built-in providers exist: `NoOpProvider` (default) and `InMemoryProvider` (for testing). Providers may optionally implement `init(evaluation_context)`, `shutdown`, and `metadata`. -- **API** (`lib/open_feature/sdk/api.rb`) — Singleton orchestrator. Manages providers (global or domain-scoped), builds clients, stores API-level evaluation context, and registers event handlers. -- **Configuration** (`lib/open_feature/sdk/configuration.rb`) — Thread-safe provider storage. Handles provider lifecycle (init/shutdown), domain-scoped provider mapping, and event dispatching. Uses Mutex for all shared state. -- **Client** (`lib/open_feature/sdk/client.rb`) — Flag evaluation interface. Uses `class_eval` metaprogramming to generate 12 typed methods: `fetch_{boolean,string,number,integer,float,object}_value` and `fetch_*_details` variants. Merges evaluation contexts (API + client + invocation). -- **EvaluationContext** (`lib/open_feature/sdk/evaluation_context.rb`) — Key-value targeting data with a special `targeting_key`. Supports merging with precedence: invocation > client > API. +### Client dynamic method generation -### Provider System +`Client` uses `class_eval` to metaprogram `fetch__value` and `fetch__details` methods from `RESULT_TYPE` and `SUFFIXES` arrays. This generates 12 public methods (6 types × 2 suffixes). -- **Provider interface** — Must implement 6 `fetch_*_value` methods, optional `init(evaluation_context)` and `shutdown`. Returns `ResolutionDetails`. -- **EventEmitter** (`lib/open_feature/sdk/provider/event_emitter.rb`) — Mixin that providers include to emit lifecycle events. -- **Built-in providers**: `NoOpProvider` (default), `InMemoryProvider` (testing/examples). -- **Provider states**: `NOT_READY → READY`, with `ERROR`, `FATAL`, `STALE` transitions. Tracked per-instance via `ProviderStateRegistry` using `object_id`. -- **Initialization modes**: `set_provider` (async, background thread) or `set_provider_and_wait` (sync, raises `ProviderInitializationError` on failure). +### Evaluation context merging -### Event System +`EvaluationContextBuilder` merges three layers of context with this precedence: invocation > client > API (global). Context is a hash-like object with a special `targeting_key` field. -- **EventDispatcher** (`lib/open_feature/sdk/event_dispatcher.rb`) — Thread-safe pub-sub. Handlers called outside mutex to prevent deadlocks. Supports API-level and client-level handlers. -- **ProviderEvent** constants: `PROVIDER_READY`, `PROVIDER_ERROR`, `PROVIDER_STALE`, `PROVIDER_CONFIGURATION_CHANGED`. +### Provider eventing -## Test Structure +`Configuration` manages provider lifecycle events (READY, ERROR, STALE, CONFIGURATION_CHANGED). Providers can emit spontaneous events by including `Provider::EventEmitter`. Event handlers can be registered at API level (global) or client level (domain-scoped). `ProviderStateRegistry` tracks provider states; `EventDispatcher` manages handler registration and invocation. -Tests in `spec/` split into two categories: -- `spec/specification/` — OpenFeature spec compliance tests, organized by requirement number (e.g., "Requirement 1.1.1") -- `spec/open_feature/` — Unit tests for individual components +### Domain-based provider binding -Uses Timecop for time-sensitive tests (auto-reset after each test), SimpleCov for coverage. +Providers can be registered for specific domains. `Configuration#provider(domain:)` resolves domain-specific providers, falling back to the default (nil-domain) provider. Clients are built with an optional `domain:` that binds them to a specific provider. ## Conventions -- **Linter**: StandardRB (Ruby Standard Style) with `standard-performance` plugin, targeting Ruby 3.1 -- **Commits**: Conventional Commits required for PR titles (enforced by CI) -- **Releases**: Automated via release-please; changelog auto-generated -- **Threading**: All shared mutable state must be Mutex-protected. Provider storage uses immutable reassignment (`@providers = @providers.dup.merge(...)`) -- **Structs for DTOs**: `EvaluationDetails`, `ResolutionDetails`, `ClientMetadata`, `ProviderMetadata` are `Struct`-based +- All `.rb` files must have `# frozen_string_literal: true` as the first line. +- Tests live under `spec/` and mirror the `lib/` structure. `spec/specification/` contains tests mapped to OpenFeature spec requirements. +- Always sign git commits using the `-S` flag. +- Always include DCO sign-off in commits using the `-s` flag (i.e., `git commit -s -S`). This adds a `Signed-off-by` trailer required by the project's CI. diff --git a/lib/open_feature/sdk/client_metadata.rb b/lib/open_feature/sdk/client_metadata.rb index 8bbbcb83..ad33ddaa 100644 --- a/lib/open_feature/sdk/client_metadata.rb +++ b/lib/open_feature/sdk/client_metadata.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OpenFeature module SDK ClientMetadata = Struct.new(:domain, keyword_init: true) diff --git a/lib/open_feature/sdk/evaluation_context.rb b/lib/open_feature/sdk/evaluation_context.rb index b9417ceb..e4fb3581 100644 --- a/lib/open_feature/sdk/evaluation_context.rb +++ b/lib/open_feature/sdk/evaluation_context.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OpenFeature module SDK class EvaluationContext diff --git a/lib/open_feature/sdk/evaluation_context_builder.rb b/lib/open_feature/sdk/evaluation_context_builder.rb index 0b3b7c06..2d1a159a 100644 --- a/lib/open_feature/sdk/evaluation_context_builder.rb +++ b/lib/open_feature/sdk/evaluation_context_builder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OpenFeature module SDK # Used to combine evaluation contexts from different sources diff --git a/lib/open_feature/sdk/evaluation_details.rb b/lib/open_feature/sdk/evaluation_details.rb index 4faccd28..a840e783 100644 --- a/lib/open_feature/sdk/evaluation_details.rb +++ b/lib/open_feature/sdk/evaluation_details.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OpenFeature module SDK EvaluationDetails = Struct.new(:flag_key, :resolution_details, keyword_init: true) do diff --git a/lib/open_feature/sdk/provider.rb b/lib/open_feature/sdk/provider.rb index 6488646b..f3f0fe8e 100644 --- a/lib/open_feature/sdk/provider.rb +++ b/lib/open_feature/sdk/provider.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "provider/error_code" require_relative "provider/reason" require_relative "provider/resolution_details" diff --git a/lib/open_feature/sdk/provider/error_code.rb b/lib/open_feature/sdk/provider/error_code.rb index e133e2a0..335f7a81 100644 --- a/lib/open_feature/sdk/provider/error_code.rb +++ b/lib/open_feature/sdk/provider/error_code.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OpenFeature module SDK module Provider diff --git a/lib/open_feature/sdk/provider/in_memory_provider.rb b/lib/open_feature/sdk/provider/in_memory_provider.rb index 3bfcb971..e74858f0 100644 --- a/lib/open_feature/sdk/provider/in_memory_provider.rb +++ b/lib/open_feature/sdk/provider/in_memory_provider.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OpenFeature module SDK module Provider diff --git a/lib/open_feature/sdk/provider/provider_metadata.rb b/lib/open_feature/sdk/provider/provider_metadata.rb index b4497c0b..4b648cec 100644 --- a/lib/open_feature/sdk/provider/provider_metadata.rb +++ b/lib/open_feature/sdk/provider/provider_metadata.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OpenFeature module SDK module Provider diff --git a/lib/open_feature/sdk/provider/reason.rb b/lib/open_feature/sdk/provider/reason.rb index 48d9be60..28a85d44 100644 --- a/lib/open_feature/sdk/provider/reason.rb +++ b/lib/open_feature/sdk/provider/reason.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OpenFeature module SDK module Provider diff --git a/lib/open_feature/sdk/provider/resolution_details.rb b/lib/open_feature/sdk/provider/resolution_details.rb index c7675fce..8168b9fa 100644 --- a/lib/open_feature/sdk/provider/resolution_details.rb +++ b/lib/open_feature/sdk/provider/resolution_details.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OpenFeature module SDK module Provider diff --git a/spec/open_feature/sdk/evaluation_context_builder_spec.rb b/spec/open_feature/sdk/evaluation_context_builder_spec.rb index e1fbf4f8..152329eb 100644 --- a/spec/open_feature/sdk/evaluation_context_builder_spec.rb +++ b/spec/open_feature/sdk/evaluation_context_builder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe OpenFeature::SDK::EvaluationContextBuilder do diff --git a/spec/open_feature/sdk/evaluation_context_spec.rb b/spec/open_feature/sdk/evaluation_context_spec.rb index d8433e9e..09040425 100644 --- a/spec/open_feature/sdk/evaluation_context_spec.rb +++ b/spec/open_feature/sdk/evaluation_context_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe OpenFeature::SDK::EvaluationContext do diff --git a/spec/open_feature/sdk/provider/in_memory_provider_spec.rb b/spec/open_feature/sdk/provider/in_memory_provider_spec.rb index 1f919299..8b7093ce 100644 --- a/spec/open_feature/sdk/provider/in_memory_provider_spec.rb +++ b/spec/open_feature/sdk/provider/in_memory_provider_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe OpenFeature::SDK::Provider::InMemoryProvider do