Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
9f421b0
feat(cpp-boost-beast): add OpenAI compliance harness for composed sch…
bold84 Jul 19, 2026
88ee024
fix(phase-0): address local review findings
bold84 Jul 19, 2026
ee72168
fix(phase-0): address strong review findings
bold84 Jul 19, 2026
5aeefac
fix(phase-0): close pin reproducibility and clean up expected-types
bold84 Jul 20, 2026
7fc4b2f
feat(cpp-boost-beast): implement oneOf/anyOf type lowering engine
bold84 Jul 20, 2026
6554228
fix(phase-1): address local review findings
bold84 Jul 20, 2026
26a8a1f
fix(phase-1): strip nested shared_ptr from template args
bold84 Jul 20, 2026
f4059ce
fix(phase-1): address strong review findings
bold84 Jul 20, 2026
6fa7be4
feat(cpp-boost-beast): implement variant-first template generation
bold84 Jul 20, 2026
7ceaf9e
fix(phase-2): address local review findings
bold84 Jul 20, 2026
9804018
fix(phase-2): address strong review findings
bold84 Jul 20, 2026
a350721
fix(phase-2): address remaining strong review findings
bold84 Jul 20, 2026
4065cfe
fix(phase-2): add missing map include for variant sources
bold84 Jul 20, 2026
8dc8060
feat(cpp-boost-beast): implement API integration for variant types
bold84 Jul 20, 2026
b0e4406
fix(phase-3): complete variant JSON helpers and model import fixes
bold84 Jul 20, 2026
251b2e8
fix(phase-3): wire SSE parseEventStream and multipart variant branches
bold84 Jul 20, 2026
3b97fb7
fix(phase-3): address local review
bold84 Jul 20, 2026
bcd4e31
fix(phase-3): address strong review findings
bold84 Jul 20, 2026
0a016f0
fix(phase-3): address strong review findings (WIP checkpoint)
bold84 Jul 20, 2026
06f0711
fix(phase-3): address strong review findings
bold84 Jul 20, 2026
5a7e9c0
fix(phase-3): address strong review findings
bold84 Jul 20, 2026
dd58f09
feat(phase-0): add OAS Gate A composition compliance harness
bold84 Jul 20, 2026
6053616
fix(phase-0): address strong review findings
bold84 Jul 20, 2026
05716b3
fix(phase-0): address strong review findings
bold84 Jul 20, 2026
2f2c05e
fix(phase-1): OAS-first composition lowering rules
bold84 Jul 20, 2026
3c6b6c0
fix(phase-1): strong review remediation — shared_ptr stripping, allOf…
bold84 Jul 20, 2026
6bf0053
fix(phase-1): address strong review findings
bold84 Jul 20, 2026
d30ef4f
fix(phase-1): gate model-source.mustache IsSet with x-cpp-no-is-set
bold84 Jul 20, 2026
84d4c22
fix(phase-2): oneOf exactly-one vs anyOf first-match decode
bold84 Jul 20, 2026
5f9d913
fix(phase-2): address strong review findings
bold84 Jul 20, 2026
db2104b
fix(phase-2): address strong review findings
bold84 Jul 20, 2026
20373f4
fix(phase-3): address strong review findings
bold84 Jul 20, 2026
f4f25a6
fix(phase-4): transitive string collapse and Gate B corpus gate
bold84 Jul 21, 2026
a1a756a
fix(phase-4): omit empty default initializers for corpus compile
bold84 Jul 21, 2026
cc07f95
fix(phase-4): nested model JSON via fromJsonValue not value_to
bold84 Jul 21, 2026
87d8a8f
fix(phase-4): complete OpenAI corpus C++ compile
bold84 Jul 21, 2026
2be0c97
fix(phase-4): fix golden runner namespace and round-trips
bold84 Jul 21, 2026
30b886c
fix(phase-4): discriminator-aware nested union round-trips
bold84 Jul 21, 2026
ef45fd5
fix(phase-4): remove unused discriminatorMatched
bold84 Jul 21, 2026
8af6a46
fix(phase-4): address strong review findings
bold84 Jul 21, 2026
781acbd
feat(phase-5): validation hardening required fields paths discriminator
bold84 Jul 21, 2026
4a7710f
fix(phase-5): address strong review findings
bold84 Jul 21, 2026
c3aa67b
docs(phase-6): variant-first OAS dual-gate and streaming docs
bold84 Jul 21, 2026
27a1ee7
fix(phase-6): address strong review findings
bold84 Jul 21, 2026
5e2f70e
fix(phase-6): align Gate A nullability blurb with implementation
bold84 Jul 21, 2026
37eda81
fix(cpp-boost-beast): harden OAS-first composed-schema fidelity
bold84 Jul 22, 2026
dcbe25c
merge: integrate origin/master into plan/cpp-boost-beast-oneof-anyof-…
bold84 Jul 22, 2026
fe87d1c
chore(cpp-boost-beast): drop OpenAI corpus harness from PR surface
bold84 Jul 22, 2026
a8ad215
chore(cpp-boost-beast): remove accidental OpenAI pin files from branch
bold84 Jul 22, 2026
57008b0
fix(cpp-boost-beast): replace setAccessible with setMappedModels
bold84 Jul 22, 2026
62dcb97
fix(cpp-boost-beast): implement incremental streaming in executeStream
bold84 Jul 22, 2026
476b450
fix(cpp-boost-beast): frame SSE events in executeStream
bold84 Jul 22, 2026
6542604
fix(cpp-boost-beast): reject SSE streaming for schema-less operations
bold84 Jul 22, 2026
95aabfe
fix(cpp-boost-beast): guard dual-content stream method behind resolve…
bold84 Jul 22, 2026
b4d7ef8
fix(cpp-boost-beast): unwrap ModelPropertyStorage for nullable inheri…
bold84 Jul 22, 2026
d18fe52
fix(cpp-boost-beast): preserve branch enum metadata in Phase 1b alias…
bold84 Jul 22, 2026
3734a35
fix(cpp-boost-beast): wrap streaming return type in std::vector when …
bold84 Jul 22, 2026
ff6ef09
fix(cpp-boost-beast): lower nullable $ref schemas and arrays to std::…
bold84 Jul 22, 2026
3dcbbf9
fix(cpp-boost-beast): enforce exactly-one match in oneOf variant resp…
bold84 Jul 22, 2026
6fb4910
chore(cpp-boost-beast): regenerate samples after oneOf variant fix
bold84 Jul 22, 2026
4e0000c
fix(cpp-boost-beast): declare variant/optional serializers before con…
bold84 Jul 22, 2026
8a76940
Qualify discriminator literal encode behavior in cpp-boost-beast README
bold84 Jul 22, 2026
e86b150
fix(cpp-boost-beast): add std::map branch to tryParseBranch for compo…
bold84 Jul 22, 2026
c149e4d
fix(cpp-boost-beast): validate int32_t bounds in variant decode to pr…
bold84 Jul 22, 2026
4575fb8
[cpp-boost-beast-client] Extend oneOf string-enum exclusivity Rule 6 …
bold84 Jul 22, 2026
aeb5f0a
fix(cpp-boost-beast): enforce const property decode invariants
bold84 Jul 22, 2026
0052b74
fix(cpp-boost-beast-client): add missing std::uint8_t branch to tryPa…
bold84 Jul 22, 2026
ad036fd
fix(cpp-boost-beast): refresh x-discriminator-resolved-type after Pha…
bold84 Jul 22, 2026
4de7012
fix(cpp-boost-beast): lower nullable arrays, maps, and inline objects…
bold84 Jul 22, 2026
9bde41f
fix(cpp-boost-beast): restore gate-a.sh compliance harness dropped wi…
bold84 Jul 22, 2026
82eefd0
docs(cpp-boost-beast): align README minimum versions with CMakeLists.txt
bold84 Jul 22, 2026
3d347c4
fix: correct README.mustache toJsonValue/fromJsonValue function names
bold84 Jul 22, 2026
059c373
fix(cpp-boost-beast): address reviewer regressions
bold84 Jul 22, 2026
aeef0bd
fix(cpp-boost-beast): resolve nullable model regressions
bold84 Jul 23, 2026
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
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,13 @@ samples/client/jetbrains/adyen/checkout71/http/client/Apis/http-client.private.e
\?/

