Skip to content

Commit a2d447a

Browse files
committed
feat: generate and release schema.json
1 parent 143d513 commit a2d447a

File tree

12 files changed

+1024
-6
lines changed

12 files changed

+1024
-6
lines changed

.github/workflows/release.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363
- name: 🛠️ Run Build
6464
run: cargo build -p pglt_cli --release --target ${{ matrix.config.target }}
6565

66-
# windows is a special snowflake to, it saves binaries as .exe
66+
# windows is a special snowflake too, it saves binaries as .exe
6767
- name: 👦 Name the Binary
6868
if: matrix.config.os == 'windows-latest'
6969
run: |
@@ -124,7 +124,9 @@ jobs:
124124
token: ${{ secrets.GITHUB_TOKEN }}
125125
body: ${{ steps.create_changelog.outputs.content }}
126126
tag_name: ${{ steps.create_changelog.outputs.version }}
127-
files: pglt_*
127+
files: |
128+
pglt_*
129+
docs/schemas/latest/schema.json
128130
fail_on_unmatched_files: true
129131
draft: true
130132

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pglt_configuration/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ version = "0.0.0"
1212

1313

1414
[dependencies]
15-
biome_deserialize = { workspace = true }
15+
biome_deserialize = { workspace = true, features = ["schema"] }
1616
biome_deserialize_macros = { workspace = true }
1717
bpaf = { workspace = true }
18+
indexmap = { workspace = true }
1819
pglt_analyse = { workspace = true }
1920
pglt_analyser = { workspace = true }
2021
pglt_console = { workspace = true }
@@ -30,4 +31,4 @@ toml = { workspace = true }
3031
doctest = false
3132

3233
[features]
33-
schema = ["dep:schemars"]
34+
schema = ["dep:schemars", "schemars/indexmap"]

