feat: generate ticket browser contract from ServerApi
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use server_api::repository_openapi_typescript::{
|
||||
INPUT_PATH, TICKET_BROWSER_OUTPUT_PATH, generate_ticket_browser_typescript,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
if let Err(error) = run() {
|
||||
eprintln!("{error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let mut check = false;
|
||||
for argument in std::env::args().skip(1) {
|
||||
match argument.as_str() {
|
||||
"--check" => check = true,
|
||||
"--help" | "-h" => {
|
||||
println!(
|
||||
"Generate the bounded Ticket Browser API TypeScript artifact.\n\n\
|
||||
Usage: cargo run -q -p server-api --example generate_ticket_api_types -- [--check]"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
other => return Err(format!("unknown argument `{other}`")),
|
||||
}
|
||||
}
|
||||
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let input_path = root.join(INPUT_PATH);
|
||||
let output_path = root.join(TICKET_BROWSER_OUTPUT_PATH);
|
||||
let input = fs::read_to_string(&input_path)
|
||||
.map_err(|error| format!("failed to read {}: {error}", input_path.display()))?;
|
||||
let generated = generate_ticket_browser_typescript(&input)
|
||||
.map_err(|error| format!("failed to generate Ticket Browser API types: {error}"))?;
|
||||
|
||||
if check {
|
||||
let checked_in = fs::read_to_string(&output_path)
|
||||
.map_err(|error| format!("failed to read {}: {error}", output_path.display()))?;
|
||||
if checked_in != generated {
|
||||
return Err(format!(
|
||||
"{} is stale; regenerate it without --check",
|
||||
output_path.display()
|
||||
));
|
||||
}
|
||||
println!("{} is current", output_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let temporary_path = output_path.with_extension("ts.tmp");
|
||||
fs::write(&temporary_path, generated)
|
||||
.map_err(|error| format!("failed to write {}: {error}", temporary_path.display()))?;
|
||||
fs::rename(&temporary_path, &output_path)
|
||||
.map_err(|error| format!("failed to replace {}: {error}", output_path.display()))?;
|
||||
println!("generated {}", output_path.display());
|
||||
Ok(())
|
||||
}
|
||||
@@ -28,6 +28,10 @@ pub const COMPANION_GENERATOR_VERSION: &str = "1";
|
||||
pub const COMPANION_GENERATOR_OPTIONS: &str =
|
||||
"companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output";
|
||||
pub const COMPANION_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/companion-api.ts";
|
||||
pub const TICKET_BROWSER_GENERATOR_NAME: &str = "yoi-ticket-browser-openapi-typescript";
|
||||
pub const TICKET_BROWSER_GENERATOR_VERSION: &str = "1";
|
||||
pub const TICKET_BROWSER_GENERATOR_OPTIONS: &str = "ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output";
|
||||
pub const TICKET_BROWSER_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/ticket-api.ts";
|
||||
|
||||
const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
|
||||
const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991.0;
|
||||
@@ -203,6 +207,19 @@ pub fn generate_companion_typescript(input: &str) -> Result<String, GenerationEr
|
||||
)
|
||||
}
|
||||
|
||||
pub fn generate_ticket_browser_typescript(input: &str) -> Result<String, GenerationError> {
|
||||
generate_domain_typescript(
|
||||
input,
|
||||
"Ticket Browser",
|
||||
validate_ticket_browser_operations,
|
||||
TICKET_BROWSER_GENERATOR_NAME,
|
||||
TICKET_BROWSER_GENERATOR_VERSION,
|
||||
TICKET_BROWSER_GENERATOR_OPTIONS,
|
||||
TICKET_BROWSER_OUTPUT_PATH,
|
||||
"generate_ticket_api_types",
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn generate_domain_typescript(
|
||||
input: &str,
|
||||
@@ -402,6 +419,112 @@ fn validate_companion_operations(
|
||||
validate_operation_contracts(paths, &operations)
|
||||
}
|
||||
|
||||
fn validate_ticket_browser_operations(
|
||||
paths: &Map<String, Value>,
|
||||
) -> Result<BTreeSet<String>, GenerationError> {
|
||||
let operations = [
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/tickets",
|
||||
method: "get",
|
||||
operation_id: "ticket_list",
|
||||
request: None,
|
||||
successes: &[("200", "TicketListResponse")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/tickets/{id}",
|
||||
method: "get",
|
||||
operation_id: "ticket_get",
|
||||
request: None,
|
||||
successes: &[("200", "TicketDetail")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/tickets/{id}",
|
||||
method: "patch",
|
||||
operation_id: "ticket_edit",
|
||||
request: Some("BrowserEditTicketRequest"),
|
||||
successes: &[("200", "TicketDetail")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/tickets/{id}/assignments/{role}",
|
||||
method: "put",
|
||||
operation_id: "ticket_assignment_set",
|
||||
request: Some("SetTicketRoleAssignmentRequest"),
|
||||
successes: &[("200", "TicketRoleAssignmentMutationResponse")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/tickets/{id}/implementation-cancellations",
|
||||
method: "post",
|
||||
operation_id: "ticket_implementation_cancel",
|
||||
request: Some("CancelTicketImplementationRequest"),
|
||||
successes: &[("200", "TicketDetail")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/tickets/{id}/state",
|
||||
method: "post",
|
||||
operation_id: "ticket_state_transition",
|
||||
request: Some("BrowserTransitionTicketStateRequest"),
|
||||
successes: &[("200", "TicketDetail")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/tickets/{id}/ready",
|
||||
method: "post",
|
||||
operation_id: "ticket_ready",
|
||||
request: Some("TicketMarkReadyRequest"),
|
||||
successes: &[("200", "TicketDetail")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/tickets/{id}/events",
|
||||
method: "post",
|
||||
operation_id: "ticket_event_append",
|
||||
request: Some("BrowserAppendTicketEventRequest"),
|
||||
successes: &[("200", "TicketDetail")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/tickets/{id}/queue",
|
||||
method: "post",
|
||||
operation_id: "ticket_queue",
|
||||
request: Some("BrowserQueueTicketRequest"),
|
||||
successes: &[("200", "TicketQueueOutcome")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/tickets/{id}/close",
|
||||
method: "post",
|
||||
operation_id: "ticket_close",
|
||||
request: Some("BrowserCloseTicketRequest"),
|
||||
successes: &[("200", "TicketDetail")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/objectives",
|
||||
method: "get",
|
||||
operation_id: "objective_list",
|
||||
request: None,
|
||||
successes: &[("200", "ObjectiveListResponse")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/objectives/{objective_id}",
|
||||
method: "get",
|
||||
operation_id: "objective_get",
|
||||
request: None,
|
||||
successes: &[("200", "ObjectiveDetail")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/merge-requests",
|
||||
method: "get",
|
||||
operation_id: "merge_request_list",
|
||||
request: None,
|
||||
successes: &[("200", "MergeRequestListResponse")],
|
||||
},
|
||||
OperationContract {
|
||||
path: "/api/w/{workspace_id}/merge-requests/{merge_request_id}",
|
||||
method: "get",
|
||||
operation_id: "merge_request_show",
|
||||
request: None,
|
||||
successes: &[("200", "MergeRequestDetailResponse")],
|
||||
},
|
||||
];
|
||||
validate_operation_contracts(paths, &operations)
|
||||
}
|
||||
|
||||
fn validate_repository_operations(
|
||||
paths: &Map<String, Value>,
|
||||
) -> Result<BTreeSet<String>, GenerationError> {
|
||||
@@ -1484,12 +1607,25 @@ fn render_typed_schema(
|
||||
None => (MIN_SAFE_INTEGER, MAX_SAFE_INTEGER),
|
||||
Some(_) => unreachable!("integer format validated above"),
|
||||
};
|
||||
if minimum != Some(expected_minimum) || maximum != Some(expected_maximum) {
|
||||
let minimum = minimum.unwrap_or(expected_minimum);
|
||||
let maximum = maximum.unwrap_or(expected_maximum);
|
||||
if minimum.fract() != 0.0
|
||||
|| maximum.fract() != 0.0
|
||||
|| minimum < expected_minimum
|
||||
|| maximum > expected_maximum
|
||||
|| minimum > maximum
|
||||
{
|
||||
return Err(GenerationError::invalid(format!(
|
||||
"{context} integer range does not match its JavaScript safe-number mapping"
|
||||
)));
|
||||
}
|
||||
Ok("number".to_owned())
|
||||
if minimum == expected_minimum && maximum == expected_maximum {
|
||||
Ok("number".to_owned())
|
||||
} else {
|
||||
Ok(format!(
|
||||
"number & {{ readonly \"__yoiIntegerRange\": \"{minimum:.0}..{maximum:.0}\"; }}"
|
||||
))
|
||||
}
|
||||
}
|
||||
"array" => {
|
||||
ensure_schema_keywords(schema, &["items", "maxItems", "minItems", "type"], context)?;
|
||||
@@ -1537,10 +1673,13 @@ fn render_typed_schema(
|
||||
}
|
||||
|
||||
fn render_object(schema: &Map<String, Value>, context: &str) -> Result<String, GenerationError> {
|
||||
let properties = schema
|
||||
.get("properties")
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| GenerationError::invalid(format!("{context} object has no properties")))?;
|
||||
let empty_properties = Map::new();
|
||||
let properties = match schema.get("properties") {
|
||||
Some(value) => value.as_object().ok_or_else(|| {
|
||||
GenerationError::invalid(format!("{context} object properties must be an object"))
|
||||
})?,
|
||||
None => &empty_properties,
|
||||
};
|
||||
let required = match schema.get("required") {
|
||||
Some(Value::Array(required)) => required
|
||||
.iter()
|
||||
@@ -1564,13 +1703,23 @@ fn render_object(schema: &Map<String, Value>, context: &str) -> Result<String, G
|
||||
"{context} requires missing property `{missing}`"
|
||||
)));
|
||||
}
|
||||
if schema
|
||||
.get("additionalProperties")
|
||||
.is_some_and(|value| value != &Value::Bool(false))
|
||||
{
|
||||
return Err(GenerationError::invalid(format!(
|
||||
"{context} contains unsupported additionalProperties"
|
||||
)));
|
||||
match schema.get("additionalProperties") {
|
||||
Some(Value::Bool(false)) | None => {}
|
||||
Some(additional) if properties.is_empty() && required.is_empty() => {
|
||||
return Ok(format!(
|
||||
"{{ [key: string]: {}; }}",
|
||||
render_schema(additional, context)?
|
||||
));
|
||||
}
|
||||
Some(_) => {
|
||||
return Err(GenerationError::invalid(format!(
|
||||
"{context} contains unsupported additionalProperties"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if properties.is_empty() {
|
||||
return Ok("{ [key: string]: never; }".to_owned());
|
||||
}
|
||||
|
||||
let mut fields = Vec::with_capacity(properties.len());
|
||||
@@ -1649,6 +1798,42 @@ mod tests {
|
||||
include_str!("../../../web/workspace/src/lib/generated/repository-api.ts");
|
||||
const RUNTIME_TYPESCRIPT: &str =
|
||||
include_str!("../../../web/workspace/src/lib/generated/runtime-api.ts");
|
||||
const TICKET_BROWSER_TYPESCRIPT: &str =
|
||||
include_str!("../../../web/workspace/src/lib/generated/ticket-api.ts");
|
||||
|
||||
#[test]
|
||||
fn checked_in_ticket_browser_typescript_is_current() {
|
||||
assert_eq!(
|
||||
generate_ticket_browser_typescript(OPENAPI).unwrap(),
|
||||
TICKET_BROWSER_TYPESCRIPT,
|
||||
"regenerate with `cargo run -q -p server-api --example generate_ticket_api_types`",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_browser_generator_validates_operation_ownership_and_schema_closure() {
|
||||
let output = generate_ticket_browser_typescript(OPENAPI).unwrap();
|
||||
assert!(output.contains(&format!(
|
||||
"@generated by {TICKET_BROWSER_GENERATOR_NAME}@{TICKET_BROWSER_GENERATOR_VERSION}"
|
||||
)));
|
||||
assert!(output.contains("export type TicketListResponse ="));
|
||||
assert!(
|
||||
output.contains("limit: number & { readonly \"__yoiIntegerRange\": \"0..1000\"; }")
|
||||
);
|
||||
assert!(
|
||||
output.contains("export type BrowserQueueTicketRequest = { [key: string]: never; };")
|
||||
);
|
||||
assert!(output.contains("attributes: { [key: string]: string; }"));
|
||||
assert!(output.contains("export type ObjectiveDetail ="));
|
||||
assert!(output.contains("export type MergeRequestDetailResponse ="));
|
||||
assert!(!output.contains("export type RuntimeListResponse ="));
|
||||
|
||||
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
|
||||
document["paths"]["/api/w/{workspace_id}/tickets"]["get"]["operationId"] =
|
||||
serde_json::json!("manual_ticket_list");
|
||||
let error = generate_ticket_browser_typescript(&document.to_string()).unwrap_err();
|
||||
assert!(error.to_string().contains("ticket_list"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checked_in_runtime_typescript_is_current() {
|
||||
@@ -1711,7 +1896,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generator_rejects_lossy_safe_integer_and_union_changes() {
|
||||
fn generator_rejects_lossy_safe_integers_and_preserves_bounded_ranges() {
|
||||
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
|
||||
document["components"]["schemas"]["RepositorySummary"]["properties"]["source_revision"]["maximum"] =
|
||||
serde_json::json!(9_007_199_254_740_992_u64);
|
||||
@@ -1727,8 +1912,10 @@ mod tests {
|
||||
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
|
||||
document["components"]["schemas"]["RepositorySummary"]["properties"]["source_revision"]["maximum"] =
|
||||
serde_json::json!(9_007_199_254_740_990_u64);
|
||||
let error = generate_repository_typescript(&document.to_string()).unwrap_err();
|
||||
assert!(error.to_string().contains("safe-number"));
|
||||
let output = generate_repository_typescript(&document.to_string()).unwrap();
|
||||
assert!(output.contains(
|
||||
"source_revision: number & { readonly \"__yoiIntegerRange\": \"0..9007199254740990\"; }"
|
||||
));
|
||||
|
||||
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
|
||||
document["components"]["schemas"]["RepositorySummary"]["properties"]["source_revision"]["format"] =
|
||||
|
||||
@@ -10,10 +10,6 @@ autobins = false
|
||||
name = "yoi-server"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["typescript"]
|
||||
typescript = ["dep:ts-rs", "server-api/typescript"]
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
axum = { workspace = true, features = ["ws"] }
|
||||
@@ -47,7 +43,7 @@ worker-runtime.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
ts-rs = { version = "12.0.1", optional = true }
|
||||
ts-rs = "12.0.1"
|
||||
url.workspace = true
|
||||
uuid = { workspace = true, features = ["v7"] }
|
||||
zeroize.workspace = true
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
print!("{}", yoi_workspace_server::ticket_api_typescript());
|
||||
}
|
||||
@@ -16,8 +16,6 @@ pub mod observation;
|
||||
pub mod profile_settings;
|
||||
pub mod prompt_settings;
|
||||
pub mod records;
|
||||
#[cfg(feature = "typescript")]
|
||||
pub use records::ticket_api_typescript;
|
||||
pub mod repositories;
|
||||
pub mod repository_access;
|
||||
pub mod repository_source;
|
||||
|
||||
@@ -22,14 +22,12 @@ pub struct TicketSummaryPage {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct InvalidProjectRecord {
|
||||
pub label: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketSummary {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
@@ -54,7 +52,6 @@ pub struct TicketListPageRequest {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketListResponse {
|
||||
pub workspace_id: String,
|
||||
pub limit: usize,
|
||||
@@ -65,7 +62,6 @@ pub struct TicketListResponse {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketDetail {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
@@ -102,7 +98,6 @@ pub struct TicketDetail {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketEventDetail {
|
||||
pub sequence: usize,
|
||||
pub event_ref: String,
|
||||
@@ -121,7 +116,6 @@ pub struct TicketEventDetail {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRelation {
|
||||
pub ticket_id: String,
|
||||
pub kind: String,
|
||||
@@ -134,7 +128,6 @@ pub struct TicketRelation {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct DerivedTicketRelation {
|
||||
pub source_ticket: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -147,7 +140,6 @@ pub struct DerivedTicketRelation {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRelationBlocker {
|
||||
pub blocking_ticket: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -159,7 +151,6 @@ pub struct TicketRelationBlocker {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRelationNotice {
|
||||
pub related_ticket: String,
|
||||
pub kind: String,
|
||||
@@ -167,7 +158,6 @@ pub struct TicketRelationNotice {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRelationView {
|
||||
pub outgoing: Vec<TicketRelation>,
|
||||
pub incoming: Vec<DerivedTicketRelation>,
|
||||
@@ -230,7 +220,6 @@ impl From<ticket::TicketRelationView> for TicketRelationView {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct ObjectiveLinkSummary {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
@@ -239,7 +228,6 @@ pub struct ObjectiveLinkSummary {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketEvidenceEvent {
|
||||
pub event_ref: String,
|
||||
pub sequence: usize,
|
||||
@@ -250,7 +238,6 @@ pub struct TicketEvidenceEvent {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketAssignmentSummary {
|
||||
pub assignment_id: String,
|
||||
pub runtime_id: String,
|
||||
@@ -260,7 +247,6 @@ pub struct TicketAssignmentSummary {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketRoleAssignmentSummary {
|
||||
pub assignment_id: String,
|
||||
pub role: String,
|
||||
@@ -271,8 +257,6 @@ pub struct TicketRoleAssignmentSummary {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "typescript", ts(tag = "kind", rename_all = "snake_case"))]
|
||||
pub enum TicketAssignmentPrincipalSummary {
|
||||
User {
|
||||
account_id: String,
|
||||
@@ -287,7 +271,6 @@ pub enum TicketAssignmentPrincipalSummary {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketActionEligibility {
|
||||
pub can_assign_orchestrator: bool,
|
||||
pub can_unassign_orchestrator: bool,
|
||||
@@ -298,7 +281,6 @@ pub struct TicketActionEligibility {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketMergeRequestSummary {
|
||||
pub merge_request_id: String,
|
||||
pub repository_key: String,
|
||||
@@ -315,14 +297,12 @@ pub struct TicketMergeRequestSummary {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct MergeRequestRefDiagnostic {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct MergeRequestListItem {
|
||||
pub summary: TicketMergeRequestSummary,
|
||||
pub ticket_ids: Vec<String>,
|
||||
@@ -332,14 +312,12 @@ pub struct MergeRequestListItem {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct MergeRequestListResponse {
|
||||
pub items: Vec<MergeRequestListItem>,
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketEvidenceSummary {
|
||||
pub has_merge_request: bool,
|
||||
pub has_current_subject_ref: bool,
|
||||
@@ -354,7 +332,6 @@ pub struct TicketEvidenceSummary {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketQueryRequest {
|
||||
pub query: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -377,7 +354,6 @@ pub struct TicketQueryRequest {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketQueryItem {
|
||||
pub id: String,
|
||||
pub resource_key: String,
|
||||
@@ -393,7 +369,6 @@ pub struct TicketQueryItem {
|
||||
pub snippet: Option<String>,
|
||||
pub matching_event: Option<TicketEvidenceEvent>,
|
||||
pub linked_objective_ids: Vec<String>,
|
||||
#[ts(skip)]
|
||||
pub linked_objective_keys: Vec<String>,
|
||||
pub relation_count: usize,
|
||||
pub blocker_count: usize,
|
||||
@@ -401,12 +376,10 @@ pub struct TicketQueryItem {
|
||||
pub unresolved_review_count: usize,
|
||||
pub evidence: TicketEvidenceSummary,
|
||||
pub merge_request: Option<TicketMergeRequestSummary>,
|
||||
#[ts(skip)]
|
||||
pub current_coder: Option<TicketAssignmentSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketQueryResponse {
|
||||
pub items: Vec<TicketQueryItem>,
|
||||
pub page: QueryPage,
|
||||
@@ -414,7 +387,6 @@ pub struct TicketQueryResponse {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct TicketShowRequest {
|
||||
pub event_limit: Option<usize>,
|
||||
pub event_cursor: Option<String>,
|
||||
@@ -461,78 +433,6 @@ pub struct ObjectiveShowRequest {
|
||||
pub event_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "typescript")]
|
||||
pub fn ticket_api_typescript() -> String {
|
||||
use ts_rs::TS;
|
||||
|
||||
let config = ts_rs::Config::default();
|
||||
let declarations = [
|
||||
InvalidProjectRecord::decl(&config),
|
||||
TicketSummary::decl(&config),
|
||||
TicketListResponse::decl(&config),
|
||||
QueryPage::decl(&config),
|
||||
TicketEventDetail::decl(&config),
|
||||
ObjectiveLinkSummary::decl(&config),
|
||||
TicketEvidenceEvent::decl(&config),
|
||||
TicketAssignmentSummary::decl(&config),
|
||||
TicketRoleAssignmentSummary::decl(&config),
|
||||
TicketAssignmentPrincipalSummary::decl(&config),
|
||||
TicketActionEligibility::decl(&config),
|
||||
TicketMergeRequestSummary::decl(&config),
|
||||
MergeRequestRefDiagnostic::decl(&config),
|
||||
MergeRequestListItem::decl(&config),
|
||||
MergeRequestListResponse::decl(&config),
|
||||
TicketEvidenceSummary::decl(&config),
|
||||
TicketQueryRequest::decl(&config),
|
||||
TicketQueryItem::decl(&config),
|
||||
TicketQueryResponse::decl(&config),
|
||||
TicketShowRequest::decl(&config),
|
||||
TicketRelation::decl(&config),
|
||||
DerivedTicketRelation::decl(&config),
|
||||
TicketRelationBlocker::decl(&config),
|
||||
TicketRelationNotice::decl(&config),
|
||||
TicketRelationView::decl(&config),
|
||||
TicketDetail::decl(&config),
|
||||
];
|
||||
format!(
|
||||
"// Generated from yoi-workspace-server. Do not edit by hand.\n// Regenerate: cargo run -q -p yoi-workspace-server --features typescript --example generate_ticket_api_types > web/workspace/src/lib/generated/ticket-api.ts\n\n{}\n",
|
||||
declarations
|
||||
.into_iter()
|
||||
.map(|declaration| format!("export {declaration}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n")
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "typescript"))]
|
||||
mod typescript_tests {
|
||||
#[test]
|
||||
fn generated_ticket_api_contract_is_current() {
|
||||
let expected = super::ticket_api_typescript();
|
||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../web/workspace/src/lib/generated/ticket-api.ts");
|
||||
let actual = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
|
||||
assert_eq!(
|
||||
normalize(&actual),
|
||||
normalize(&expected),
|
||||
"regenerate Ticket API TypeScript types with `cargo run -q -p yoi-workspace-server --features typescript --example generate_ticket_api_types > web/workspace/src/lib/generated/ticket-api.ts` and format the generated file",
|
||||
);
|
||||
}
|
||||
|
||||
fn normalize(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.filter_map(|character| match character {
|
||||
character if character.is_whitespace() => None,
|
||||
',' => Some(';'),
|
||||
character => Some(character),
|
||||
})
|
||||
.collect::<String>()
|
||||
.replace(";}", "}")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn summarize_body(body: &str) -> String {
|
||||
let summary = body
|
||||
.lines()
|
||||
|
||||
Reference in New Issue
Block a user