Skip to content

feat: support creating resources from spec#239

Merged
infiniteregrets merged 13 commits intomainfrom
m/102
Feb 24, 2026
Merged

feat: support creating resources from spec#239
infiniteregrets merged 13 commits intomainfrom
m/102

Conversation

@infiniteregrets
Copy link
Member

@infiniteregrets infiniteregrets commented Feb 24, 2026

credits: sonnet 4.6, codex 5.3

@greptile-apps
Copy link

greptile-apps bot commented Feb 24, 2026

Greptile Summary

Added declarative spec-based resource creation using JSON files with create-or-reconfigure semantics. Implemented via new s2 apply CLI command and S2 Lite --init-file flag.

Key changes:

  • New s2 apply -f spec.json command with dry-run support and schema generation
  • HTTP PUT endpoints for idempotent basin/stream creation or reconfiguration
  • CreateOrReconfigured<T> result type distinguishing creation from reconfiguration
  • S2 Lite startup initialization from spec file via --init-file or S2LITE_INIT_FILE
  • Comprehensive spec validation with duplicate detection and name validation
  • Backend refactoring to support reconfiguration through BasinReconfiguration and StreamReconfiguration types

The implementation follows PUT semantics consistently across both CLI and Lite server paths. Spec files use JSON with humantime durations and kebab-case enums for user-friendliness.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation is clean, well-structured, and follows Rust best practices. The create-or-reconfigure semantics are correctly implemented across all layers (CLI, SDK, Lite backend). Type safety is maintained throughout with proper error handling. The refactoring of backend methods to use reconfiguration types is an improvement. Previous review comments have been addressed regarding partial config semantics.
  • No files require special attention

Important Files Changed

Filename Overview
cli/src/apply.rs New file implementing declarative spec-based basin/stream creation with apply and dry-run functionality
lite/src/init.rs New module with spec types, validation, and backend integration for declarative resource initialization
lite/src/backend/basins.rs Refactored create_basin to accept BasinReconfiguration and properly handle create-or-reconfigure semantics
lite/src/backend/streams.rs Refactored create_stream to accept StreamReconfiguration and properly handle create-or-reconfigure semantics
sdk/src/types.rs Added CreateOrReconfigured enum and input types for new basin/stream create-or-reconfigure operations
sdk/src/api.rs Added PUT methods and API handlers for create-or-reconfigure basin/stream operations

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User: s2 apply -f spec.json] --> B[CLI: apply.rs]
    B --> C{Parse & Validate Spec}
    C -->|Invalid| D[Return Error]
    C -->|Valid| E[SDK: create_or_reconfigure_basin]
    E --> F[API: PUT /v1/basins/:name]
    F --> G{Basin Exists?}
    G -->|No| H[Create with Config]
    G -->|Yes| I[Reconfigure Existing]
    H --> J[Return 201 Created]
    I --> K[Return 200 OK]
    J --> L[CLI: Display Green]
    K --> M[CLI: Display Yellow]
    E --> N[SDK: create_or_reconfigure_stream]
    N --> O[API: PUT /v1/streams/:name]
    O --> P{Stream Exists?}
    P -->|No| Q[Create with Config]
    P -->|Yes| R[Reconfigure Existing]
    Q --> S[Return 201 Created]
    R --> T[Return 200 OK]
    
    U[S2 Lite: --init-file spec.json] --> V[lite/init.rs]
    V --> W[Backend: create_basin]
    W --> X[CreateMode::CreateOrReconfigure]
    V --> Y[Backend: create_stream]
    Y --> Z[CreateMode::CreateOrReconfigure]
Loading

Last reviewed commit: cf69094

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

9 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

