Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 11 additions & 23 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,33 @@
**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).

## 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:
```
Expand All @@ -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)
92 changes: 34 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
![spec coverage](https://img.shields.io/badge/spec_coverage-100%25-brightgreen)
![Go](https://img.shields.io/badge/go-1.23+-00ADD8)

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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
MIT
19 changes: 0 additions & 19 deletions cmd/oasmock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
48 changes: 8 additions & 40 deletions cmd/oasmock/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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"))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
})
Expand Down
Loading
Loading