# AI agent caches
.junie
.junie

# openai-cpp-sdk build artifacts (Gate A / B harnesses)
/generated-oas-client/
/generated-oas-client-negative/
openai-cpp-sdk/generated-oas/
openai-cpp-sdk/generated-oas-negative/
openai-cpp-sdk/generated-openai/
openai-cpp-sdk/build-generated/
openai-cpp-sdk/build-master-verification/
14 changes: 9 additions & 5 deletions docs/generators/cpp-boost-beast-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|int32_t|#include <cstdint>|
|int64_t|#include <cstdint>|
|std::map|#include <map>|
|std::monostate|#include <variant>|
|std::nullptr_t|#include <cstddef>|
|std::optional|#include <optional>|
|std::shared_ptr|#include <memory>|
|std::string|#include <string>|
|std::variant|#include <variant>|
|std::vector|#include <vector>|


Expand Down Expand Up @@ -239,11 +243,11 @@ These options may be applied as additional-properties (cli) or configOptions (pl
| ---- | --------- | ---------- |
|Simple|✓|OAS2,OAS3
|Composite|✓|OAS2,OAS3
|Polymorphism||OAS2,OAS3
|Union||OAS3
|allOf||OAS2,OAS3
|anyOf||OAS3
|oneOf||OAS3
|Polymorphism||OAS2,OAS3
|Union||OAS3
|allOf||OAS2,OAS3
|anyOf||OAS3
|oneOf||OAS3
|not|✗|OAS3

### Security Feature
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ endif ()

add_library(${PROJECT_NAME} SHARED)

set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 11)
set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 17)
set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD_REQUIRED ON)
set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_EXTENSIONS OFF)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# {{packageName}}

