diff --git a/AGENTS.md b/AGENTS.md
index ff3b2b9..2e68eed 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -3,17 +3,17 @@
**Agent SHOULD NOT change this file, only suggest changes to user when inconsistency or potential improvement can be done**
## References
-- [Project-specific standarts](docs/project.md)
-- [Arhitecture docs](docs/architecture.md)
+- [Project-specific standards](docs/project.md)
+- [Architecture docs](docs/architecture.md)
- [Project specs (BDD)](openspec/specs/)
## Development Guidelines
-- Always read project standarts and architecture when new session started
-- Cognitive Complexity ([metric by Sonar Source](https://redirect.sonarsource.com/doc/cognitive-complexity.html)) MUST be as low as possible by keeping conditionals simple and nesting levels moderately low (with helper functions and\or declarative approach)
+- Always read project standards and architecture when new session started
+- Cognitive Complexity ([metric by Sonar Source](https://redirect.sonarsource.com/doc/cognitive-complexity.html)) MUST be as low as possible by keeping conditionals simple and nesting levels moderately low (with helper functions and/or declarative approach)
- Module coupling MUST be moderately low to enable clean unit testing and make codebase resilient to changes
-- Module cohesion (module context, knoledge and logix dencity) MUST be as high as possible
-- Code duplication SHOULD be as minimal as possible as long as it's reduces complexity (see the rule about coupling and cohesion)
-- Function length SHOULD be ignored, as long no code or logic duplication is presented and code resposibility in the right place (high cohesion)
+- Module cohesion (module context, knowledge and logic density) MUST be as high as possible
+- Code duplication SHOULD be as minimal as possible as long as it reduces complexity (see the rule about coupling and cohesion)
+- Function length SHOULD be ignored, as long as no code or logic duplication is present and code responsibility is in the right place (high cohesion)
- Data-driven approaches SHOULD be used instead of repetitive control structures (declarative over imperative)
- Core constants or configuration MUST be defined in one place, and derived representations (e.g., a set for fast lookup) SHOULD be derived programmatically.
- When in need to perform frequent membership checks, source-of-truth slice SHOULD be converted into a map (set) once—preferably at initialization (init).
@@ -21,15 +21,15 @@
## Code Design
- Use design-first and TDD principle:
1. Design function interface according to usage need and check it's usability in context
- 2. Write or edit tests for parent code (code where new interface is used), mocking new\edited interface, to ensure host code works as expected
- 3. Write or edir tests for interface itself
+ 2. Write or edit tests for parent code (code where new interface is used), mocking new/edited interface, to ensure host code works as expected
+ 3. Write or edit tests for interface itself
4. Write implementation of interface until tests will pass
## Quality Assurance Guidelines
- All tests MUST follow common development guidelines
- All test functions MUST contain multiline (/**/) comment before function declaration with:
- Gherkin notation of test case
- - List of related requirement scenario codes from opencode/spec at separate line
+ - List of related requirement scenario codes from openspec/specs at separate line
example:
```
@@ -43,16 +43,4 @@
*/
```
- Use parameterized tests when the all test’s steps (AAA) are identical across all cases, and only the input and expected output differ. Otherwise, write separate tests.
-- All tests are divided into "unit" and "integration"
-- Benchmarks can be unit or integrative, and MUST comply with the corresponding rules
-- **Unit tests** - checking one interface at the time
- - MUST call one interface per test exclusively
- - All dependencies including public interfaces calls within project codebase MUST be mocked or stubbed
- - Private interfaces SHOULD NOT be tested directly, although they coverage MUST be implemented indirectly
- - MUST be placed near tested module
- - SHOULD use parralel execution when conflicts completely impossible
- - SHOULD contain one assertion (or one logical group of assertions) per test
-- **Integration tests** - checks ready-to-ship application as a complete system
- - MUST check gaps in unit test cases and system integration result
- - SHOULD NOT call any internal interfaces directly (only bundled system as black box)
- - MUST be placed at test/ or it's subdirectories
+- Unit tests and integration tests placement and conventions are defined in [project standards](docs/project.md#testing-standarts)
diff --git a/README.md b/README.md
index a3ed57f..13a0708 100644
--- a/README.md
+++ b/README.md
@@ -5,18 +5,19 @@


-A Go‑based mock server that leverages OpenAPI 3.0 schemas enhanced with custom extensions for conditional examples, state management, and runtime expressions.
+A Go‑based mock server that leverages OpenAPI 3.0 schemas enhanced with custom extensions for conditional examples, state management, runtime expressions, and JSON-RPC 2.0 support.
## Features
- Loads one or more OpenAPI 3.0 YAML/JSON files (with optional path prefixes)
-- Supports custom extensions (`x‑mock‑params‑match`, `x‑mock‑skip`, `x‑mock‑once`, `x‑mock‑set‑state`, `x‑mock‑headers`)
+- Supports custom extensions (`x‑mock‑match`, `x‑mock‑skip`, `x‑mock‑once`, `x‑mock‑set‑state`, `x‑mock‑headers`; legacy `x‑mock‑params‑match` alias still supported)
- Runtime expressions (`{$request.path.id}`, `{$state.counter}`, `{$env.VAR}`) with modifiers (`default`, `getByPath`, `toJWT`)
- In‑memory state per namespace (get/set/increment/delete)
- Request history ring buffer with filtering via management API
- Dynamic example injection at runtime via HTTP API
- Configurable request delay, CORS, verbose logging
-- Single binary, zero dependencies
+- Single static binary, no runtime dependencies
+- JSON‑RPC 2.0 gateway via `x‑rpc` extension (batch requests, notifications)
## Installation
@@ -70,83 +71,48 @@ curl http://localhost:8080/hello
## OpenAPI Extensions
-OASMock adds several custom extensions to OpenAPI example objects. Full documentation is available in [extensions.md](./extensions.md).
+OASMock adds several custom extensions to OpenAPI example objects. Full reference: [extensions.md](./docs/extensions.md).
-### `x‑mock‑params‑match`
+### Match Conditions (`x‑mock‑match`)
-Selects the example when the request matches the given conditions.
+Selects the example when the request matches the given conditions (deprecated alias: `x‑mock‑params‑match`).
```yaml
examples:
admin:
- x‑mock‑params‑match:
+ x‑mock‑match:
'{$request.header.role}': admin
value:
message: Welcome, admin!
```
-### `x‑mock‑skip`
+### Other Extensions
-Skips the example (useful for temporarily disabling an example).
+| Extension | Purpose |
+|---|---|
+| `x‑mock‑skip` | Temporarily exclude an example |
+| `x‑mock‑once` | One‑time example (removed after first match) |
+| `x‑mock‑set‑state` | Update server‑side state (supports `increment`, `value`, `null` for delete) |
+| `x‑mock‑headers` | Set response headers (runtime expressions in values) |
-### `x‑mock‑once`
+### JSON‑RPC Gateway (`x‑rpc`)
-Makes the example one‑time only (removed after first match).
-
-### `x‑mock‑set‑state`
-
-Updates server‑side state that can be referenced later via `{$state.*}`.
-
-```yaml
-x‑mock‑set‑state:
- counter:
- increment: 1
- 'user-{$request.path.id}':
- value: '{$request.body.name}'
-```
-
-### `x‑mock‑headers`
-
-Sets response headers (supports runtime expressions in values).
+Route calls by body field instead of URL path. See [json-rpc.md](./docs/json-rpc.md).
## Runtime Expressions
-Runtime expressions are enclosed in `{$...}` and can appear in extension keys, values, and response bodies.
-
-### Data Sources
-
-- `{$request.path.param}`
-- `{$request.query.param}`
-- `{$request.header.name}`
-- `{$request.cookie.name}`
-- `{$request.body.field}`
-- `{$state.key}`
-- `{$env.VARIABLE}`
+Runtime expressions are enclosed in `{$...}` and resolved at request time. Data sources: `{$request.path.param}`, `{$request.query.param}`, `{$request.header.name}`, `{$request.body.field}`, `{$request.cookie.name}`, `{$state.key}`, `{$env.VARIABLE}`.
-### Modifiers
+Modifiers: `\|default:value` (fallback), `\|getByPath:path` (traverse nested objects), `\|toJWT` (stub).
-- `{$request.query.id|default:unknown}` – provides a default value if the expression cannot be resolved
-- `{$state.object|getByPath:deep.nested.value}` – traverses an object
-- `{$state.payload|toJWT}` – (stub) encodes the value as a JWT
-
-Embedded expressions are supported:
-
-```yaml
-value:
- url: "/api/users/{$request.path.id}/profile"
-```
+Expressions can appear in extension keys, values, and response bodies. Full reference: [extensions.md](./docs/extensions.md#runtime-expressions).
## Management API
-The server exposes a control HTTP API under the `/_mock` prefix.
-
-### `GET /_mock/requests`
+The server exposes a control HTTP API under the `/_mock` prefix. Full schema: [api/openapi.yaml](./api/openapi.yaml).
-Retrieves the request history (optionally filtered by path, method, time range, etc.).
-
-### `POST /_mock/examples`
-
-Adds a dynamic example to an existing route. The request body follows the schema defined in [openapi.yaml](./api/openapi.yaml).
+- `GET /_mock/requests` — request history (filterable by path, method, time range, pagination)
+- `POST /_mock/examples` — add a dynamic example to an existing route
## Command‑Line Interface
@@ -190,6 +156,16 @@ go test ./...
golangci-lint run
```
+## Further Reading
+
+- [CLI reference](./docs/cli.md) — all flags, env vars, config file (`.oasmock.yaml`)
+- [Extensions & runtime expressions](./docs/extensions.md) — full `x‑mock‑*` / `x‑rpc` reference
+- [JSON‑RPC 2.0](./docs/json-rpc.md) — protocol details, batch support, error codes
+- [Architecture](./docs/architecture.md) — component diagrams, interfaces, data flows
+- [CI/CD](./docs/ci-cd.md) — pipeline, quality gates, release process
+- [Project standards](./docs/project.md) — tech stack, conventions, testing, coverage policy
+- [Specifications (BDD)](./openspec/specs/) — requirement scenarios
+
## License
-MIT
\ No newline at end of file
+MIT
diff --git a/cmd/oasmock/mock.go b/cmd/oasmock/mock.go
index d7beb77..e800120 100644
--- a/cmd/oasmock/mock.go
+++ b/cmd/oasmock/mock.go
@@ -61,25 +61,8 @@ func parseSchemaConfig(cmd *cobra.Command) error {
return nil
}
- schemaVal := viper.Get("schema")
schemasVal := viper.Get("schemas")
- // Check mutual exclusivity
- if schemaVal != nil && schemasVal != nil {
- return validationError("cannot specify both 'schema' and 'schemas' in config file")
- }
-
- // Handle single schema
- if schemaVal != nil {
- schema, ok := schemaVal.(string)
- if !ok {
- return validationError("'schema' must be a string")
- }
- config.sources = []string{schema}
- config.prefixes = []string{}
- return nil
- }
-
// Handle schemas list
if schemasVal != nil {
schemas, ok := schemasVal.([]any)
@@ -162,8 +145,6 @@ func init() {
_ = viper.BindPFlag("nocors", mockCmd.Flags().Lookup("nocors"))
_ = viper.BindPFlag("history_size", mockCmd.Flags().Lookup("history-size"))
_ = viper.BindPFlag("no_control_api", mockCmd.Flags().Lookup("no-control-api"))
- _ = viper.BindPFlag("from", mockCmd.Flags().Lookup("from"))
- _ = viper.BindPFlag("prefix", mockCmd.Flags().Lookup("prefix"))
}
func runMock(cmd *cobra.Command, args []string) error {
diff --git a/cmd/oasmock/mock_test.go b/cmd/oasmock/mock_test.go
index 3f55703..d54d3ec 100644
--- a/cmd/oasmock/mock_test.go
+++ b/cmd/oasmock/mock_test.go
@@ -161,7 +161,7 @@ Given various YAML configuration inputs (valid and invalid)
When parseSchemaConfig is called
Then it should parse valid configurations correctly and return appropriate errors for invalid ones
-Related spec scenarios: RS.CLI.19, RS.CLI.26, RS.CLI.27
+Related spec scenarios: RS.CLI.19, RS.CLI.27
*/
func TestParseSchemaConfig(t *testing.T) {
@@ -184,18 +184,6 @@ func TestParseSchemaConfig(t *testing.T) {
assert.Nil(t, config.prefixes)
},
},
- {
- name: "single schema",
- setup: func(cmd *cobra.Command) {
- viper.Reset()
- config = mockConfig{}
- viper.Set("schema", "custom.yaml")
- },
- check: func(t *testing.T) {
- assert.Equal(t, []string{"custom.yaml"}, config.sources)
- assert.Equal(t, []string{}, config.prefixes)
- },
- },
{
name: "multiple schemas with mixed formats",
setup: func(cmd *cobra.Command) {
@@ -211,16 +199,6 @@ func TestParseSchemaConfig(t *testing.T) {
assert.Equal(t, []string{"/v1", ""}, config.prefixes)
},
},
- {
- name: "both schema and schemas present",
- setup: func(cmd *cobra.Command) {
- viper.Reset()
- config = mockConfig{}
- viper.Set("schema", "single.yaml")
- viper.Set("schemas", []any{"multi.yaml"})
- },
- expectError: true,
- },
{
name: "invalid schemas element",
setup: func(cmd *cobra.Command) {
@@ -230,15 +208,6 @@ func TestParseSchemaConfig(t *testing.T) {
},
expectError: true,
},
- {
- name: "invalid schema type",
- setup: func(cmd *cobra.Command) {
- viper.Reset()
- config = mockConfig{}
- viper.Set("schema", 123)
- },
- expectError: true,
- },
{
name: "schemas object missing src",
setup: func(cmd *cobra.Command) {
@@ -287,8 +256,7 @@ func TestValidYAMLStructure(t *testing.T) {
viper.Reset()
config = mockConfig{}
- yamlConfig := `schema: test.yaml
-port: 8080
+ yamlConfig := `port: 8080
delay: 500
verbose: true
nocors: true
@@ -299,7 +267,6 @@ no-control-api: true`
require.NoError(t, viper.ReadConfig(bytes.NewBufferString(yamlConfig)))
// Verify viper can read all keys
- assert.Equal(t, "test.yaml", viper.GetString("schema"))
assert.Equal(t, 8080, viper.GetInt("port"))
assert.Equal(t, 500, viper.GetInt("delay"))
assert.Equal(t, true, viper.GetBool("verbose"))
@@ -331,7 +298,7 @@ Given configuration values defined in multiple sources (CLI flags, environment v
When the configuration is resolved
Then values from higher precedence sources override those from lower precedence sources
-Related spec scenarios: RS.CLI.22, RS.CLI.23, RS.CLI.28, RS.CLI.29
+Related spec scenarios: RS.CLI.22, RS.CLI.23, RS.CLI.29
*/
func TestConfigPrecedence(t *testing.T) {
// t.Parallel() - cannot use with t.Setenv
@@ -375,11 +342,12 @@ func TestConfigPrecedence(t *testing.T) {
assert.Equal(t, 7070, port, "environment variable should override config file")
})
- t.Run("CLI from flag overrides YAML schema", func(t *testing.T) {
+ t.Run("CLI from flag overrides YAML schemas", func(t *testing.T) {
viper.Reset()
config = mockConfig{}
- // Simulate config file with schema
- yamlConfig := `schema: custom.yaml`
+ // Simulate config file with schemas
+ yamlConfig := `schemas:
+ - custom.yaml`
viper.SetConfigType("yaml")
require.NoError(t, viper.ReadConfig(bytes.NewBufferString(yamlConfig)))
// Create command with --from flag set
@@ -395,7 +363,7 @@ func TestConfigPrecedence(t *testing.T) {
// Call parseSchemaConfig
err := parseSchemaConfig(cmd)
require.NoError(t, err)
- // Should keep flag value, not config file schema
+ // Should keep flag value, not config file schemas
assert.Equal(t, []string{"flag.yaml"}, config.sources)
assert.Equal(t, []string{}, config.prefixes)
})
diff --git a/docs/architecture.md b/docs/architecture.md
index 306db0d..cbbf51d 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,32 +1,52 @@
# OASMock Architecture Documentation
## Table of Contents
-1. [Overview](#overview)
- - [Diagram Conventions](#diagram-conventions)
-2. [Component Architecture](#1-component-architecture)
-3. [Component Details](#2-component-details)
- - [2.1 CLI Component](#21-cli-component-cmdoasmock)
- - [2.2 Server Component](#22-server-component-internalserver)
- - [2.3 Runtime Component](#23-runtime-component-internalruntime)
- - [2.4 Extensions Component](#24-extensions-component-internalextensions)
- - [2.5 Loader Component](#25-loader-component-internalloader)
- - [2.6 State Component](#26-state-component-internalstate)
- - [2.7 History Component](#27-history-component-internalhistory)
- - [2.8 Mock Component](#28-mock-component-mock)
-4. [Sequence Flows](#3-sequence-flows)
- - [3.1 CLI Initialization Flow](#31-cli-initialization-flow)
- - [3.2 HTTP Mock Request Flow](#32-http-mock-request-flow)
- - [3.3 Management API - Dynamic Example Addition](#33-management-api---dynamic-example-addition)
-5. [Interface Definitions](#4-interface-definitions)
- - [4.1 Server Interfaces](#41-server-interfaces-internalserverinterfacesgo)
- - [4.2 Runtime Interfaces](#42-runtime-interfaces-internalruntimeexpressiongo)
- - [4.3 Extension Functions](#43-extension-functions-internalextractionsextractgo)
-6. [Data Flow Summary](#5-data-flow-summary)
-7. [Design Patterns](#6-design-patterns)
-8. [Testing Architecture](#7-testing-architecture)
-9. [Extension Points](#8-extension-points)
-10. [Performance Considerations](#9-performance-considerations)
-11. [Conclusion](#conclusion)
+- [OASMock Architecture Documentation](#oasmock-architecture-documentation)
+ - [Table of Contents](#table-of-contents)
+ - [Overview](#overview)
+ - [Diagram Conventions](#diagram-conventions)
+ - [1. Component Architecture](#1-component-architecture)
+ - [2. Component Details](#2-component-details)
+ - [2.1 CLI Component (`cmd/oasmock/`)](#21-cli-component-cmdoasmock)
+ - [2.2 Server Component (`internal/server/`)](#22-server-component-internalserver)
+ - [2.3 Runtime Component (`internal/runtime/`)](#23-runtime-component-internalruntime)
+ - [2.4 Extensions Component (`internal/extensions/`)](#24-extensions-component-internalextensions)
+ - [2.5 Loader Component (`internal/loader/`)](#25-loader-component-internalloader)
+ - [2.6 State Component (`internal/state/`)](#26-state-component-internalstate)
+ - [2.7 History Component (`internal/history/`)](#27-history-component-internalhistory)
+ - [2.8 Mock Component (`mock/`)](#28-mock-component-mock)
+ - [3. Sequence Flows](#3-sequence-flows)
+ - [3.1 CLI Initialization Flow](#31-cli-initialization-flow)
+ - [3.2 HTTP Mock Request Flow](#32-http-mock-request-flow)
+ - [3.3 Management API - Dynamic Example Addition](#33-management-api---dynamic-example-addition)
+ - [4. Interface Definitions](#4-interface-definitions)
+ - [4.1 Server Interfaces (`internal/server/interfaces.go`)](#41-server-interfaces-internalserverinterfacesgo)
+ - [4.2 Runtime Interfaces (`internal/runtime/expression.go`)](#42-runtime-interfaces-internalruntimeexpressiongo)
+ - [4.3 Extension Functions (`internal/extensions/extract.go`)](#43-extension-functions-internalextensionsextractgo)
+ - [5. Data Flow Summary](#5-data-flow-summary)
+ - [5.1 Initialization Flow](#51-initialization-flow)
+ - [5.2 Request Handling Flow](#52-request-handling-flow)
+ - [5.3 State Management Flow](#53-state-management-flow)
+ - [5.4 History Tracking Flow](#54-history-tracking-flow)
+ - [5.5 Dynamic Example Flow](#55-dynamic-example-flow)
+ - [6. Design Patterns](#6-design-patterns)
+ - [6.1 Dependency Injection](#61-dependency-injection)
+ - [6.2 Adapter Pattern](#62-adapter-pattern)
+ - [6.3 Factory Pattern](#63-factory-pattern)
+ - [6.4 Strategy Pattern](#64-strategy-pattern)
+ - [6.5 Observer Pattern](#65-observer-pattern)
+ - [7. Testing Architecture](#7-testing-architecture)
+ - [7.1 Mock Generation](#71-mock-generation)
+ - [7.2 Unit Testing Strategy](#72-unit-testing-strategy)
+ - [7.3 Integration Testing](#73-integration-testing)
+ - [8. Extension Points](#8-extension-points)
+ - [8.1 Custom Data Sources](#81-custom-data-sources)
+ - [8.2 Custom Extension Processing](#82-custom-extension-processing)
+ - [8.3 Custom State/History Stores](#83-custom-statehistory-stores)
+ - [9. Performance Considerations](#9-performance-considerations)
+ - [9.1 Memory Management](#91-memory-management)
+ - [9.2 Concurrency](#92-concurrency)
+ - [9.3 Runtime Evaluation](#93-runtime-evaluation)
## Overview
@@ -34,225 +54,83 @@ OASMock is an OpenAPI-based mock server with a modular architecture built in Go.
### Diagram Conventions
-All PlantUML diagrams in this document follow the guidelines from the [plantuml-creator skill](../.opencode/skills/plantuml-creator/SKILL.md) to ensure consistency and maintainability:
+All diagrams in this document use Mermaid syntax for broad compatibility across Git SaaS platforms and IDEs:
-- **Source annotations**: Each diagram element that corresponds to actual code includes a source reference in the format `/'source: @/path/to/file.go:line'/` linking to the relevant file and line number.
-- **Modern styling**: Diagrams use the modern `
-
-title 🏗️ OASMock Component Architecture
-
-package "🎯 Presentation Layer" #E8F5E9 {
- /'source: @/cmd/oasmock'/
- [CLI] <> as CLI
- /'source: @/cmd/oasmock'/
- note right of CLI
- **Responsibilities**:
- - Parse command-line arguments
- - Load configuration
- - Start server
- - Handle graceful shutdown
- **Key Files**:
- - cmd/oasmock/root.go
- - cmd/oasmock/mock.go
- end note
-}
-
-package "⚙️ Application Layer" #E3F2FD {
- /'source: @/internal/server'/
- [Server\n(internal/server/)] <> as Server
- /'source: @/internal/server/server.go:101'/
- note right of Server
- **Responsibilities**:
- - HTTP server management
- - Request routing
- - Response generation
- - Component coordination
- **Key Interfaces**:
- - RouteProvider
- - StateStore
- - HistoryStore
- - ExpressionEvaluator
- - ExtensionProcessor
- end note
-}
-
-package "🔧 Domain Layer" #F3E5F5 {
- /'source: @/internal'/
- [Runtime\n(internal/runtime/)] <> as Runtime
- /'source: @/internal/runtime/expression.go:180'/
- note right of Runtime
- **Responsibilities**:
- - Runtime expression evaluation
- - Data source abstraction
- **Key Interfaces**:
- - DataSource
- - Evaluator
- **Implementations**:
- - RequestSource
- - StateSource
- - EnvSource
- end note
-
- [Extensions\n(internal/extensions/)] <> as Extensions
- /'source: @/internal/extensions/extract.go:20'/
- note right of Extensions
- **Responsibilities**:
- - Process OpenAPI extensions
- - x-mock-* extension handling
- **Functions**:
- - ExtractSetState()
- - ExtractParamsMatch()
- - EvaluateParamsMatch()
- - ExtractHeaders()
- end note
-}
-
-package "🛠️ Infrastructure Layer" #FFF8E1 {
- /'source: @/internal'/
- [Loader\n(internal/loader/)] <> as Loader
- /'source: @/internal/loader/schema.go:20'/
- note right of Loader
- **Responsibilities**:
- - Load OpenAPI schemas
- - Build route mappings
- - Path pattern conversion
- **Key Types**:
- - SchemaInfo
- - RouteMapping
- end note
-
- [State\n(internal/state/)] <> as State
- /'source: @/internal/state/state.go:20'/
- note right of State
- **Responsibilities**:
- - Namespaced key-value storage
- - Thread-safe operations
- **Key Type**:
- - Manager
- **Operations**:
- - Get/Set/Increment/Delete
- - GetNamespace/GetAll
- end note
-
- [History\n(internal/history/)] <> as History
- /'source: @/internal/history/history.go:20'/
- note right of History
- **Responsibilities**:
- - Request/response history
- - Ring buffer storage
- **Key Types**:
- - RequestRecord
- - ResponseRecord
- - RingBuffer
- end note
-
- [Mock\n(mock/)] <> as Mock
- /'source: @/mock'/
- note right of Mock
- **Responsibilities**:
- - Generated interface mocks
- - Unit testing support
- **Packages**:
- - mock_runtime
- - mock_server
- **Generated via**:
- - go:generate mockgen
- end note
-}
-
-' Dependency Relationships
-CLI --> Loader : Load schemas
-CLI --> Server : Start server
-
-Server --> Loader : RouteProvider.BuildRouteMappings()
-Server --> Runtime : ExpressionEvaluator, DataSource factories
-Server --> Extensions : ExtensionProcessor
-Server --> State : StateStore operations
-Server --> History : HistoryStore operations
-
-Extensions --> Runtime : Uses Evaluator for param matching
-
-Mock ..> Server : Mocks all server interfaces
-Mock ..> Runtime : Mocks runtime interfaces
-
-legend right
- | Color | Layer |
- | Light Green | Presentation |
- | Light Blue | Application |
- | Light Purple | Domain |
- | Light Yellow | Infrastructure |
-endlegend
-
-caption Figure 1: High-level component architecture
-
-@enduml
+```mermaid
+flowchart LR
+ subgraph PRES["🎯 Presentation Layer"]
+ CLI["CLI
cmd/oasmock/"]
+ end
+ subgraph APP["⚙️ Application Layer"]
+ Server["Server
internal/server/"]
+ end
+ subgraph DOM["🔧 Domain Layer"]
+ Runtime["Runtime
internal/runtime/"]
+ Extensions["Extensions
internal/extensions/"]
+ end
+ subgraph INF["🛠️ Infrastructure Layer"]
+ Loader["Loader
internal/loader/"]
+ State["State
internal/state/"]
+ History["History
internal/history/"]
+ Mock["Mock
mock/"]
+ end
+ subgraph LEGEND["Legend"]
+ direction LR
+ L1["Presentation"]
+ L2["Application"]
+ L3["Domain"]
+ L4["Infrastructure"]
+ end
+
+ CLI -->|Load schemas| Loader
+ CLI -->|Start server| Server
+
+ Server -->|"RouteProvider.BuildRouteMappings()"| Loader
+ Server -->|ExpressionEvaluator, DataSource factories| Runtime
+ Server -->|ExtensionProcessor| Extensions
+ Server -->|StateStore operations| State
+ Server -->|HistoryStore operations| History
+
+ Extensions -->|Uses Evaluator for param matching| Runtime
+
+ Mock -.->|Mocks all server interfaces| Server
+ Mock -.->|Mocks runtime interfaces| Runtime
+
+ classDef green fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
+ classDef blue fill:#E3F2FD,stroke:#1565C0,color:#0D47A1
+ classDef purple fill:#F3E5F5,stroke:#7B1FA2,color:#4A148C
+ classDef orange fill:#FFF3E0,stroke:#F57C00,color:#E65100
+ classDef yellow fill:#FFF8E1,stroke:#FF8F00,color:#FF6F00
+ classDef pink fill:#FFEBEE,stroke:#C2185B,color:#880E4F
+ classDef teal fill:#E0F2F1,stroke:#00897B,color:#004D40
+ classDef gray fill:#F5F5F5,stroke:#616161,color:#424242
+ classDef leg fill:none,stroke:#ccc,color:#424242
+
+ class CLI green
+ class Server blue
+ class Runtime purple
+ class Extensions orange
+ class Loader yellow
+ class State pink
+ class History teal
+ class Mock gray
+ class L1 green
+ class L2 blue
+ class L3 purple
+ class L4 yellow
```
+*Figure 1: High-level component architecture*
+
## 2. Component Details
### 2.1 CLI Component (`cmd/oasmock/`)
@@ -274,8 +152,14 @@ caption Figure 1: High-level component architecture
- **Key Files**:
- `server.go` - Main server implementation and HTTP handlers
- `interfaces.go` - All public interfaces and dependency definitions
- - `server_management.go` - Management API endpoints
- - `wrappers.go` - Adapter implementations
+ - `server_management.go` - Management API endpoints
+ - `server_example.go` - Example selection and response generation
+ - `server_eval.go` - Runtime expression evaluation integration
+ - `server_state.go` - State management helpers
+ - `jsonrpc.go` - JSON-RPC handler (gateway requests)
+ - `jsonrpc_protocol.go` - JSON-RPC 2.0 protocol parsing and error responses
+ - `wrappers.go` - Adapter implementations
+ - `adapters/` - Formal adapter layer for external components
- **Public Interfaces**:
- `RouteProvider` - Builds route mappings from OpenAPI schemas
- `StateStore` - Manages namespaced state with CRUD operations
@@ -289,7 +173,8 @@ caption Figure 1: High-level component architecture
- Response generation and example selection
- Runtime expression evaluation coordination
- Extension processing and state updates
- - Management API endpoints (`/_mock/examples`, `/_mock/requests`)
+ - Management API endpoints (`/_mock/examples`, `/_mock/requests`)
+ - RPC gateway dispatch (JSON-RPC to operation mapping via `x-rpc`)
- **Dependencies**: Loader, Runtime, Extensions, State, History
### 2.3 Runtime Component (`internal/runtime/`)
@@ -318,7 +203,7 @@ caption Figure 1: High-level component architecture
- `x-mock-set-state` - Set server state after response
- `x-mock-skip` - Skip example from selection
- `x-mock-once` - Use example only once
- - `x-mock-params-match` - Conditional example selection
+ - `x-mock-match` - Conditional example selection (legacy alias: `x-mock-params-match`)
- `x-mock-headers` - Custom response headers
- **Functions**:
- `ExtractSetState()`, `ExtractParamsMatch()`, `ExtractHeaders()`
@@ -388,7 +273,24 @@ caption Figure 1: High-level component architecture
- Thread-safe concurrent access
- **Dependencies**: None (self-contained)
-### 2.8 Mock Component (`mock/`)
+### 2.8 RPC Protocol Subsystem (`internal/server/` and `internal/loader/`)
+**Purpose**: JSON-RPC 2.0 gateway enabling procedure dispatch by body field instead of URL path.
+- **Key Files**:
+ - `internal/server/jsonrpc.go` - RPC handler (batch support, notification handling)
+ - `internal/server/jsonrpc_protocol.go` - JSON-RPC 2.0 protocol (parse, error responses)
+ - `internal/loader/rpc.go` - `x-rpc` extension parsing
+ - `internal/loader/rpc_config.go` - RPC configuration types
+- **Key Interfaces**:
+ - `RpcProtocol` - `ParseBody()`, `ErrorResponse()`, `ContentType()`
+ - `RpcCall` - Parsed call representation (Procedure, ID, Raw body)
+- **Responsibilities**:
+ - Parse batch and single JSON-RPC request bodies
+ - Dispatch each call through the example selection pipeline
+ - Handle notifications (no response entry)
+ - Generate standard JSON-RPC 2.0 error responses
+- **Dependencies**: Extensions (for x-mock-* processing), Runtime (expression evaluation)
+
+### 2.9 Mock Component (`mock/`)
**Purpose**: Generated interface mocks for unit testing.
- **Packages**:
- `mock_runtime` - Mocks for runtime interfaces (`DataSource`, `Evaluator`)
@@ -407,114 +309,68 @@ caption Figure 1: High-level component architecture
### 3.1 CLI Initialization Flow
-```plantuml
-@startuml cli-init-sequence-grouped
-!theme cerulean
-autonumber "[000]"
-
-
-
-title 🌱 CLI Initialization Flow - Component Grouped
-
-actor "👤 User" as User
-
-box "🎯 CLI Component" #E8F5E9 /'source: @/cmd/oasmock'/
- participant "🖥️ CLI\n(cmd/oasmock)" as CLI /'source: @/cmd/oasmock/root.go:46'/
-end box
-
-box "🛠️ Loader Component" #FFF8E1 /'source: @/internal/loader'/
- participant "📚 loader.LoadSchemas" as Loader /'source: @/internal/loader/schema.go:20'/
-end box
-
-box "⚙️ Server Component" #E3F2FD /'source: @/internal/server'/
- participant "🏗️ server.New" as ServerNew /'source: @/internal/server/server.go:101'/
- participant "🔧 RouteProvider" as RouteProvider /'source: @/internal/server/interfaces.go:13'/
- participant "🚀 Server.Start" as ServerStart /'source: @/internal/server/server.go:447'/
- participant "🌐 http.Server" as HTTPServer
-end box
-
-box "💾 Infrastructure Components" #E0F2F1 /'source: @/internal'/
- participant "💾 StateStore" as StateStore /'source: @/internal/server/interfaces.go:37'/
- participant "📜 HistoryStore" as HistoryStore /'source: @/internal/server/interfaces.go:55'/
- participant "⚙️ Runtime Factories" as RuntimeFactories /'source: @/internal/server/interfaces.go:97'/
-end box
-
-== Configuration Phase ==
-
-User -> CLI ++ : Execute `oasmock mock --from ...`
-CLI -> CLI : Parse flags & config (viper)
-CLI --> User : Validate configuration
-
-== Schema Loading ==
-
-CLI -> Loader ++ : LoadSchemas(sources, prefixes)
-loop for each source
- Loader -> Loader : loadSingleSchema()
- Loader -> Loader : openapi3.NewLoader().LoadFromData()
- Loader -> Loader : spec.Validate()
-end
-Loader --> CLI -- : []loader.SchemaInfo
-
-== Server Initialization ==
-
-CLI -> ServerNew ++ : New(config, schemas)
-ServerNew -> ServerNew : Convert schemas to server.SchemaInfo
-ServerNew -> ServerNew : Create default dependencies
-
-ServerNew -> RouteProvider ++ : BuildRouteMappings(schemas)
-RouteProvider -> RouteProvider : Process OpenAPI paths/operations
-RouteProvider --> ServerNew -- : []server.RouteMapping
-
-ServerNew -> StateStore : Initialize state manager
-ServerNew -> HistoryStore : Initialize ring buffer
-ServerNew -> RuntimeFactories : Create data source factories
-
-ServerNew -> ServerNew : Initialize routeMap, onceExamples
-ServerNew -> ServerNew : setupRouter() with middleware
-ServerNew --> CLI -- : *Server instance
-
-== Server Startup ==
-
-CLI -> ServerStart ++ : Start() (goroutine)
-ServerStart -> HTTPServer ++ : ListenAndServe()
-HTTPServer --> ServerStart -- : Listening on port
-ServerStart --> CLI -- : Server running
-
-CLI -> CLI : Wait for interrupt signal
-CLI --> User -- : Server ready message
-
-caption Figure 2: CLI startup sequence
-
-@enduml
+```mermaid
+sequenceDiagram
+ autonumber
+
+ actor User as 👤 User
+ box 🎯 CLI Component
+ participant CLI as 🖥️ CLI
(cmd/oasmock)
+ end
+ box 🛠️ Loader Component
+ participant Loader as 📚 loader.LoadSchemas
+ end
+ box ⚙️ Server Component
+ participant ServerNew as 🏗️ server.New
+ participant RouteProvider as 🔧 RouteProvider
+ participant ServerStart as 🚀 Server.Start
+ participant HTTPServer as 🌐 http.Server
+ end
+ box 💾 Infrastructure Components
+ participant StateStore as 💾 StateStore
+ participant HistoryStore as 📜 HistoryStore
+ participant RuntimeFactories as ⚙️ Runtime Factories
+ end
+
+ Note over User,CLI: === Configuration Phase ===
+ User->>+CLI: Execute `oasmock mock --from ...`
+ CLI->>CLI: Parse flags & config (viper)
+ CLI-->>User: Validate configuration
+
+ Note over CLI,Loader: === Schema Loading ===
+ CLI->>+Loader: LoadSchemas(sources, prefixes)
+ loop for each source
+ Loader->>Loader: loadSingleSchema()
+ Loader->>Loader: openapi3.NewLoader().LoadFromData()
+ Loader->>Loader: spec.Validate()
+ end
+ Loader-->>-CLI: []loader.SchemaInfo
+
+ Note over CLI,ServerNew: === Server Initialization ===
+ CLI->>+ServerNew: New(config, schemas)
+ ServerNew->>ServerNew: Convert schemas to server.SchemaInfo
+ ServerNew->>ServerNew: Create default dependencies
+ ServerNew->>+RouteProvider: BuildRouteMappings(schemas)
+ RouteProvider->>RouteProvider: Process OpenAPI paths/operations
+ RouteProvider-->>-ServerNew: []server.RouteMapping
+ ServerNew->>StateStore: Initialize state manager
+ ServerNew->>HistoryStore: Initialize ring buffer
+ ServerNew->>RuntimeFactories: Create data source factories
+ ServerNew->>ServerNew: Initialize routeMap, onceExamples
+ ServerNew->>ServerNew: setupRouter() with middleware
+ ServerNew-->>-CLI: *Server instance
+
+ Note over CLI,HTTPServer: === Server Startup ===
+ CLI->>+ServerStart: Start() (goroutine)
+ ServerStart->>+HTTPServer: ListenAndServe()
+ HTTPServer-->>-ServerStart: Listening on port
+ ServerStart-->>-CLI: Server running
+ CLI->>CLI: Wait for interrupt signal
+ CLI-->>-User: Server ready message
```
+*Figure 2: CLI startup sequence*
+
**Description**:
1. **User Interaction**: CLI component parses command-line arguments and validates configuration
2. **Schema Loading**: Loader component loads and validates OpenAPI schemas from files
@@ -524,124 +380,73 @@ caption Figure 2: CLI startup sequence
### 3.2 HTTP Mock Request Flow
-```plantuml
-@startuml http-mock-request-sequence-grouped
-!theme bluegray
-autonumber "[000]"
-
-
-
-title 🌐 HTTP Mock Request Flow - Component Grouped
-
-actor "👤 HTTP Client" as Client
-
-box "⚙️ Server Component" #E3F2FD /'source: @/internal/server'/
- participant "🛣️ Server Router" as Router /'source: @/internal/server/server.go:286'/
- entity "📍 RouteMapping" as Mapping /'source: @/internal/server/interfaces.go:19'/
- participant "📄 Response Generator" as ResponseGen /'source: @/internal/server/server_example.go:95'/
-end box
-
-box "🔧 Runtime Component" #F3E5F5 /'source: @/internal/runtime'/
- participant "⚡ Runtime.Evaluator" as Evaluator /'source: @/internal/runtime/expression.go:180'/
- participant "📤 RequestSource" as RequestSource /'source: @/internal/runtime/expression.go:77'/
- participant "💾 StateSource" as StateSource /'source: @/internal/runtime/expression.go:134'/
- participant "🌍 EnvSource" as EnvSource /'source: @/internal/runtime/expression.go:162'/
-end box
-
-box "🔌 Extensions Component" #FFF3E0 /'source: @/internal/extensions'/
- control "🔌 ExtensionProcessor" as ExtProcessor /'source: @/internal/server/interfaces.go:123'/
-end box
-
-== Request Reception & Middleware ==
-
-Client -> Router ++ : GET /v1/users/123
-Router -> Router : Middleware stack execution
-
-== Route Lookup ==
-
-Router -> Mapping ++ : Route lookup via routeKey()
-Mapping --> Router -- : RouteMapping struct
-
-== Runtime Environment Setup ==
-
-Router -> Evaluator ++ : runtime.NewEvaluator()
-Evaluator -> RequestSource ++ : AddSource("request", source)
-RequestSource -> RequestSource : Parse path/query/headers/body
-RequestSource --> Evaluator -- : DataSource ready
-
-Evaluator -> StateSource ++ : AddSource("state", source)
-StateSource -> StateSource : Get namespace data
-StateSource --> Evaluator -- : DataSource ready
-
-Evaluator -> EnvSource ++ : AddSource("env", source)
-EnvSource -> EnvSource : Read OS environment
-EnvSource --> Evaluator -- : DataSource ready
-
-Evaluator --> Router -- : Configured evaluator
-
-== Response Selection & Processing ==
-
-group Response Selection
- Router -> Router : selectResponse(mapping, evaluator)
- Router -> Router : selectMediaType(response)
- Router -> Router : selectDynamicExample() / selectExample()
-end
-
-group Extension Processing
- Router -> ExtProcessor ++ : ExtractSetState(example)
- ExtProcessor --> Router : map[string]any
-
- Router -> ExtProcessor : ExtractParamsMatch(example)
- ExtProcessor --> Router : ParamsMatch
-
- Router -> ExtProcessor : EvaluateParamsMatch(params, evaluator)
- ExtProcessor -> ExtProcessor : Evaluate runtime expressions
- ExtProcessor --> Router -- : bool match result
-end
-
-== Response Generation ==
-
-Router -> ResponseGen ++ : generateResponse(example, evaluator)
-ResponseGen -> ResponseGen : Evaluate runtime expressions
-ResponseGen -> ResponseGen : Apply state updates via StateStore
-ResponseGen --> Router -- : body, headers, statusCode
-
-== Final Response ==
-
-Router -> Client : HTTP Response (200 OK)
-deactivate Router
-
-caption Figure 3: HTTP request handling sequence
-
-@enduml
+```mermaid
+sequenceDiagram
+ autonumber
+
+ actor Client as 👤 HTTP Client
+ box ⚙️ Server Component
+ participant Router as 🛣️ Server Router
+ participant Mapping as 📍 RouteMapping
+ participant ResponseGen as 📄 Response Generator
+ end
+ box 🔧 Runtime Component
+ participant Evaluator as ⚡ Runtime.Evaluator
+ participant RequestSource as 📤 RequestSource
+ participant StateSource as 💾 StateSource
+ participant EnvSource as 🌍 EnvSource
+ end
+ box 🔌 Extensions Component
+ participant ExtProcessor as 🔌 ExtensionProcessor
+ end
+
+ Note over Client,Router: === Request Reception & Middleware ===
+ Client->>+Router: GET /v1/users/123
+ Router->>Router: Middleware stack execution
+
+ Note over Router,Mapping: === Route Lookup ===
+ Router->>+Mapping: Route lookup via routeKey()
+ Mapping-->>-Router: RouteMapping struct
+
+ Note over Router,Evaluator: === Runtime Environment Setup ===
+ Router->>+Evaluator: runtime.NewEvaluator()
+ Evaluator->>+RequestSource: AddSource("request", source)
+ RequestSource->>RequestSource: Parse path/query/headers/body
+ RequestSource-->>-Evaluator: DataSource ready
+ Evaluator->>+StateSource: AddSource("state", source)
+ StateSource->>StateSource: Get namespace data
+ StateSource-->>-Evaluator: DataSource ready
+ Evaluator->>+EnvSource: AddSource("env", source)
+ EnvSource->>EnvSource: Read OS environment
+ EnvSource-->>-Evaluator: DataSource ready
+ Evaluator-->>-Router: Configured evaluator
+
+ Note over Router,Router: === Response Selection & Processing ===
+ Router->>Router: selectResponse(mapping, evaluator)
+ Router->>Router: selectMediaType(response)
+ Router->>Router: selectDynamicExample() / selectExample()
+
+ Router->>+ExtProcessor: ExtractSetState(example)
+ ExtProcessor-->>Router: map[string]any
+ Router->>ExtProcessor: ExtractParamsMatch(example)
+ ExtProcessor-->>Router: ParamsMatch
+ Router->>ExtProcessor: EvaluateParamsMatch(params, evaluator)
+ ExtProcessor->>ExtProcessor: Evaluate runtime expressions
+ ExtProcessor-->>-Router: bool match result
+
+ Note over Router,ResponseGen: === Response Generation ===
+ Router->>+ResponseGen: generateResponse(example, evaluator)
+ ResponseGen->>ResponseGen: Evaluate runtime expressions
+ ResponseGen->>ResponseGen: Apply state updates via StateStore
+ ResponseGen-->>-Router: body, headers, statusCode
+
+ Note over Client,Router: === Final Response ===
+ Router->>Client: HTTP Response (200 OK)
+ deactivate Router
```
+*Figure 3: HTTP request handling sequence*
+
**Description**:
1. **Request Reception**: Server component receives HTTP request and processes middleware
2. **Route Resolution**: RouteMapping lookup finds matching OpenAPI operation
@@ -652,117 +457,64 @@ caption Figure 3: HTTP request handling sequence
### 3.3 Management API - Dynamic Example Addition
-```plantuml
-@startuml management-api-sequence-grouped
-!theme bluegray
-autonumber "[000]"
-
-
-
-title 🛠️ Management API - Dynamic Example Addition
-
-actor "👤 HTTP Client" as Client
-
-box "⚙️ Server Component" #E3F2FD /'source: @/internal/server'/
- participant "🛣️ Server Router" as Router /'source: @/internal/server/server.go:286'/
- control "✅ Request Validator" as Validator /'source: @/internal/server/server_management.go:50'/
- participant "📍 RouteMapping" as Mapping /'source: @/internal/server/interfaces.go:19'/
- database "➕ Dynamic Examples Store" as DynStore /'source: @/internal/server/server_management.go:177'/
- participant "📤 Response Builder" as ResponseBuilder /'source: @/internal/server/server.go:35'/
-end box
-
-== Request Reception ==
-
-Client -> Router ++ : POST /_mock/examples\nContent-Type: application/json
-Router -> Router : Parse JSON body
-
-== Request Validation ==
-
-group Schema Validation
- Router -> Validator ++ : validateAddExampleRequest(body)
- Validator -> Validator : JSON schema validation (gojsonschema)
-
- alt Invalid Schema
- Validator --> Router : Validation error
- Router --> Client : 400 Bad Request\n{"error": "..."}
+```mermaid
+sequenceDiagram
+ autonumber
+
+ actor Client as 👤 HTTP Client
+ box ⚙️ Server Component
+ participant Router as 🛣️ Server Router
+ participant Validator as ✅ Request Validator
+ participant Mapping as 📍 RouteMapping
+ participant DynStore as ➕ Dynamic Examples Store
+ participant ResponseBuilder as 📤 Response Builder
+ end
+
+ Note over Client,Router: === Request Reception ===
+ Client->>+Router: POST /_mock/examples
Content-Type: application/json
+ Router->>Router: Parse JSON body
+
+ Note over Router,Validator: === Request Validation ===
+ Router->>+Validator: validateAddExampleRequest(body)
+ Validator->>Validator: JSON schema validation (gojsonschema)
+ alt Invalid Schema
+ Validator-->>Router: Validation error
+ Router-->>Client: 400 Bad Request
{"error": "..."}
+ else Valid Schema
+ Validator-->>-Router: Validation passed
+ end
+
+ Note over Router,Mapping: === Route Matching ===
+ Router->>+Mapping: Find matching route
(Pattern, Method)
+ Mapping->>Mapping: Search through []RouteMapping
+ alt No Match Found
+ Mapping-->>Router: nil
+ Router-->>Client: 400 No matching route
+ else Match Found
+ Mapping-->>-Router: *RouteMapping
+ end
+
+ Note over Router,DynStore: === Dynamic Example Creation ===
+ Router->>+DynStore: Create dynamicExample struct
+ DynStore->>DynStore: Parse conditions, response
+ DynStore->>DynStore: Generate unique ID
+ DynStore-->>-Router: dynamicExample ready
+
+ Note over Router,DynStore: === Storage Operation ===
+ Router->>DynStore: Store under routeKey
+ DynStore->>DynStore: Append to examples slice
+ DynStore-->>Router: Success
+
+ Note over Client,Router: === Success Response ===
+ Router->>+ResponseBuilder: Build success response
+ ResponseBuilder->>ResponseBuilder: JSON encoding
+ ResponseBuilder-->>-Router: Success message
+ Router-->>Client: 200 OK
{"success": true, "id": "dynex-...", "message": "Example added"}
deactivate Router
-
- else Valid Schema
- Validator --> Router -- : Validation passed
- end
-end
-
-== Route Matching ==
-
-Router -> Mapping ++ : Find matching route\n(Pattern, Method)
-Mapping -> Mapping : Search through []RouteMapping
-
-alt No Match Found
- Mapping --> Router : nil
- Router --> Client : 400 No matching route
- deactivate Router
-
-else Match Found
- Mapping --> Router -- : *RouteMapping
-end
-
-== Dynamic Example Creation ==
-
-Router -> DynStore ++ : Create dynamicExample struct
-DynStore -> DynStore : Parse conditions, response
-DynStore -> DynStore : Generate unique ID
-DynStore --> Router -- : dynamicExample ready
-
-== Storage Operation ==
-
-Router -> DynStore : Store under routeKey
-DynStore -> DynStore : Append to examples slice
-DynStore --> Router : Success
-
-== Success Response ==
-
-Router -> ResponseBuilder ++ : Build success response
-ResponseBuilder -> ResponseBuilder : JSON encoding
-ResponseBuilder --> Router -- : Success message
-Router --> Client : 200 OK\n{"success": true, "id": "dynex-...", "message": "Example added"}
-deactivate Router
-
-caption Figure 4: Dynamic example addition via management API
-
-@enduml
```
+*Figure 4: Dynamic example addition via management API*
+
**Description**:
1. **API Request**: Client sends POST request to management API endpoint
2. **Request Validation**: Server validates JSON payload against schema
@@ -871,7 +623,7 @@ type EnvSource struct {
### 4.3 Extension Functions (`internal/extensions/extract.go`)
```go
-// ExtractParamsMatch extracts the x-mock-params-match extension
+// ExtractParamsMatch extracts the x-mock-match (or x-mock-params-match) extension from an example.
func ExtractParamsMatch(ex *openapi3.Example) (ParamsMatch, bool)
// ExtractSkip extracts x-mock-skip extension
@@ -886,7 +638,8 @@ func ExtractSetState(ex *openapi3.Example) (map[string]any, bool)
// ExtractHeaders extracts x-mock-headers extension
func ExtractHeaders(ex *openapi3.Example) (map[string]any, bool)
-// EvaluateParamsMatch evaluates parameter matching conditions
+// EvaluateParamsMatch evaluates parameter matching conditions.
+// The ParamsMatch type is an alias for map[string]any (defined in extensions/match.go).
func EvaluateParamsMatch(pm ParamsMatch, eval runtime.Evaluator) (bool, error)
```
@@ -920,6 +673,13 @@ Management API request → Validation → Route matching → Create dynamic exam
Store in Server → Future requests can use dynamic example
```
+### 5.6 JSON-RPC Flow
+```
+JSON-RPC request → RpcProtocol.ParseBody → Per-call dispatch (batch) →
+RouteMapping lookup by procedure name → Expression evaluation →
+Example selection → State / history / response collection
+```
+
## 6. Design Patterns
### 6.1 Dependency Injection
@@ -938,9 +698,10 @@ Store in Server → Future requests can use dynamic example
- Different data sources and evaluators can be plugged in
- Extension processing strategies for different x-mock-* extensions
-### 6.5 Observer Pattern
-- History tracks all requests/responses
-- State updates observable through management API
+### 6.5 History Tracking / State Exposure
+- History ring buffer tracks all requests/responses for later inspection
+- State is queryable through management API endpoints
+- Middleware-based recording pattern decouples tracking from business logic
## 7. Testing Architecture
@@ -973,6 +734,10 @@ Store in Server → Future requests can use dynamic example
- Implement `StateStore` or `HistoryStore` interfaces
- Replace default implementations via `Dependencies`
+### 8.4 Custom RPC Protocols
+- Implement `RpcProtocol` interface (`ParseBody`, `ErrorResponse`, `ContentType`)
+- Configure via `x-rpc.protocolType` in the OpenAPI spec
+
## 9. Performance Considerations
### 9.1 Memory Management
@@ -988,17 +753,3 @@ Store in Server → Future requests can use dynamic example
### 9.3 Runtime Evaluation
- Expression caching could be added for performance
- Simple path parsing algorithm with O(n) complexity
-
-## Conclusion
-
-OASMock follows a clean, modular architecture with clear separation of concerns. The component-based design enables testability, maintainability, and extensibility. Key strengths include:
-
-1. **Modularity**: Clear component boundaries with defined interfaces
-2. **Testability**: Dependency injection and mock generation support
-3. **Extensibility**: Pluggable interfaces for custom implementations
-4. **Performance**: Efficient algorithms and memory management
-5. **Standards Compliance**: Full OpenAPI 3.0 support with extensions
-
-The architecture supports both static OpenAPI-based mocking and dynamic runtime behavior through extensions and the management API.
-
-The server also supports protocol-level routing via `x-rpc`: a single gateway endpoint dispatches requests by procedure name (extracted from the request body), reusing the same example selection, expression evaluation, and extension processing pipeline. See [JSON-RPC Documentation](json-rpc.md) for details.
\ No newline at end of file
diff --git a/docs/ci-cd.md b/docs/ci-cd.md
index 0340eff..c150932 100644
--- a/docs/ci-cd.md
+++ b/docs/ci-cd.md
@@ -14,55 +14,36 @@ The OASMock CI/CD pipeline is a unified GitHub Actions workflow that ensures cod
## Pipeline Architecture
-```plantuml
-@startuml
-title OASMock CI/CD Pipeline
-skinparam backgroundColor #F5F5F5
-skinparam activityBackgroundColor #FFFFFF
-skinparam activityBorderColor #333333
-skinparam activityFontSize 14
-skinparam arrowColor #333333
-
-start
-
-partition "Parallel Fast Checks" {
- :Unit Tests & Coverage;
- :Spec Coverage Check;
-}
-
-partition "Build & Package" {
- :Build Binaries;
- note right
- Cross-compile for:
- - Linux (amd64)
- - macOS (amd64)
- - Windows (amd64)
- end note
- :Upload Artifacts;
-}
-
-partition "Integration Testing" {
- :Download Linux Binary;
- :Run Integration Tests;
-}
-
-partition "Release (Tags Only)" {
- :Create GitHub Release;
- :Publish npm Package;
-}
-
-stop
-
-' Dependency arrows
-Unit Tests & Coverage --> Build Binaries
-Spec Coverage Check --> Build Binaries
-Build Binaries --> Upload Artifacts
-Upload Artifacts --> Download Linux Binary
-Download Linux Binary --> Run Integration Tests
-Run Integration Tests --> Create GitHub Release
-Create GitHub Release --> Publish npm Package
-
-@enduml
+```mermaid
+flowchart TD
+ subgraph P1["Parallel Fast Checks"]
+ direction LR
+ A[Unit Tests & Coverage]
+ B[Spec Coverage Check]
+ end
+
+ subgraph P2["Build & Package"]
+ C[Build Binaries
Cross-compile for:
• Linux amd64
• macOS amd64
• Windows amd64]
+ D[Upload Artifacts]
+ end
+
+ subgraph P3["Integration Testing"]
+ E[Download Linux Binary]
+ F[Run Integration Tests]
+ end
+
+ subgraph P4["Release Tags Only"]
+ G[Create GitHub Release]
+ H[Publish npm Package]
+ end
+
+ A --> C
+ B --> C
+ C --> D
+ D --> E
+ E --> F
+ F --> G
+ G --> H
```
## Trigger Events
@@ -170,44 +151,39 @@ Create GitHub Release --> Publish npm Package
## Artifact Flow
-```plantuml
-@startuml
-title Artifact Flow Through Pipeline
-skinparam backgroundColor #F5F5F5
-
-rectangle "Build Job" as build {
- file "dist/oasmock-linux-amd64" as linux
- file "dist/oasmock-darwin-amd64" as darwin
- file "dist/oasmock-windows-amd64.exe" as windows
-}
-
-database "GitHub Artifacts" as artifacts {
- file "oasmock-binaries" as artifact
-}
-
-rectangle "Integration Tests" as integration {
- file "bin/oasmock-linux-amd64" as testbin
-}
-
-rectangle "Release Job" as release {
- file "dist/*" as releasebin
-}
-
-cloud "GitHub Release" as github {
- file "Release Assets" as assets
-}
-
-cloud "npm Registry" as npm {
- file "npm package" as npmpkg
-}
-
-build --> artifacts : Upload
-artifacts --> integration : Download (linux only)
-artifacts --> release : Download (all)
-release --> github : Create release with binaries
-release --> npm : Publish package
-
-@enduml
+```mermaid
+flowchart LR
+ subgraph BUILD["Build Job"]
+ A[dist/oasmock-linux-amd64]
+ B[dist/oasmock-darwin-amd64]
+ C[dist/oasmock-windows-amd64.exe]
+ end
+
+ subgraph ARTIFACTS["GitHub Artifacts"]
+ D[oasmock-binaries]
+ end
+
+ subgraph INTEG["Integration Tests"]
+ E[bin/oasmock-linux-amd64]
+ end
+
+ subgraph RELEASE["Release Job"]
+ F[dist/*]
+ end
+
+ subgraph GH["GitHub Release"]
+ G[Release Assets]
+ end
+
+ subgraph NPM["npm Registry"]
+ H[npm package]
+ end
+
+ BUILD -->|Upload| ARTIFACTS
+ ARTIFACTS -->|Download linux only| INTEG
+ ARTIFACTS -->|Download all| RELEASE
+ RELEASE -->|Create release with binaries| GH
+ RELEASE -->|Publish package| NPM
```
## Environment Variables
@@ -272,15 +248,3 @@ To add support for a new platform (e.g., ARM64):
- `scripts/check-coverage.sh` - Coverage check script
- `scripts/analyze_scenario_coverage.py` - Spec coverage analysis
- `test/_shared/binhelper/` - Binary helper for integration tests
-
-## Migration from Previous Workflows
-
-This unified pipeline replaces:
-- `.github/workflows/go.yml` (build and test)
-- `.github/workflows/release.yml` (release)
-
-The new pipeline provides:
-- Better parallelism (unit tests + spec coverage)
-- Single build artifact reused across jobs
-- Consistent "test what you ship" approach
-- Simplified maintenance with single workflow file
\ No newline at end of file
diff --git a/docs/cli.md b/docs/cli.md
index c7852cf..0c40dea 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -17,16 +17,17 @@ oasmock [options]
| `--from` | string | `src/openapi.yaml` | Source OpenAPI schema. Can be specified multiple times. |
| `--prefix` | string | `''` | URI prefix for the schema. Can be specified for each `--from` parameter. |
| `--port` | number | `19191` | Port to listen on. |
-| `--delay` | number | `0` | Delay between request and response in milliseconds. |
+| `--delay` | number | `100` | Delay between request and response in milliseconds. |
| `--verbose` | boolean | `false` | Enable verbose logging. |
| `--nocors` | boolean | `false` | Disable automatic CORS compliance. |
| `--no-control-api` | boolean | `false` | Disable management HTTP API served at _mock/ |
+| `--history-size` | number | `1000` | Maximum number of requests to keep in history. |
| `--version`, `-v` | boolean | `false` | Show version information and exit. |
| `--help`, `-h` | boolean | `false` | Show global help and exit. |
### Environment Variables
-All values overridable by they cli options counterparts.
+All values are overridable by their CLI option counterparts.
| Variable | Description |
|--------------------------|-------------------------------------|
@@ -34,6 +35,7 @@ All values overridable by they cli options counterparts.
| `OASMOCK_VERBOSE` | If `true`, enables verbose logging. |
| `OASMOCK_NO_CORS` | If `true`, disables CORS. |
| `OASMOCK_NO_CONTROL_API` | Disable management HTTP API. |
+| `OASMOCK_HISTORY_SIZE` | Maximum request history size. |
### Configuration File
@@ -49,25 +51,17 @@ The CLI can read configuration from a `.oasmock.yaml` file in the current workin
| Key | Type | Description |
|-------------------|---------------------|--------------------------------------------------------------------------|
-| `schema` | string | Single OpenAPI schema path. Mutually exclusive with `schemas`. |
| `schemas` | list | Multiple schemas, each either a string (path) or object with `src` and optional `prefix`. |
| `port` | number | Port to listen on. |
| `delay` | number | Delay between request and response in milliseconds. |
| `verbose` | boolean | Enable verbose logging. |
| `nocors` | boolean | Disable automatic CORS compliance. |
-| `history-size` | number | Maximum number of requests to keep in history. |
-| `no-control-api` | boolean | Disable the management control API. |
+| `history_size` | number | Maximum number of requests to keep in history. |
+| `no_control_api` | boolean | Disable the management control API. |
**Examples:**
-**Single schema:**
-```yaml
-schema: ../some/path/openapi.yaml
-port: 8080
-verbose: true
-```
-
- **Multiple schemas with prefixes:**
+**Multiple schemas with prefixes:**
```yaml
schemas:
- src: api/v1/openapi.yaml
@@ -78,17 +72,6 @@ delay: 500
nocors: true
```
-**All options (single schema):**
-```yaml
-schema: api/openapi.yaml
-port: 9090
-delay: 200
-verbose: true
-nocors: false
-history-size: 100
-no-control-api: false
-```
-
### Examples
**Start mock server with default params**:
@@ -123,5 +106,4 @@ oasmock --nocors
| 1 | General error |
| 2 | Invalid command‑line arguments |
| 3 | Schema loading or validation failed |
-| 4 | Port already in use |
diff --git a/docs/extensions.md b/docs/extensions.md
index 0a8822f..f5b6ce2 100644
--- a/docs/extensions.md
+++ b/docs/extensions.md
@@ -1,6 +1,6 @@
# OASMock OpenAPI Extensions
-This document describes the custom OpenAPI extensions used by the OpenApi Mock tool.
+This document describes the custom OpenAPI extensions used by OASMock.
## x-mock-match
@@ -12,7 +12,7 @@ This document describes the custom OpenAPI extensions used by the OpenApi Mock t
```yaml
examples:
first:
- x-mock-params-match:
+ x-mock-match:
'{$request.query.id}': 12
'{$request.query.limit}':
type: number
@@ -21,7 +21,7 @@ examples:
# ... example data
```
-**Alias**: x-mock-params-match - deprecated, only for backward compatibility
+**Alias**: x-mock-params-match — deprecated, kept for backward compatibility
## x-mock-skip
@@ -41,7 +41,7 @@ examples:
**Location**: OAS example object
-**Purpose**: Sets server state that can be used later as a condition in `x-mock-params-match`. Runtime expressions are available in keys and values.
+**Purpose**: Sets server state that can be used later as a condition in `x-mock-match`. Runtime expressions are available in keys and values.
**Example**:
```yaml
@@ -49,7 +49,7 @@ examples:
first:
x-mock-set-state:
state-plain-key: '{$request.body.param}'
- 'state-mixed-{$request.cookie.some}': plain value,
+ 'state-mixed-{$request.cookie.some}': plain value
'{$request.cookie.some}': plain value
state-obj-key:
value:
@@ -59,7 +59,7 @@ examples:
deleted-state-key: null
```
-### x-mock-headers
+## x-mock-headers
**Location**: OAS example object
@@ -76,7 +76,7 @@ examples:
- 'cookie-name=second cookie;'
```
-### x-mock-once
+## x-mock-once
**Location**: OAS example object
@@ -90,55 +90,46 @@ examples:
# ... example data
```
-## Runtime Expressions
-
-Runtime expressions like `{$request.url}` are evaluated inside keys and values of mock extensions. Dot as part of a property name must be escaped: `{$request.cookie.dot\.dot}`.
-
-Value modifiers can be specified after a `|` sign. Example: `{$request.path.param|encodeURIComponent}`.
-
-### Custom Modifiers
-
-| Modifier | Description | Example |
-|--------------|-----------------------------------------------------------------------------|---------------------------------------------------|
-| `default` | Returns a default value if the provided data is empty. | `{$request.path.param\|default:some default value}` |
-| `getByPath` | Returns part of an object or array by a dot‑separated path. | `{$state.someObject\|getByPath:some.example.array.last}` |
-| `toJWT` | Packs the provided object into JWT format (expires in 1h, aud="mock‑client"). | `{$state.someObject\|toJWT}` |
-| `getJWKn` | *(To be documented)* | |
-| `getJWKe` | *(To be documented)* | |
-
-### Available Data
-
-| Expression example | Description | Value Example |
-|-------------------------------|-----------------------------------------------------|------------------------------------------------|
-| `$url` | Full request URL | `https://example.org/api/pathParamValue/?param=value` |
-| `$method` | Request method | `POST` |
-| `$request.path.param` | Path parameters (declared in routes) | `pathParamValue` |
-| `$request.query.param` | Query string parameters | `value` |
-| `$request.header.header-name` | Request headers | `application/json` |
-| `$request.body.param` | Data from request body (JSON or form) | `some body data` |
-| `$request.cookie.cookieName` | Parsed request cookies | `value from cookie` |
-| `$state.someSavedParam` | State data (set previously with `x-mock-set-state`) | `param saved to state` |
-| `$env.ENV_VAR` | Runtime environment variables | `value from env` |
-
## x-rpc
**Location**: OpenAPI document root
**Purpose**: Configures an RPC gateway endpoint for routing calls by body field instead of URL path. Currently supports JSON-RPC 2.0.
-**Example**:
+**Minimal example**:
```yaml
x-rpc:
gateway: /rpc
protocolType: json-rpc
- procedure:
- call: method
- match: post.operationId
```
-When `x-rpc` is present, all POST operations under the gateway path are treated as RPC procedures. The procedure name is derived from the operation's `operationId`. Requests are dispatched by matching the `method` field in the JSON-RPC request body against the procedure name.
+When `x-rpc` is present, all POST operations under the gateway path are treated as RPC procedures. The procedure name is derived from the field specified at `x-rpc.procedure.match`. Requests are dispatched by matching the field specified in `x-rpc.procedure.call` in the JSON-RPC request body against the procedure name.
For JSON-RPC contexts, `{$request.body.id}`, `{$request.body.method}`, and `{$request.body.params.*}` expressions evaluate against the individual call object (not the batch array), enabling per-call resolution in batch requests.
See [JSON-RPC Documentation](json-rpc.md) for full details.
+# Runtime Expressions
+
+Runtime expressions like `{$request.path.id}` are evaluated inside keys and values of mock extensions. Dot as part of a property name must be escaped: `{$request.cookie.dot\.dot}`.
+
+Value modifiers can be specified after a `|` sign. Example: `{$request.path.param|default:not-found}`.
+
+### Custom Modifiers
+
+| Modifier | Description | Example |
+|--------------|-----------------------------------------------------------------------------|---------------------------------------------------|
+| `default` | Returns a default value if the provided data is empty. | `{$request.path.param\|default:some default value}` |
+| `getByPath` | Returns part of an object or array by a dot‑separated path. | `{$state.someObject\|getByPath:some.example.array.last}` |
+| `toJWT` | (stub) Returns a placeholder JWT‑like string. | `{$state.someObject\|toJWT}` |
+### Available Data
+
+| Expression example | Description |
+|----------------------------------------|-----------------------------------------------------|
+| `{$request.path.param}` | Path parameters (declared in routes) |
+| `{$request.query.param}` | Query string parameters |
+| `{$request.header.header-name}` | Request headers |
+| `{$request.body.param}` | Data from request body (JSON or form) |
+| `{$request.cookie.cookieName}` | Parsed request cookies |
+| `{$state.someSavedParam}` | State data (set previously with `x-mock-set-state`) |
+| `{$env.ENV_VAR}` | Runtime environment variables |
diff --git a/docs/json-rpc.md b/docs/json-rpc.md
index bce2c5f..b1714aa 100644
--- a/docs/json-rpc.md
+++ b/docs/json-rpc.md
@@ -111,14 +111,14 @@ A single OpenAPI spec can contain both RPC procedures and normal HTTP routes. Pa
Start the server with a spec containing `x-rpc`:
```bash
-oasmock mock --schema spec.yaml
+oasmock mock --from spec.yaml
```
With a schema prefix:
```bash
-oasmock mock --schema spec.yaml --prefix /api
+oasmock mock --from spec.yaml --prefix /api
# Gateway available at POST /api/rpc
```
-All existing flags (`--port`, `--delay`, `--verbose`, `--cors`, `--history-size`, `--control-api`) work identically with RPC-enabled specs.
+All existing flags (`--port`, `--delay`, `--verbose`, `--nocors`, `--history-size`, `--no-control-api`) work identically with RPC-enabled specs.
diff --git a/docs/project.md b/docs/project.md
index 2d49299..c6caf32 100644
--- a/docs/project.md
+++ b/docs/project.md
@@ -17,7 +17,7 @@
- `test/_shared` - Common files for tests codebase including fixtures, helper functions, resources etc
- `test/_shared/resources` - Various resources (e.g. yaml, json files)
- `docs/` - Project documentation
- - `docs/diagrams` - PlantUML diagrams
+ - `docs/diagrams` - PlantUML diagrams (container for extracted `.puml` files)
## Conventions
@@ -30,8 +30,20 @@
- Use Testify package for all test assertions
- Define multiple test scenarios using slice of structs
- Each test should be marked with list of requirement scenario codes from [openspec's specs](`openspec/specs`)
-- Unit tests: place near tested module with `_test.go` suffix
-- Integration tests: place under `test/` directory, skip when `testing.Short()`
+- Benchmarks can be unit or integrative, and MUST comply with the corresponding rules
+- **Unit tests** - checking one interface at the time
+ - SHOULD be placed near tested module with `_test.go` suffix
+ - MUST call one interface per test exclusively
+ - All dependencies including public interfaces calls within project codebase MUST be mocked or stubbed
+ - Private interfaces SHOULD NOT be tested directly, although they coverage MUST be implemented indirectly
+ - MUST be placed near tested module
+ - SHOULD use parralel execution when conflicts completely impossible
+ - SHOULD contain one assertion (or one logical group of assertions) per test
+- **Integration tests** - checks ready-to-ship application as a complete system
+ - SHOULD be placed under `test/` directory, skip when `testing.Short()`
+ - MUST check gaps in unit test cases and system integration result
+ - SHOULD NOT call any internal interfaces directly (only bundled system as black box)
+ - MUST be placed at test/ or it's subdirectories
## Coverage Policy
- **Code coverage**: Minimum threshold (currently 70%) that must not regress
diff --git a/openspec/specs/cli/spec.md b/openspec/specs/cli/spec.md
index 634d9cf..e522550 100644
--- a/openspec/specs/cli/spec.md
+++ b/openspec/specs/cli/spec.md
@@ -87,7 +87,7 @@ The CLI SHALL return appropriate exit codes as defined in [cli.md](../../../../c
- **THEN** the CLI exits with code 1
### Requirement: Configuration file support
-The CLI SHALL read configuration from a `.oasmock.yaml` file in the current working directory (or user home directory). The configuration file SHALL use simplified schema configuration keys: `schema` (single string) for one schema, `schemas` (list) for multiple schemas. Each element in `schemas` SHALL be either a string (schema path) or an object with `src` and optional `prefix`. Other options SHALL use kebab‑case keys matching CLI flag names (`port`, `delay`, `verbose`, `nocors`, `history‑size`, `no‑control‑api`).
+The CLI SHALL read configuration from a `.oasmock.yaml` file in the current working directory (or user home directory). The configuration file SHALL use the `schemas` list key for schema configuration. Each element in `schemas` SHALL be either a string (schema path) or an object with `src` and optional `prefix`. Other options SHALL use kebab‑case keys matching CLI flag names (`port`, `delay`, `verbose`, `nocors`, `history‑size`, `no‑control‑api`).
#### Scenario RS.CLI.19: Config file present with valid YAML
- **WHEN** a `.oasmock.yaml` file exists in the current directory with valid YAML content
@@ -109,14 +109,6 @@ The CLI SHALL read configuration from a `.oasmock.yaml` file in the current work
- **WHEN** a configuration value is defined both in `.oasmock.yaml` and as an environment variable (e.g., `port: 8080` in YAML and `OASMOCK_PORT=7070`)
- **THEN** the CLI uses the value from the environment variable (unless overridden by a CLI flag)
-#### Scenario RS.CLI.24: Single schema configuration
-- **WHEN** a `.oasmock.yaml` file contains:
- ```yaml
- schema: ../some/path/openapi.yaml
- port: 8080
- ```
-- **THEN** the CLI loads the single schema from the specified path, as if `--from ../some/path/openapi.yaml` were given on the command line
-
#### Scenario RS.CLI.25: Multiple schemas configuration
- **WHEN** a `.oasmock.yaml` file contains:
```yaml
@@ -127,18 +119,10 @@ The CLI SHALL read configuration from a `.oasmock.yaml` file in the current work
```
- **THEN** the CLI loads both schemas, the first with prefix `/url/prefix` and the second without prefix, as if `--from ../some/path/openapi.yaml --prefix /url/prefix --from ../path/unprefixed.openapi.yaml` were given on the command line
-#### Scenario RS.CLI.26: Invalid schema configuration (both schema and schemas)
-- **WHEN** a `.oasmock.yaml` file contains both `schema` and `schemas` keys
-- **THEN** the CLI reports an error and exits with code 2 (invalid command-line arguments)
-
#### Scenario RS.CLI.27: Invalid schemas list element
- **WHEN** a `.oasmock.yaml` file contains a `schemas` list with an element that is neither a string nor an object with `src`
- **THEN** the CLI reports an error and exits with code 2 (invalid command-line arguments)
-#### Scenario RS.CLI.28: CLI flag overrides YAML schema configuration
-- **WHEN** a `.oasmock.yaml` file contains `schema: path/to/schema.yaml` and the user runs `oasmock --from other.yaml`
-- **THEN** the CLI loads `other.yaml` (ignoring the YAML schema configuration)
-
#### Scenario RS.CLI.29: CLI flag overrides YAML schemas configuration
- **WHEN** a `.oasmock.yaml` file contains `schemas:` list with multiple schemas and the user runs `oasmock --from single.yaml`
- **THEN** the CLI loads `single.yaml` (ignoring the YAML schemas configuration)
diff --git a/openspec/specs/json-rpc/spec.md b/openspec/specs/json-rpc/spec.md
index 173fbf1..8dacb00 100644
--- a/openspec/specs/json-rpc/spec.md
+++ b/openspec/specs/json-rpc/spec.md
@@ -54,8 +54,8 @@ The mock server SHALL parse incoming JSON-RPC request bodies according to the pr
- **THEN** the server parses it as one call with procedure "subtract", params `{"a":10}`, id `1`, HasID=true
#### Scenario RS.JRP.11: Batch parsing
-- **WHEN** a POST body is an array of two JSON-RPC call objects
-- **THEN** the server parses it into two calls with correct fields per element
+- **WHEN** a POST body is an array of two or more JSON-RPC call objects
+- **THEN** the server parses it into same number of calls with correct fields per element
#### Scenario RS.JRP.12: Invalid JSON body
- **WHEN** the POST body is not valid JSON
@@ -92,8 +92,8 @@ The mock server SHALL route a single JSON-RPC call to the matching operation and
The mock server SHALL process batch JSON-RPC calls (array body) and return an array of responses.
#### Scenario RS.JRP.19: Batch processing
-- **WHEN** a POST body is an array of two valid calls with different ids and methods
-- **THEN** the response is a JSON array of two response envelopes with matching ids
+- **WHEN** a POST body is an array of two or more valid calls with different ids and methods
+- **THEN** the response is a JSON array of corresponding number of response envelopes with matching ids
#### Scenario RS.JRP.20: Batch with mixed success and error
- **WHEN** a batch contains one valid method and one unknown method
diff --git a/test/cli/cli_integration_test.go b/test/cli/cli_integration_test.go
index 431ca92..f3683c6 100644
--- a/test/cli/cli_integration_test.go
+++ b/test/cli/cli_integration_test.go
@@ -678,7 +678,7 @@ Scenario: Config file present with valid YAML
When a .oasmock.yaml file exists in the current directory with valid YAML content
Then the CLI uses the values from the file as defaults (unless overridden by environment variables or CLI flags)
-Related spec scenarios: RS.CLI.19, RS.CLI.24
+Related spec scenarios: RS.CLI.19
*/
func TestCLIConfigFilePresent(t *testing.T) {
if testing.Short() {
@@ -700,7 +700,8 @@ func TestCLIConfigFilePresent(t *testing.T) {
// Create config file with custom port
configContent := `port: 19999
verbose: true
-schema: test.yaml`
+schemas:
+ - test.yaml`
require.NoError(t, os.WriteFile(".oasmock.yaml", []byte(configContent), 0644), "failed to write config file")
// Copy test schema to current directory (relative path)
schemaPath := filepath.Join(originalWd, "../../test/_shared/resources/test.yaml")
@@ -891,7 +892,8 @@ func TestCLIPrecedenceFlagOverridesConfig(t *testing.T) {
require.NoError(t, os.Chdir(tmpDir), "failed to change to temp directory")
// Config file sets port 8080
configContent := `port: 8080
-schema: test.yaml`
+schemas:
+ - test.yaml`
require.NoError(t, os.WriteFile(".oasmock.yaml", []byte(configContent), 0644), "failed to write config file")
// Copy test schema
schemaPath := filepath.Join(originalWd, "../../test/_shared/resources/test.yaml")
@@ -951,7 +953,8 @@ func TestCLIPrecedenceEnvOverridesConfig(t *testing.T) {
require.NoError(t, os.Chdir(tmpDir), "failed to change to temp directory")
// Config file sets port 8080
configContent := `port: 8080
-schema: test.yaml`
+schemas:
+ - test.yaml`
require.NoError(t, os.WriteFile(".oasmock.yaml", []byte(configContent), 0644), "failed to write config file")
// Copy test schema
schemaPath := filepath.Join(originalWd, "../../test/_shared/resources/test.yaml")
@@ -1088,51 +1091,6 @@ verbose: true`
_ = cmd.Wait()
}
-/*
-Scenario: Invalid schema configuration (both schema and schemas)
-When a .oasmock.yaml file contains both schema and schemas keys
-Then the CLI reports an error and exits with code 2
-
-Related spec scenarios: RS.CLI.26
-*/
-func TestCLIConfigInvalidBothSchemaAndSchemas(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping integration test in short mode")
- }
- // Build the binary if not present
- if _, err := os.Stat("../../bin/oasmock"); os.IsNotExist(err) {
- t.Skip("binary not found, skipping integration test")
- }
- tmpDir := t.TempDir()
- originalWd, err := os.Getwd()
- require.NoError(t, err, "failed to get working directory")
- binaryPath := filepath.Join(originalWd, "../../bin/oasmock")
- defer os.Chdir(originalWd) //nolint:errcheck
-
- require.NoError(t, os.Chdir(tmpDir), "failed to change to temp directory")
- // Config file with both schema and schemas (invalid)
- configContent := `schema: single.yaml
-schemas:
- - multi.yaml
-port: 8080`
- require.NoError(t, os.WriteFile(".oasmock.yaml", []byte(configContent), 0644), "failed to write config file")
- // Create a dummy schema file
- require.NoError(t, os.WriteFile("single.yaml", []byte("dummy"), 0644), "failed to write dummy schema")
-
- // Run oasmock - should exit with error code 2
- cmd := exec.Command(binaryPath, "mock")
- output, err := cmd.CombinedOutput()
- require.Error(t, err, "command should fail with invalid config")
- // Check exit code (2 = invalid arguments)
- exitErr, ok := err.(*exec.ExitError)
- require.True(t, ok, "error should be ExitError")
- expectedCode := 2 // invalid command-line arguments
- assert.Equal(t, expectedCode, exitErr.ExitCode(), "expected exit code %d for invalid config, got %d", expectedCode, exitErr.ExitCode())
- // Error message should indicate the problem
- outputStr := string(output)
- assert.Contains(t, outputStr, "cannot specify both 'schema' and 'schemas'", "error message missing expected content: %s", outputStr)
-}
-
/*
Scenario: CLI integration test location
When CLI integration tests are written