crates/pglt_configuration/src/database.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
55
/// The configuration of the database connection.
66
#[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize)]
77
#[partial(derive(Bpaf, Clone, Eq, PartialEq, Merge))]
8+
#[partial(cfg_attr(feature = "schema", derive(schemars::JsonSchema)))]
89
#[partial(serde(rename_all = "snake_case", default, deny_unknown_fields))]
910
pub struct DatabaseConfiguration {
1011
/// The host of the database.

crates/pglt_configuration/src/files.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub const DEFAULT_FILE_SIZE_LIMIT: NonZeroU64 =
1313
/// The configuration of the filesystem
1414
#[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize)]
1515
#[partial(derive(Bpaf, Clone, Eq, PartialEq, Merge))]
16+
#[partial(cfg_attr(feature = "schema", derive(schemars::JsonSchema)))]
1617
#[partial(serde(rename_all = "snake_case", default, deny_unknown_fields))]
1718
pub struct FilesConfiguration {
1819
/// The maximum allowed size for source code files in bytes. Files above

crates/pglt_configuration/src/migrations.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
66
#[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize, Default)]
77
#[partial(derive(Bpaf, Clone, Eq, PartialEq, Merge))]
88
#[partial(serde(rename_all = "snake_case", default, deny_unknown_fields))]
9+
#[partial(cfg_attr(feature = "schema", derive(schemars::JsonSchema)))]
910
pub struct MigrationsConfiguration {
1011
/// The directory where the migration files are stored
1112
#[partial(bpaf(long("migrations-dir")))]

docs/codegen/Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ regex = { workspace = true }
1616
toml = { workspace = true }
1717
anyhow = { workspace = true }
1818
bpaf = { workspace = true, features = ["docgen"] }
19+
schemars = { workspace = true }
20+
serde = { workspace = true }
21+
serde_json = { workspace = true }
22+
pulldown-cmark = "0.12.2"
1923

20-
pglt_configuration = { workspace = true }
24+
pglt_configuration = { workspace = true, features = ["schema"] }
2125
pglt_flags = { workspace = true }
2226
pglt_cli = { workspace = true }
2327
pglt_analyse = { workspace = true }
@@ -28,5 +32,4 @@ pglt_workspace = { workspace = true }
2832
pglt_statement_splitter = { workspace = true }
2933
pglt_console = { workspace = true }
3034
biome_string_case = { workspace = true }
31-
pulldown-cmark = "0.12.2"
3235

docs/codegen/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ pub mod env_variables;
44
pub mod rules_docs;
55
pub mod rules_index;
66
pub mod rules_sources;
7+
pub mod schema;
78

89
mod utils;

docs/codegen/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use docs_codegen::env_variables::generate_env_variables;
77
use docs_codegen::rules_docs::generate_rules_docs;
88
use docs_codegen::rules_index::generate_rules_index;
99
use docs_codegen::rules_sources::generate_rule_sources;
10+
use docs_codegen::schema::generate_schema;
1011

1112
fn docs_root() -> PathBuf {
1213
let dir =
@@ -23,6 +24,7 @@ fn main() -> anyhow::Result<()> {
2324
generate_rules_docs(&docs_root)?;
2425
generate_rules_index(&docs_root)?;
2526
generate_rule_sources(&docs_root)?;
27+
generate_schema(&docs_root)?;
2628

2729
Ok(())
2830
}

docs/codegen/src/schema.rs

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
use pglt_configuration::{PartialConfiguration, VERSION};
2+
use schemars::{
3+
schema::{RootSchema, Schema, SchemaObject},
4+
schema_for,
5+
};
6+
use serde_json::to_string_pretty;
7+
use std::{fs, path::Path};
8+
9+
/// Generates the lint rules index.
10+
///
11+
/// * `docs_dir`: Path to the docs directory.
12+
pub fn generate_schema(docs_dir: &Path) -> anyhow::Result<()> {
13+
let schemas_dir = docs_dir.join("schemas");
14+
let latest_schema_dir = schemas_dir.join("latest");
15+
let latest_schema_path = latest_schema_dir.join("schema.json");
16+
17+
let version_schema_dir = schemas_dir.join(VERSION);
18+
let version_schema_path = version_schema_dir.join("schema.json");
19+
20+
if !latest_schema_dir.exists() {
21+
fs::create_dir_all(&latest_schema_dir)?;
22+
}
23+
24+
if !version_schema_dir.exists() {
25+
fs::create_dir_all(&version_schema_dir)?;
26+
}
27+
28+
let schema_content = get_configuration_schema_content()?;
29+
30+
fs::write(latest_schema_path, &schema_content)?;
31+
fs::write(version_schema_path, &schema_content)?;
32+
33+
Ok(())
34+
}
35+
36+
// TODO: publish the schemas in the release assets and update config init to use the latest schema
37+
38+
/// Get the content of the configuration schema
39+
pub(crate) fn get_configuration_schema_content() -> anyhow::Result<String> {
40+
let schema = rename_partial_references_in_schema(schema_for!(PartialConfiguration));
41+
42+
Ok(to_string_pretty(&schema)?)
43+
}
44+
45+
/// Strips all "Partial" prefixes from type names in the schema.
46+
///
47+
/// We do this to avoid leaking our `Partial` derive macro to the outside world,
48+
/// since it should be just an implementation detail.
49+
fn rename_partial_references_in_schema(mut schema: RootSchema) -> RootSchema {
50+
if let Some(meta) = schema.schema.metadata.as_mut() {
51+
if let Some(title) = meta.title.as_ref() {
52+
if let Some(stripped) = title.strip_prefix("Partial") {
53+
meta.title = Some(stripped.to_string());
54+
} else if title == "RuleWithOptions_for_Null" {
55+
meta.title = Some("RuleWithNoOptions".to_string());
56+
} else if title == "RuleWithFixOptions_for_Null" {
57+
meta.title = Some("RuleWithFixNoOptions".to_string());
58+
} else if title == "RuleConfiguration_for_Null" {
59+
meta.title = Some("RuleConfiguration".to_string());
60+
} else if title == "RuleFixConfiguration_for_Null" {
61+
meta.title = Some("RuleFixConfiguration".to_string());
62+
} else if let Some(stripped) = title.strip_prefix("RuleWithOptions_for_") {
63+
meta.title = Some(format!("RuleWith{stripped}"));
64+
} else if let Some(stripped) = title.strip_prefix("RuleWithFixOptions_for_") {
65+
meta.title = Some(format!("RuleWith{stripped}"));
66+
} else if let Some(stripped) = title
67+
.strip_prefix("RuleConfiguration_for_")
68+
.map(|x| x.strip_suffix("Options").unwrap_or(x))
69+
{
70+
meta.title = Some(format!("{stripped}Configuration"));
71+
} else if let Some(stripped) = title
72+
.strip_prefix("RuleFixConfiguration_for_")
73+
.map(|x| x.strip_suffix("Options").unwrap_or(x))
74+
{
75+
meta.title = Some(format!("{stripped}Configuration"));
76+
}
77+
}
78+
}
79+
80+
rename_partial_references_in_schema_object(&mut schema.schema);
81+
82+
schema.definitions = schema
83+
.definitions
84+
.into_iter()
85+
.map(|(mut key, mut schema)| {
86+
if let Some(stripped) = key.strip_prefix("Partial") {
87+
key = stripped.to_string();
88+
} else if key == "RuleWithOptions_for_Null" || key == "RuleWithFixOptions_for_Null" {
89+
key = if key == "RuleWithOptions_for_Null" {
90+
"RuleWithNoOptions".to_string()
91+
} else {
92+
"RuleWithFixNoOptions".to_string()
93+
};
94+
if let Schema::Object(schema_object) = &mut schema {
95+
if let Some(object) = &mut schema_object.object {
96+
object.required.remove("options");
97+
object.properties.remove("options");
98+
}
99+
}
100+
} else if key == "RuleConfiguration_for_Null" {
101+
key = "RuleConfiguration".to_string();
102+
} else if key == "RuleFixConfiguration_for_Null" {
103+
key = "RuleFixConfiguration".to_string();
104+
} else if let Some(stripped) = key.strip_prefix("RuleWithOptions_for_") {
105+
key = format!("RuleWith{stripped}");
106+
} else if let Some(stripped) = key.strip_prefix("RuleWithFixOptions_for_") {
107+
key = format!("RuleWith{stripped}");
108+
} else if let Some(stripped) = key
109+
.strip_prefix("RuleConfiguration_for_")
110+
.map(|x| x.strip_suffix("Options").unwrap_or(x))
111+
{
112+
key = format!("{stripped}Configuration");
113+
} else if let Some(stripped) = key
114+
.strip_prefix("RuleFixConfiguration_for_")
115+
.map(|x| x.strip_suffix("Options").unwrap_or(x))
116+
{
117+
key = format!("{stripped}Configuration");
118+
}
119+
120+
if let Schema::Object(object) = &mut schema {
121+
rename_partial_references_in_schema_object(object);
122+
}
123+
124+
(key, schema)
125+
})
126+
.collect();
127+
128+
schema
129+
}
130+
131+
fn rename_partial_references_in_schema_object(object: &mut SchemaObject) {
132+
if let Some(object) = &mut object.object {
133+
for prop_schema in object.properties.values_mut() {
134+
if let Schema::Object(object) = prop_schema {
135+
rename_partial_references_in_schema_object(object);
136+
}
137+
}
138+
}
139+
140+
if let Some(reference) = &mut object.reference {
141+
if let Some(stripped) = reference.strip_prefix("#/definitions/Partial") {
142+
*reference = format!("#/definitions/{stripped}");
143+
} else if reference == "#/definitions/RuleWithOptions_for_Null" {
144+
*reference = "#/definitions/RuleWithNoOptions".to_string();
145+
} else if reference == "#/definitions/RuleWithFixOptions_for_Null" {
146+
*reference = "#/definitions/RuleWithFixNoOptions".to_string();
147+
} else if reference == "#/definitions/RuleConfiguration_for_Null" {
148+
*reference = "#/definitions/RuleConfiguration".to_string();
149+
} else if reference == "#/definitions/RuleFixConfiguration_for_Null" {
150+
*reference = "#/definitions/RuleFixConfiguration".to_string();
151+
} else if let Some(stripped) = reference.strip_prefix("#/definitions/RuleWithOptions_for_")
152+
{
153+
*reference = format!("#/definitions/RuleWith{stripped}");
154+
} else if let Some(stripped) =
155+
reference.strip_prefix("#/definitions/RuleWithFixOptions_for_")
156+
{
157+
*reference = format!("#/definitions/RuleWith{stripped}");
158+
} else if let Some(stripped) = reference
159+
.strip_prefix("#/definitions/RuleConfiguration_for_")
160+
.map(|x| x.strip_suffix("Options").unwrap_or(x))
161+
{
162+
*reference = format!("#/definitions/{stripped}Configuration");
163+
} else if let Some(stripped) = reference
164+
.strip_prefix("#/definitions/RuleFixConfiguration_for_")
165+
.map(|x| x.strip_suffix("Options").unwrap_or(x))
166+
{
167+
*reference = format!("#/definitions/{stripped}Configuration");
168+
}
169+
}
170+
171+
if let Some(subschemas) = &mut object.subschemas {
172+
rename_partial_references_in_optional_schema_vec(&mut subschemas.all_of);
173+
rename_partial_references_in_optional_schema_vec(&mut subschemas.any_of);
174+
rename_partial_references_in_optional_schema_vec(&mut subschemas.one_of);
175+
176+
rename_partial_references_in_optional_schema_box(&mut subschemas.not);
177+
rename_partial_references_in_optional_schema_box(&mut subschemas.if_schema);
178+
rename_partial_references_in_optional_schema_box(&mut subschemas.then_schema);
179+
rename_partial_references_in_optional_schema_box(&mut subschemas.else_schema);
180+
}
181+
}
182+
183+
fn rename_partial_references_in_optional_schema_box(schema: &mut Option<Box<Schema>>) {
184+
if let Some(schema) = schema {
185+
if let Schema::Object(object) = schema.as_mut() {
186+
rename_partial_references_in_schema_object(object);
187+
}
188+
}
189+
}
190+
191+
fn rename_partial_references_in_optional_schema_vec(schemas: &mut Option<Vec<Schema>>) {
192+
if let Some(schemas) = schemas {
193+
rename_partial_references_in_schema_slice(schemas);
194+
}
195+
}
196+
197+
fn rename_partial_references_in_schema_slice(schemas: &mut [Schema]) {
198+
for schema in schemas {
199+
if let Schema::Object(object) = schema {
200+
rename_partial_references_in_schema_object(object);
201+
}
202+
}
203+
}

0 commit comments

Comments
 (0)