lite/src/init.rs Outdated
Comment on lines 167 to 176
impl From<BasinConfigSpec> for BasinConfig {
fn from(s: BasinConfigSpec) -> Self {
BasinConfig {
default_stream_config: s
.default_stream_config
.map(OptionalStreamConfig::from)
.unwrap_or_default(),
create_stream_on_append: s.create_stream_on_append.unwrap_or(false),
create_stream_on_read: s.create_stream_on_read.unwrap_or(false),
}
Copy link

Choose a reason for hiding this comment

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

Partial config silently resets unspecified fields

When CreateMode::CreateOrReconfigure is used (i.e., config is Some), the backend does a full replacement of the BasinConfig. Because this From impl defaults create_stream_on_append and create_stream_on_read to false when not specified in the spec, a user who provides a partial config (e.g., only default_stream_config) will unintentionally reset previously-set create_stream_on_append: true back to false.

This is consistent with PUT semantics, but may surprise users who expect partial-update behavior from a spec file. Consider documenting this full-replacement behavior prominently (e.g., in the --init-file help text or the spec format docs) so users understand that omitted fields revert to defaults on reconfigure.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: lite/src/init.rs
Line: 167-176

Comment:
**Partial config silently resets unspecified fields**

When `CreateMode::CreateOrReconfigure` is used (i.e., `config` is `Some`), the backend does a full replacement of the `BasinConfig`. Because this `From` impl defaults `create_stream_on_append` and `create_stream_on_read` to `false` when not specified in the spec, a user who provides a partial config (e.g., only `default_stream_config`) will unintentionally reset previously-set `create_stream_on_append: true` back to `false`.

This is consistent with PUT semantics, but may surprise users who expect partial-update behavior from a spec file. Consider documenting this full-replacement behavior prominently (e.g., in the `--init-file` help text or the spec format docs) so users understand that omitted fields revert to defaults on reconfigure.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

cli/src/apply.rs Outdated
Comment on lines 172 to 176
let sdk_config = config
.as_ref()
.cloned()
.map(basin_config_to_sdk)
.unwrap_or_default();
Copy link

Choose a reason for hiding this comment

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

Unnecessary reconfigure call when config is None

When config is None, the "already exists" branch at line 184 calls config.map(basin_config_to_reconfig).unwrap_or_default(), resulting in a no-op reconfigure API call. The Lite path avoids this by using CreateMode::CreateOnly when there's no config. Consider short-circuiting here as well to skip the redundant network round-trip:

Suggested change
let sdk_config = config
.as_ref()
.cloned()
.map(basin_config_to_sdk)
.unwrap_or_default();
let sdk_config = config
.as_ref()
.cloned()
.map(basin_config_to_sdk)
.unwrap_or_default();
let input = CreateBasinInput::new(basin.clone()).with_config(sdk_config);
match s2.create_basin(input).await {
Ok(_) => {
eprintln!("{}", format!(" basin {basin}").green().bold());
}
Err(ref e) if is_already_exists(e) => {
if let Some(config) = config {
let reconfig = basin_config_to_reconfig(config);
s2.reconfigure_basin(ReconfigureBasinInput::new(basin.clone(), reconfig))
.await
.map_err(|e| {
miette::miette!("failed to reconfigure basin {:?}: {}", basin.as_ref(), e)
})?;
eprintln!(
"{}",
format!(" basin {basin} (reconfigured)").yellow().bold()
);
} else {
eprintln!(
"{}",
format!(" basin {basin} (already exists)").yellow().bold()
);
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: cli/src/apply.rs
Line: 172-176

Comment:
**Unnecessary reconfigure call when config is None**

When `config` is `None`, the "already exists" branch at line 184 calls `config.map(basin_config_to_reconfig).unwrap_or_default()`, resulting in a no-op reconfigure API call. The Lite path avoids this by using `CreateMode::CreateOnly` when there's no config. Consider short-circuiting here as well to skip the redundant network round-trip:

```suggestion
    let sdk_config = config
        .as_ref()
        .cloned()
        .map(basin_config_to_sdk)
        .unwrap_or_default();

    let input = CreateBasinInput::new(basin.clone()).with_config(sdk_config);
    match s2.create_basin(input).await {
        Ok(_) => {
            eprintln!("{}", format!("  basin {basin}").green().bold());
        }
        Err(ref e) if is_already_exists(e) => {
            if let Some(config) = config {
                let reconfig = basin_config_to_reconfig(config);
                s2.reconfigure_basin(ReconfigureBasinInput::new(basin.clone(), reconfig))
                    .await
                    .map_err(|e| {
                        miette::miette!("failed to reconfigure basin {:?}: {}", basin.as_ref(), e)
                    })?;
                eprintln!(
                    "{}",
                    format!("  basin {basin} (reconfigured)").yellow().bold()
                );
            } else {
                eprintln!(
                    "{}",
                    format!("  basin {basin} (already exists)").yellow().bold()
                );
            }
        }
```

How can I resolve this? If you propose a fix, please make it concise.

cli/src/apply.rs Outdated
Comment on lines 211 to 216
) -> miette::Result<()> {
let sdk_config = config
.as_ref()
.cloned()
.map(stream_config_to_sdk)
.unwrap_or_default();
Copy link

Choose a reason for hiding this comment

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

Same unnecessary reconfigure for streams when config is None

Same pattern as apply_basin — when config is None and the stream already exists, line 225 issues a no-op reconfigure API call. Skipping it when config.is_none() avoids a redundant round-trip and gives the user a more accurate "(already exists)" message instead of "(reconfigured)".

Prompt To Fix With AI
This is a comment left during a code review.
Path: cli/src/apply.rs
Line: 211-216

Comment:
**Same unnecessary reconfigure for streams when config is None**

Same pattern as `apply_basin` — when `config` is `None` and the stream already exists, line 225 issues a no-op reconfigure API call. Skipping it when `config.is_none()` avoids a redundant round-trip and gives the user a more accurate "(already exists)" message instead of "(reconfigured)".

How can I resolve this? If you propose a fix, please make it concise.

@infiniteregrets
Copy link
Member Author

@greptile-apps review

@shikhar shikhar linked an issue Feb 24, 2026 that may be closed by this pull request
Copy link
Member

@quettabit quettabit left a comment

Choose a reason for hiding this comment

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

LGTM!

@infiniteregrets infiniteregrets merged commit 7c58e8d into main Feb 24, 2026
17 checks passed
@infiniteregrets infiniteregrets deleted the m/102 branch February 24, 2026 20:27
@s2-release-plz s2-release-plz bot mentioned this pull request Feb 24, 2026
infiniteregrets pushed a commit that referenced this pull request Feb 25, 2026
## 🤖 New release

* `s2-api`: 0.27.6 -> 0.27.7 (✓ API compatible changes)
* `s2-lite`: 0.29.7 -> 0.29.8 (✓ API compatible changes)
* `s2-sdk`: 0.24.2 -> 0.24.3 (✓ API compatible changes)
* `s2-cli`: 0.29.7 -> 0.29.8

<details><summary><i><b>Changelog</b></i></summary><p>

## `s2-api`

<blockquote>

## [0.27.7] - 2026-02-24

### Features

- Support creating resources from spec
([#239](#239))

### Documentation

- Display default values for `create_stream_on_*` config
([#241](#241))

<!-- generated by git-cliff -->
</blockquote>

## `s2-lite`

<blockquote>

## [0.29.8] - 2026-02-24

### Features

- Support creating resources from spec
([#239](#239))

<!-- generated by git-cliff -->
</blockquote>

## `s2-sdk`

<blockquote>

## [0.24.3] - 2026-02-24

### Features

- Support creating resources from spec
([#239](#239))

<!-- generated by git-cliff -->
</blockquote>

## `s2-cli`

<blockquote>

## [0.29.8] - 2026-02-24

### Features

- Support creating resources from spec
([#239](#239))

<!-- generated by git-cliff -->
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

Co-authored-by: s2-release-plz[bot] <262023388+s2-release-plz[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support creating basins from a JSON spec

2 participants