Skip to content

Commit bbd9e69

Browse files
committed
Add CI workflows, rustfmt, clippy, and cargo-deny configs
CI workflow (ci.yml): - Format check (cargo fmt --check) - Clippy with -D warnings - Tests on ubuntu + macos - cargo-deny for license/advisory/ban checks - Swatinem/rust-cache with save-if for PRs - Concurrency control (cancel in-progress on new push) Security workflow (security.yml): - Weekly advisory scan + on Cargo.toml changes Also fixes all clippy warnings: - Row::is_empty added - match → if let in column_map - map_or → is_some_and in planner - Doc comment formatting in pagination module
1 parent 9818557 commit bbd9e69

22 files changed

Lines changed: 525 additions & 294 deletions

File tree

.github/workflows/ci.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
concurrency:
9+
group: ${{ github.workflow }}-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
env:
13+
CARGO_TERM_COLOR: always
14+
15+
jobs:
16+
fmt:
17+
name: Format
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: dtolnay/rust-toolchain@stable
22+
with:
23+
components: rustfmt
24+
- run: cargo fmt --all -- --check
25+
26+
clippy:
27+
name: Clippy
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: actions/checkout@v4
31+
- uses: dtolnay/rust-toolchain@stable
32+
with:
33+
components: clippy
34+
- uses: Swatinem/rust-cache@v2
35+
with:
36+
save-if: ${{ github.ref == 'refs/heads/main' }}
37+
- run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
38+
39+
test:
40+
name: Test (${{ matrix.os }})
41+
runs-on: ${{ matrix.os }}
42+
strategy:
43+
fail-fast: false
44+
matrix:
45+
os: [ubuntu-latest, macos-latest]
46+
steps:
47+
- uses: actions/checkout@v4
48+
- uses: dtolnay/rust-toolchain@stable
49+
- uses: Swatinem/rust-cache@v2
50+
with:
51+
shared-key: test-${{ matrix.os }}
52+
save-if: ${{ github.ref == 'refs/heads/main' }}
53+
- run: cargo test --workspace --all-features --locked
54+
55+
deny:
56+
name: Deny
57+
runs-on: ubuntu-latest
58+
steps:
59+
- uses: actions/checkout@v4
60+
- uses: EmbarkStudios/cargo-deny-action@v2

.github/workflows/security.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Security audit
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * 1'
6+
push:
7+
paths:
8+
- '**/Cargo.toml'
9+
- '**/Cargo.lock'
10+
11+
jobs:
12+
audit:
13+
name: Advisories
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: EmbarkStudios/cargo-deny-action@v2
18+
with:
19+
command: check advisories

clippy.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
msrv = "1.85"
2+
cognitive-complexity-threshold = 25
3+
too-many-arguments-threshold = 7
4+
too-many-lines-threshold = 100
5+
allow-unwrap-in-tests = true
6+
allow-expect-in-tests = true