{{#appDescriptionWithNewLines}}{{{.}}}{{/appDescriptionWithNewLines}}

Generated by [OpenAPI Generator](https://openapi-generator.tech).
{{#generatorVersion}}Generator version: {{generatorVersion}}{{/generatorVersion}}

- API version: {{appVersion}}
{{^hideGenerationTimestamp}}- Build date: {{generatedDate}}{{/hideGenerationTimestamp}}
- API namespace: `{{{apiPackage}}}`
- Model namespace: `{{{modelPackage}}}`

---

## Architecture

This client uses **variant-first composed-schema lowering** — a deliberate departure
from the default OpenAPI Generator composed-schema handling.

### Breaking model strategy

The generator treats composed schemas (`oneOf`, `anyOf`, `allOf`) as first-class
C++ types rather than empty object shells. This is a **breaking change**: generated
model headers, source, and API surfaces are fundamentally different from the previous
output.

**The break is intentional and acceptable.** No known downstream project depends on
the prior composed-model output for `cpp-boost-beast-client`. The generator stability
label stays STABLE because the generated output targets a specific C++ client; the
prior output for composed schemas was non-functional (empty structs that could not
hold union data).

| Before | After |
|--------|-------|
| Empty struct `AnyOfStringArray {}` | `std::variant<std::string, std::vector<ItemStruct>>` |
| Composed → `{}` on wire | Correct encode/decode via `toJsonValue`/`fromJsonValue` |
| Polymorphism disabled | `oneOf`/`anyOf`/`allOf` fully supported |

### Ordered composition lowering

The generator applies the following rules in order to every composed schema:

1. **Merge compatible `allOf` object composition** through generated inheritance and property aggregation. Conflict → error.
2. **Lower `anyOf`/`oneOf` null unions** with a single non-null branch to `std::optional<T>` (and OAS 3.0 `nullable`).
3. **Collapse `anyOf` of only string and string-enums to `std::string`.**
Does **not** apply this collapse to `oneOf` unless exclusivity is proven.
4. **Collapse single remaining non-null branch** to that branch type.
5. **Deduplicate identical C++ branch types** inside variants.
6. **Else emit `std::variant<Branches...>`**.
7. **If still untyped/open**, emit `boost::json::value`.

### OpenAPI → C++ type mapping

| OpenAPI shape | C++ type |
|---------------|----------|
| `string` | `std::string` |
| `integer`/`int32` | `std::int32_t` |
| `integer`/`int64` | `std::int64_t` |
| `number` | `double` |
| `boolean` | `bool` |
| `anyOf: [T, null]` / nullable `T` | `std::optional<T>` |
| `oneOf` / non-null `anyOf` unions | `std::variant<...>` |
| `allOf` object | Inherited/merged value class |
| array | `std::vector<T>` |
| map / `additionalProperties` | `std::map<std::string, T>` |
| free-form JSON | `boost::json::value` |
| object struct | Value class |
| cyclic edge | `std::shared_ptr<T>` |
| `enum` string | `enum class` + string conversion |
| Discriminator literal | Fixed on encode when branch schema declares a `const` (OAS 3.1) or single-value `enum`; otherwise not injected |

---

## Nullability: omit vs null policy

### Current scope (Phase 2)

The generated encode logic uses a **two-state model** for optional fields:

| Field kind | Encode behavior |
|-----------|----------------|
| Non-required, disengaged optional / isSet=false | **Key omitted** from JSON output |
| Non-required, engaged optional / isSet=true | Key written with converted value |
| Required, nullable, `std::nullopt` | Key written as JSON `null` |
| Required, nullable, has value | Key written with converted value |

For non-required fields, the current implementation omits the key when the
optional is disengaged. The `JsonValueConverter<std::optional<T>>::toJsonValue`
helper unwraps the inner value; the outer `has_value()` check at the call site
prevents unreachable `null` output for absent optionals.

### Deferred: full tri-state

A full **tri-state model** (field absent / field present with JSON null / field
present with JSON value) is **not yet implemented**. When the schema allows null,
the current implementation cannot distinguish "field was sent as JSON null" from
"field was absent" for non-required properties — both result in the key being
omitted on encode and `std::nullopt` on decode.

A future phase may add a separate `null_state` flag per nullable field to
preserve the absent-vs-null distinction on the wire, matching OAS 3.1's
semantic model where a missing key and a present `null` value are distinct.

---

## Compliance testing

Composition lowering is covered by unit tests and OAS fixture specs under:

```text
modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-client/
modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-client/oas-compliance/
```

Run:

```sh
./mvnw -pl modules/openapi-generator \
-Dtest=CppBoostBeastClientCodegenTest,CppBoostBeastClientApiCodegenTest test
```

---

## Media-type-driven streaming (not `stream` flags)

OpenAPI models streaming via the response `content` map, not via boolean parameters:

```yaml
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Response'
text/event-stream:
schema:
$ref: '#/components/schemas/ResponseStreamEvent'
```

The generator **discovers** media types and schemas from the `content` map. This
includes same-status multi-content operations where one media type is
`text/event-stream` and another is `application/json`.

### Required behavior

1. **Client selection** — The caller chooses representation via the `Accept` header.
Non-streaming path sets `Accept: application/json`; streaming path sets
`Accept: text/event-stream`.
2. **SSE decoding** — When `text/event-stream` is selected, the generator emits
an event-stream parser that decodes each SSE `data:` line using the
schema from the `text/event-stream` media type entry (often a `oneOf` union).
3. **Transport** — Streaming operations use `HttpClient::executeStream`, which
reads the response body incrementally and delivers complete SSE events
(WHATWG-framed `data` payloads) via callback as they arrive.

### Explicitly forbidden as core OAS behavior

- Hard-coding parameter name `stream` or operationId patterns from any single API.
- Assuming event type names (`ResponseStreamEvent`, etc.) in the engine.
- Treating dual media-type content as vendor-specific.

Parameter properties named `stream` in the OpenAPI document are ordinary schema
parameters — not OAS keywords. The generator does not special-case them.

### Incremental SSE transport

`HttpClient::executeStream` performs incremental `async_read_some` reads and
frames Server-Sent Events per the WHATWG specification. Each callback receives
one complete event's data payload (multi-line `data:` fields joined by LF).
Generated streaming operations parse each event as it is framed. Incomplete
events at EOF are discarded.

---

## Non-standard format mappings

The generator documents non-standard format mappings as explicit type-mapping
entries, not as core OAS vocabulary.

| Format | OAS status | C++ type | Notes |
|--------|-----------|----------|-------|
| `unixtime` | **Non-standard** (not in OAS format registry) | `std::int64_t` | Optional generator convenience in `getSchemaType` when `format: unixtime` appears. Not core OAS vocabulary. |

This mapping is a **documented non-standard convenience** for APIs that encode
Unix-epoch seconds as integers. It does **not** imply that `unixtime` is a
standard OAS format. Specs that omit `format: unixtime` are unaffected. Any API
using a different time representation (ISO 8601 strings, millisecond epochs,
etc.) must define its own schema.

---

## Large breaking PR is acceptable

This work ships as a single, large, explicitly-breaking pull request that:

1. Eliminates the prior empty-shell composed-model output entirely.
2. Replaces it with `std::variant` / `std::optional` / collapsed types.
3. Updates every mustache template for models and API operations.
4. Adds OAS composition fixtures and unit tests for lowering rules.
5. Adds media-type-driven streaming support.

**No attempt is made to preserve backward compatibility with prior generated
output.** The prior output was non-functional for composed schemas, and no
known downstream project depends on it. The breaking change is confined to
a single generator (`cpp-boost-beast-client`) and does not affect any other
OpenAPI Generator language target.

Reviewers should evaluate the PR against:
- **OAS compliance** — do the generated types and encode/decode faithfully
represent the OpenAPI specification's composition rules?
- **Fixture coverage** — does every lowering rule have dedicated unit/fixture
coverage with green tests?

---

## Requirements

- C++17 compiler (GCC 7+, Clang 8+, MSVC 2019+)
- CMake 3.14+
- Boost 1.75+ (Beast, Asio, JSON)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The documented Boost 1.75 minimum is too low for generated nullable alias models: their alias source relies on Boost.JSON's std::optional value conversion, which was added in Boost 1.81. Either raise the generated client's minimum to Boost 1.81 or route alias serialization through a generator-provided optional converter.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/cpp-boost-beast-client/README.mustache, line 220:

<comment>The documented Boost 1.75 minimum is too low for generated nullable alias models: their alias source relies on Boost.JSON's `std::optional` value conversion, which was added in Boost 1.81. Either raise the generated client's minimum to Boost 1.81 or route alias serialization through a generator-provided optional converter.</comment>

<file context>
@@ -0,0 +1,235 @@
+
+- C++17 compiler (GCC 7+, Clang 8+, MSVC 2019+)
+- CMake 3.14+
+- Boost 1.75+ (Beast, Asio, JSON)
+- OpenSSL 1.1.0+
+
</file context>
Suggested change
- Boost 1.75+ (Beast, Asio, JSON)
- Boost 1.81+ (Beast, Asio, JSON)

- OpenSSL 1.1.0+

## Build

```sh
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build .
```

## Installation

```sh
cmake --install .
```
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,27 @@ public:
/// <remarks>
/// {{notes}}
/// </remarks>
virtual {{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}void{{/returnType}}
virtual {{#vendorExtensions.x-codegen-streaming-response}}{{#vendorExtensions.x-codegen-stream-element-type}}std::vector<{{{.}}}>{{/vendorExtensions.x-codegen-stream-element-type}}{{^vendorExtensions.x-codegen-stream-element-type}}{{#returnType}}std::vector<{{{.}}}>{{/returnType}}{{^returnType}}void{{/returnType}}{{/vendorExtensions.x-codegen-stream-element-type}}{{/vendorExtensions.x-codegen-streaming-response}}{{^vendorExtensions.x-codegen-streaming-response}}{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}void{{/returnType}}{{/vendorExtensions.x-codegen-streaming-response}}
{{#operationId}}{{{.}}}{{/operationId}}{{^operationId}}{{httpMethod}}_{{vendorExtensions.x-codegen-resource-name}}{{/operationId}}(
{{#allParams}}const {{#vendorExtensions.x-codegen-is-optional-query-parameter}}boost::optional<{{/vendorExtensions.x-codegen-is-optional-query-parameter}}{{{dataType}}}{{#vendorExtensions.x-codegen-is-optional-query-parameter}}>{{/vendorExtensions.x-codegen-is-optional-query-parameter}}& {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}});

{{#vendorExtensions.x-codegen-dual-content}}
/// <summary>
/// {{summary}} (streaming)
/// </summary>
virtual std::vector<{{#vendorExtensions.x-codegen-dual-stream-element-type}}{{{.}}}{{/vendorExtensions.x-codegen-dual-stream-element-type}}{{^vendorExtensions.x-codegen-dual-stream-element-type}}{{{vendorExtensions.x-codegen-dual-stream-return-type}}}{{/vendorExtensions.x-codegen-dual-stream-element-type}}>
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
{{#operationId}}{{{.}}}{{/operationId}}Stream(
{{#allParams}}const {{#vendorExtensions.x-codegen-is-optional-query-parameter}}boost::optional<{{/vendorExtensions.x-codegen-is-optional-query-parameter}}{{{dataType}}}{{#vendorExtensions.x-codegen-is-optional-query-parameter}}>{{/vendorExtensions.x-codegen-is-optional-query-parameter}}& {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}});

{{/vendorExtensions.x-codegen-dual-content}}
{{#vendorExtensions.x-codegen-other-methods}}
/// <summary>
/// {{summary}}
/// </summary>
/// <remarks>
/// {{notes}}
/// </remarks>
virtual {{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}void{{/returnType}}
virtual {{#vendorExtensions.x-codegen-streaming-response}}{{#vendorExtensions.x-codegen-stream-element-type}}std::vector<{{{.}}}>{{/vendorExtensions.x-codegen-stream-element-type}}{{^vendorExtensions.x-codegen-stream-element-type}}{{#returnType}}std::vector<{{{.}}}>{{/returnType}}{{^returnType}}void{{/returnType}}{{/vendorExtensions.x-codegen-stream-element-type}}{{/vendorExtensions.x-codegen-streaming-response}}{{^vendorExtensions.x-codegen-streaming-response}}{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}void{{/returnType}}{{/vendorExtensions.x-codegen-streaming-response}}
{{#operationId}}{{{.}}}{{/operationId}}{{^operationId}}{{httpMethod}}_{{vendorExtensions.x-codegen-resource-name}}{{/operationId}}(
{{#allParams}}const {{#vendorExtensions.x-codegen-is-optional-query-parameter}}boost::optional<{{/vendorExtensions.x-codegen-is-optional-query-parameter}}{{{dataType}}}{{#vendorExtensions.x-codegen-is-optional-query-parameter}}>{{/vendorExtensions.x-codegen-is-optional-query-parameter}}& {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}});
{{/vendorExtensions.x-codegen-other-methods}}
Expand Down
Loading
Loading