cosmwasm_schema/
idl.rs

1//! The Cosmwasm IDL (Interface Description Language)
2
3use std::collections::BTreeMap;
4
5use schemars::schema::RootSchema;
6use thiserror::Error;
7
8/// The version of the CosmWasm IDL.
9///
10/// Follows Semantic Versioning 2.0.0: <https://semver.org/>
11// To determine if a change is breaking, assume consumers allow unknown fields and bump accordingly.
12pub const IDL_VERSION: &str = "1.0.0";
13
14/// Rust representation of a contract's API.
15pub struct Api {
16    pub contract_name: String,
17    pub contract_version: String,
18    pub instantiate: Option<RootSchema>,
19    pub execute: Option<RootSchema>,
20    pub query: Option<RootSchema>,
21    pub migrate: Option<RootSchema>,
22    pub sudo: Option<RootSchema>,
23    /// A mapping of query variants to response types
24    pub responses: Option<BTreeMap<String, RootSchema>>,
25}
26
27impl Api {
28    pub fn render(self) -> JsonApi {
29        let mut json_api = JsonApi {
30            contract_name: self.contract_name,
31            contract_version: self.contract_version,
32            idl_version: IDL_VERSION.to_string(),
33            instantiate: self.instantiate,
34            execute: self.execute,
35            query: self.query,
36            migrate: self.migrate,
37            sudo: self.sudo,
38            responses: self.responses,
39        };
40
41        if let Some(instantiate) = &mut json_api.instantiate {
42            if let Some(metadata) = &mut instantiate.schema.metadata {
43                metadata.title = Some("InstantiateMsg".to_string());
44            }
45        }
46        if let Some(execute) = &mut json_api.execute {
47            if let Some(metadata) = &mut execute.schema.metadata {
48                metadata.title = Some("ExecuteMsg".to_string());
49            }
50        }
51        if let Some(query) = &mut json_api.query {
52            if let Some(metadata) = &mut query.schema.metadata {
53                metadata.title = Some("QueryMsg".to_string());
54            }
55        }
56        if let Some(migrate) = &mut json_api.migrate {
57            if let Some(metadata) = &mut migrate.schema.metadata {
58                metadata.title = Some("MigrateMsg".to_string());
59            }
60        }
61        if let Some(sudo) = &mut json_api.sudo {
62            if let Some(metadata) = &mut sudo.schema.metadata {
63                metadata.title = Some("SudoMsg".to_string());
64            }
65        }
66
67        json_api
68    }
69}
70
71/// A JSON representation of a contract's API.
72#[derive(serde::Serialize)]
73pub struct JsonApi {
74    contract_name: String,
75    contract_version: String,
76    idl_version: String,
77    instantiate: Option<RootSchema>,
78    execute: Option<RootSchema>,
79    query: Option<RootSchema>,
80    migrate: Option<RootSchema>,
81    sudo: Option<RootSchema>,
82    responses: Option<BTreeMap<String, RootSchema>>,
83}
84
85impl JsonApi {
86    pub fn to_string(&self) -> Result<String, EncodeError> {
87        serde_json::to_string_pretty(&self).map_err(Into::into)
88    }
89
90    pub fn to_schema_files(&self) -> Result<Vec<(String, String)>, EncodeError> {
91        let mut result = Vec::new();
92
93        if let Some(instantiate) = &self.instantiate {
94            result.push((
95                "instantiate.json".to_string(),
96                serde_json::to_string_pretty(&instantiate)?,
97            ));
98        }
99
100        if let Some(execute) = &self.execute {
101            result.push((
102                "execute.json".to_string(),
103                serde_json::to_string_pretty(&execute)?,
104            ));
105        }
106        if let Some(query) = &self.query {
107            result.push((
108                "query.json".to_string(),
109                serde_json::to_string_pretty(&query)?,
110            ));
111        }
112        if let Some(migrate) = &self.migrate {
113            result.push((
114                "migrate.json".to_string(),
115                serde_json::to_string_pretty(&migrate)?,
116            ));
117        }
118        if let Some(sudo) = &self.sudo {
119            result.push((
120                "sudo.json".to_string(),
121                serde_json::to_string_pretty(&sudo)?,
122            ));
123        }
124        if let Some(responses) = &self.responses {
125            for (name, response) in responses {
126                result.push((
127                    format!("response_to_{name}.json"),
128                    serde_json::to_string_pretty(&response)?,
129                ));
130            }
131        }
132
133        Ok(result)
134    }
135
136    pub fn to_writer(&self, writer: impl std::io::Write) -> Result<(), EncodeError> {
137        serde_json::to_writer_pretty(writer, self).map_err(Into::into)
138    }
139}
140
141#[derive(Error, Debug)]
142pub enum EncodeError {
143    #[error("{0}")]
144    JsonError(#[from] serde_json::Error),
145}
146
147#[cfg(test)]
148mod tests {
149    use crate::schema_for;
150
151    use super::*;
152
153    #[test]
154    fn version_is_semver() {
155        semver::Version::parse(IDL_VERSION).unwrap();
156    }
157
158    #[test]
159    fn to_schema_files_works() {
160        let empty = Api {
161            contract_name: "my_contract".to_string(),
162            contract_version: "1.2.3".to_string(),
163            instantiate: None,
164            execute: None,
165            query: None,
166            migrate: None,
167            sudo: None,
168            responses: None,
169        };
170
171        let files = empty.render().to_schema_files().unwrap();
172        assert_eq!(files, []);
173
174        #[derive(schemars::JsonSchema)]
175        struct TestMsg {}
176
177        let full = Api {
178            contract_name: "my_contract".to_string(),
179            contract_version: "1.2.3".to_string(),
180            instantiate: Some(schema_for!(TestMsg)),
181            execute: Some(schema_for!(TestMsg)),
182            query: Some(schema_for!(TestMsg)),
183            migrate: Some(schema_for!(TestMsg)),
184            sudo: Some(schema_for!(TestMsg)),
185            responses: Some(BTreeMap::from([(
186                "TestMsg".to_string(),
187                schema_for!(TestMsg),
188            )])),
189        };
190
191        let files = full.render().to_schema_files().unwrap();
192        assert_eq!(files.len(), 6);
193        assert_eq!(files[0].0, "instantiate.json");
194        assert_eq!(files[1].0, "execute.json");
195        assert_eq!(files[2].0, "query.json");
196        assert_eq!(files[3].0, "migrate.json");
197        assert_eq!(files[4].0, "sudo.json");
198        assert_eq!(files[5].0, "response_to_TestMsg.json");
199    }
200}