crates/sqlize-core/examples/github_query.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,16 @@ async fn main() {
4949
match execute(&plan, &auth, &client).await {
5050
Ok(result) => {
5151
let json = result_set_to_json(&result);
52-
let toon = result_set_to_toon(&result).unwrap_or_else(|e| format!("TOON error: {e}"));
52+
let toon =
53+
result_set_to_toon(&result).unwrap_or_else(|e| format!("TOON error: {e}"));
5354

5455
println!("--- JSON ({} bytes) ---", json.len());
5556
println!("{json}");
56-
println!("\n--- TOON ({} bytes, {:.0}% smaller) ---", toon.len(), (1.0 - toon.len() as f64 / json.len() as f64) * 100.0);
57+
println!(
58+
"\n--- TOON ({} bytes, {:.0}% smaller) ---",
59+
toon.len(),
60+
(1.0 - toon.len() as f64 / json.len() as f64) * 100.0
61+
);
5762
println!("{toon}");
5863
}
5964
Err(e) => {

crates/sqlize-core/src/catalog/ddl.rs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,13 @@ pub fn table_ddl(table: &VirtualTable) -> String {
1717
let required: Vec<_> = table.required_params().collect();
1818
if !required.is_empty() {
1919
let names: Vec<_> = required.iter().map(|c| c.name.as_str()).collect();
20-
writeln!(
21-
out,
22-
"-- Required WHERE clause: {}",
23-
names.join(" AND ")
24-
)
25-
.unwrap();
20+
writeln!(out, "-- Required WHERE clause: {}", names.join(" AND ")).unwrap();
2621
}
2722

2823
writeln!(out, "CREATE TABLE {} (", table.name).unwrap();
2924

3025
for (i, col) in table.columns.iter().enumerate() {
31-
let trailing_comma = if i + 1 < table.columns.len() {
32-
","
33-
} else {
34-
""
35-
};
26+
let trailing_comma = if i + 1 < table.columns.len() { "," } else { "" };
3627
let nullable = if col.nullable { "" } else { " NOT NULL" };
3728

3829
let origin_tag = match col.role {

crates/sqlize-core/src/catalog/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ impl Catalog {
2828
map.insert(table.name.clone(), table);
2929
}
3030
if map.is_empty() {
31-
tracing::warn!("catalog is empty — no tables were generated from the spec (check your tag filter)");
31+
tracing::warn!(
32+
"catalog is empty — no tables were generated from the spec (check your tag filter)"
33+
);
3234
}
3335
Ok(Self { tables: map })
3436
}
@@ -50,5 +52,4 @@ impl Catalog {
5052
self.get(name)
5153
.ok_or_else(|| Error::TableNotFound(name.clone()))
5254
}
53-
5455
}

crates/sqlize-core/src/catalog/types.rs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,18 @@ impl fmt::Display for TableName {
4747
pub struct ApiParamName(String);
4848

4949
impl ApiParamName {
50-
pub fn new(s: impl Into<String>) -> Self { Self(s.into()) }
51-
pub fn as_str(&self) -> &str { &self.0 }
50+
pub fn new(s: impl Into<String>) -> Self {
51+
Self(s.into())
52+
}
53+
pub fn as_str(&self) -> &str {
54+
&self.0
55+
}
5256
}
5357

5458
impl fmt::Display for ApiParamName {
55-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) }
59+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60+
f.write_str(&self.0)
61+
}
5662
}
5763

5864
/// A validated, non-empty column name containing only `[a-z0-9_]`.
@@ -121,11 +127,7 @@ impl PathTemplate {
121127
pub fn placeholders(&self) -> Vec<&str> {
122128
self.0
123129
.split('/')
124-
.filter_map(|segment| {
125-
segment
126-
.strip_prefix('{')
127-
.and_then(|s| s.strip_suffix('}'))
128-
})
130+
.filter_map(|segment| segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')))
129131
.collect()
130132
}
131133

@@ -204,7 +206,10 @@ impl ColumnRole {
204206

205207
/// Whether a WHERE `=` filter on this column can be pushed to the API.
206208
pub fn is_pushable(&self) -> bool {
207-
matches!(self, Self::PathParam | Self::QueryParam | Self::QueryParamAndResponse)
209+
matches!(
210+
self,
211+
Self::PathParam | Self::QueryParam | Self::QueryParamAndResponse
212+
)
208213
}
209214

210215
/// Whether this column appears in query results.
@@ -234,7 +239,10 @@ impl Column {
234239
/// The key to use when sending this column's value as an API parameter.
235240
/// Falls back to the SQL column name when no explicit API name is set.
236241
pub fn api_param_key(&self) -> &str {
237-
self.api_name.as_ref().map(|n| n.as_str()).unwrap_or(self.name.as_str())
242+
self.api_name
243+
.as_ref()
244+
.map(|n| n.as_str())
245+
.unwrap_or(self.name.as_str())
238246
}
239247
}
240248

@@ -371,7 +379,10 @@ pub fn truncate_str(s: &str, max_chars: usize) -> String {
371379
if first_line.chars().count() <= max_chars {
372380
first_line.to_owned()
373381
} else {
374-
let truncated: String = first_line.chars().take(max_chars.saturating_sub(3)).collect();
382+
let truncated: String = first_line
383+
.chars()
384+
.take(max_chars.saturating_sub(3))
385+
.collect();
375386
format!("{truncated}...")
376387
}
377388
}
@@ -421,6 +432,10 @@ impl Row {
421432
self.0.len()
422433
}
423434

435+
pub fn is_empty(&self) -> bool {
436+
self.0.is_empty()
437+
}
438+
424439
pub fn values(&self) -> &[Scalar] {
425440
&self.0
426441
}

crates/sqlize-core/src/error.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,16 @@ pub enum Error {
1919
TableNotFound(TableName),
2020

2121
#[error("column {column} not found in table {table}")]
22-
ColumnNotFound { table: TableName, column: ColumnName },
22+
ColumnNotFound {
23+
table: TableName,
24+
column: ColumnName,
25+
},
2326

2427
#[error("missing required parameter {column} for table {table}")]
25-
MissingRequiredParam { table: TableName, column: ColumnName },
28+
MissingRequiredParam {
29+
table: TableName,
30+
column: ColumnName,
31+
},
2632

2733
#[error("duplicate table name: {0}")]
2834
DuplicateTable(TableName),
@@ -52,7 +58,11 @@ pub enum Error {
5258

5359
// ---- Execution ----
5460
#[error("API returned {status}: {body}")]
55-
ApiError { status: u16, url: String, body: String },
61+
ApiError {
62+
status: u16,
63+
url: String,
64+
body: String,
65+
},
5666

5767
#[error("failed to resolve URL: missing path parameters")]
5868
UnresolvedUrl,

crates/sqlize-core/src/exec/mod.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,14 @@ pub struct AuthConfig {
2121
}
2222

2323
/// Execute a query plan against live APIs.
24-
pub async fn execute(
25-
plan: &QueryPlan,
26-
auth: &AuthConfig,
27-
client: &Client,
28-
) -> Result<ResultSet> {
24+
pub async fn execute(plan: &QueryPlan, auth: &AuthConfig, client: &Client) -> Result<ResultSet> {
2925
let fetch_limit = plan.post.limit.map(|l| {
3026
let offset = plan.post.offset.unwrap_or(0);
3127
l + offset
3228
});
3329

3430
let mut result = match &plan.source {
35-
PlanSource::ApiCall(call) => {
36-
execute_api_call(call, client, auth, fetch_limit).await?
37-
}
31+
PlanSource::ApiCall(call) => execute_api_call(call, client, auth, fetch_limit).await?,
3832
};
3933

4034
postprocess::apply(&plan.post, &mut result);
@@ -96,14 +90,18 @@ async fn execute_api_call(
9690
is_first_page = false;
9791
}
9892

99-
Ok(ResultSet { columns: all_columns, rows: all_rows })
93+
Ok(ResultSet {
94+
columns: all_columns,
95+
rows: all_rows,
96+
})
10097
}
10198

10299
fn resolve_url(call: &ApiCall) -> Result<String> {
103100
call.endpoint
104101
.url(|placeholder| {
105102
// Find the column whose API name matches this URL placeholder
106-
call.columns.iter()
103+
call.columns
104+
.iter()
107105
.find(|c| c.api_param_key() == placeholder)
108106
.and_then(|c| {
109107
let key = ApiParamName::new(c.api_param_key());
@@ -151,15 +149,18 @@ async fn fetch_page(
151149

152150
if is_first_page {
153151
// Extract query params (non-path pushable params) and stringify for HTTP
154-
let query_params: Vec<(String, String)> = call.columns.iter()
152+
let query_params: Vec<(String, String)> = call
153+
.columns
154+
.iter()
155155
.filter(|c| c.role.is_pushable() && !c.role.is_required())
156156
.filter_map(|c| {
157157
let key = ApiParamName::new(c.api_param_key());
158158
let val = call.params.get(&key)?;
159159
Some((key.as_str().to_owned(), val.to_string()))
160160
})
161161
.collect();
162-
let query_refs: Vec<(&str, &str)> = query_params.iter()
162+
let query_refs: Vec<(&str, &str)> = query_params
163+
.iter()
163164
.map(|(k, v)| (k.as_str(), v.as_str()))
164165
.collect();
165166
request = request.query(&query_refs);
@@ -208,4 +209,3 @@ fn unwrap_response<'a>(
208209
None => body,
209210
}
210211
}
211-

0 commit comments

Comments
 (0)