1use std::{collections::BTreeSet, env, path::PathBuf};
7
8pub use casper_contract_schema;
9use casper_contract_schema::{
10 Access, Argument, CallMethod, ContractSchema, CustomType, Entrypoint, EnumVariant, Event,
11 NamedCLType, StructMember, UserError
12};
13
14use convert_case::{Boundary, Case, Casing};
15
16use odra_core::args::EntrypointArgument;
17
18const CCSV: u8 = 1;
19
20mod custom_type;
21mod ty;
22
23pub use ty::NamedCLTyped;
24
25pub trait SchemaEntrypoints {
27 fn schema_entrypoints() -> Vec<Entrypoint>;
29}
30
31pub trait SchemaEvents {
33 fn schema_events() -> Vec<Event> {
35 vec![]
36 }
37
38 fn custom_types() -> Vec<Option<CustomType>> {
43 vec![]
44 }
45}
46
47pub trait SchemaCustomTypes {
49 fn schema_types() -> Vec<Option<CustomType>> {
51 vec![]
52 }
53}
54
55pub trait SchemaErrors {
57 fn schema_errors() -> Vec<UserError> {
59 vec![]
60 }
61}
62
63pub trait SchemaCustomElement {}
65
66impl<T: SchemaCustomElement> SchemaErrors for T {}
67impl<T: SchemaCustomElement> SchemaEvents for T {}
68
69pub fn argument<T: NamedCLTyped + EntrypointArgument>(name: &str) -> Argument {
71 if T::is_required() {
72 Argument::new(name, "", <T as NamedCLTyped>::ty())
73 } else {
74 Argument::new_opt(name, "", <T as NamedCLTyped>::ty())
75 }
76}
77
78pub fn entry_point<T: NamedCLTyped>(
80 name: &str,
81 description: &str,
82 is_mutable: bool,
83 arguments: Vec<Argument>
84) -> Entrypoint {
85 Entrypoint {
86 name: name.into(),
87 description: Some(description.to_string()),
88 is_mutable,
89 arguments,
90 return_ty: T::ty().into(),
91 is_contract_context: true,
92 access: Access::Public
93 }
94}
95
96pub fn struct_member<T: NamedCLTyped>(name: &str) -> StructMember {
98 StructMember {
99 name: name.to_string(),
100 description: None,
101 ty: T::ty().into()
102 }
103}
104
105pub fn enum_typed_variant<T: NamedCLTyped>(name: &str, discriminant: u16) -> EnumVariant {
107 EnumVariant {
108 name: name.to_string(),
109 description: None,
110 discriminant,
111 ty: T::ty().into()
112 }
113}
114
115pub fn enum_variant(name: &str, discriminant: u16) -> EnumVariant {
117 enum_typed_variant::<()>(name, discriminant)
118}
119
120pub fn enum_custom_type_variant(name: &str, discriminant: u16, custom_type: &str) -> EnumVariant {
122 EnumVariant {
123 name: name.to_string(),
124 description: None,
125 discriminant,
126 ty: NamedCLType::Custom(custom_type.into()).into()
127 }
128}
129
130pub fn custom_struct(name: &str, members: Vec<StructMember>) -> CustomType {
132 CustomType::Struct {
133 name: name.into(),
134 description: None,
135 members
136 }
137}
138
139pub fn custom_enum(name: &str, variants: Vec<EnumVariant>) -> CustomType {
141 CustomType::Enum {
142 name: name.into(),
143 description: None,
144 variants
145 }
146}
147
148pub fn event(name: &str) -> Event {
150 Event {
151 name: name.into(),
152 ty: name.into()
153 }
154}
155
156pub fn error(name: &str, description: &str, discriminant: u16) -> UserError {
158 UserError {
159 name: name.into(),
160 description: Some(description.into()),
161 discriminant
162 }
163}
164
165pub fn schema<T: SchemaEntrypoints + SchemaEvents + SchemaCustomTypes + SchemaErrors>(
170 module_name: &str,
171 contract_name: &str,
172 contract_version: &str,
173 authors: Vec<String>,
174 repository: &str,
175 homepage: &str
176) -> ContractSchema {
177 let entry_points = T::schema_entrypoints();
178 let events = T::schema_events();
179 let errors = T::schema_errors();
180 let types = BTreeSet::from_iter(T::schema_types())
181 .into_iter()
182 .flatten()
183 .collect();
184
185 let init_ep = entry_points.iter().find(|e| e.name == "init");
186
187 let init_args = init_ep.map(|e| e.arguments.clone()).unwrap_or_default();
188
189 let init_description = init_ep.and_then(|e| e.description.clone());
190
191 let entry_points = entry_points
192 .into_iter()
193 .filter(|e| e.name != "init")
194 .collect();
195
196 let wasm_file_name = format!("{}.wasm", module_name);
197
198 let repository = match repository {
199 "" => None,
200 _ => Some(repository.to_string())
201 };
202
203 let homepage = match homepage {
204 "" => None,
205 _ => Some(homepage.to_string())
206 };
207
208 ContractSchema {
209 casper_contract_schema_version: CCSV,
210 toolchain: env!("RUSTC_VERSION").to_string(),
211 contract_name: contract_name.to_string(),
212 contract_version: contract_version.to_string(),
213 types,
214 entry_points,
215 events,
216 call: Some(call_method(wasm_file_name, init_description, &init_args)),
217 authors,
218 repository,
219 homepage,
220 errors
221 }
222}
223
224pub fn find_schema_file_path(
226 contract_name: &str,
227 root_path: PathBuf
228) -> Result<PathBuf, &'static str> {
229 let mut path = root_path
230 .join(format!("{}_schema.json", camel_to_snake(contract_name)))
231 .with_extension("json");
232
233 let mut checked_paths = vec![];
234 for _ in 0..2 {
235 if path.exists() && path.is_file() {
236 return Ok(path);
237 } else {
238 checked_paths.push(path.clone());
239 path = path.parent().unwrap().to_path_buf();
240 }
241 }
242 Err("Schema not found")
243}
244
245fn call_method(
246 file_name: String,
247 description: Option<String>,
248 constructor_args: &[Argument]
249) -> CallMethod {
250 CallMethod {
251 wasm_file_name: file_name.to_string(),
252 description: description.map(String::from),
253 arguments: vec![
254 Argument {
255 name: odra_core::consts::PACKAGE_HASH_KEY_NAME_ARG.to_string(),
256 description: Some("The arg name for the package hash key name.".to_string()),
257 ty: NamedCLType::String.into(),
258 optional: false
259 },
260 Argument {
261 name: odra_core::consts::ALLOW_KEY_OVERRIDE_ARG.to_string(),
262 description: Some("If true and the key specified in odra_cfg_package_hash_key_name already exists, it will be overwritten.".to_string()),
263 ty: NamedCLType::Bool.into(),
264 optional: false
265 },
266 Argument {
267 name: odra_core::consts::IS_UPGRADABLE_ARG.to_string(),
268 description: Some(
269 "The arg name for the contract upgradeability setting.".to_string()
270 ),
271 ty: NamedCLType::Bool.into(),
272 optional: false
273 },
274 ]
275 .iter()
276 .chain(constructor_args.iter())
277 .cloned()
278 .collect()
279 }
280}
281
282pub fn camel_to_snake<T: ToString>(text: T) -> String {
284 text.to_string()
285 .from_case(Case::UpperCamel)
286 .without_boundaries(&[Boundary::UpperDigit, Boundary::LowerDigit])
287 .to_case(Case::Snake)
288}
289
290#[cfg(test)]
291mod test {
292 use odra_core::args::Maybe;
293 use odra_core::prelude::Address;
294
295 use super::*;
296
297 #[test]
298 fn test_argument() {
299 let arg = super::argument::<u32>("arg1");
300 assert_eq!(arg.name, "arg1");
301 assert_eq!(arg.ty, casper_contract_schema::NamedCLType::U32.into());
302 }
303
304 #[test]
305 fn test_opt_argument() {
306 let arg = super::argument::<Maybe<u32>>("arg1");
307 assert_eq!(arg.name, "arg1");
308 assert_eq!(arg.ty, casper_contract_schema::NamedCLType::U32.into());
309 }
310
311 #[test]
312 fn test_entry_point() {
313 let arg = super::argument::<u32>("arg1");
314 let entry_point = super::entry_point::<u32>("entry1", "description", true, vec![arg]);
315 assert_eq!(entry_point.name, "entry1");
316 assert_eq!(entry_point.description, Some("description".to_string()));
317 assert!(entry_point.is_mutable);
318 assert_eq!(entry_point.arguments.len(), 1);
319 assert_eq!(
320 entry_point.return_ty,
321 casper_contract_schema::NamedCLType::U32.into()
322 );
323 }
324
325 #[test]
326 fn test_struct_member() {
327 let member = super::struct_member::<u32>("member1");
328 assert_eq!(member.name, "member1");
329 assert_eq!(member.ty, casper_contract_schema::NamedCLType::U32.into());
330 }
331
332 #[test]
333 fn test_enum_typed_variant() {
334 let variant = super::enum_typed_variant::<Address>("variant1", 1);
335 assert_eq!(variant.name, "variant1");
336 assert_eq!(variant.discriminant, 1);
337 assert_eq!(variant.ty, casper_contract_schema::NamedCLType::Key.into());
338 }
339
340 #[test]
341 fn test_enum_variant() {
342 let variant = super::enum_variant("variant1", 1);
343 assert_eq!(variant.name, "variant1");
344 assert_eq!(variant.discriminant, 1);
345 assert_eq!(variant.ty, casper_contract_schema::NamedCLType::Unit.into());
346 }
347
348 #[test]
349 fn test_custom_struct() {
350 let member = super::struct_member::<u32>("member1");
351 let custom_struct = super::custom_struct("struct1", vec![member]);
352 match custom_struct {
353 casper_contract_schema::CustomType::Struct { name, members, .. } => {
354 assert_eq!(name, "struct1".into());
355 assert_eq!(members.len(), 1);
356 }
357 _ => panic!("Expected CustomType::Struct")
358 }
359 }
360
361 #[test]
362 fn test_custom_enum() {
363 let variant1 = super::enum_variant("variant1", 1);
364 let variant2 = super::enum_typed_variant::<String>("v2", 2);
365 let variant3 = super::enum_custom_type_variant("v3", 3, "Type1");
366 let custom_enum = super::custom_enum("enum1", vec![variant1, variant2, variant3]);
367 match custom_enum {
368 casper_contract_schema::CustomType::Enum { name, variants, .. } => {
369 assert_eq!(name, "enum1".into());
370 assert_eq!(variants.len(), 3);
371 assert_eq!(variants[0].ty, NamedCLType::Unit.into());
372 assert_eq!(variants[1].ty, NamedCLType::String.into());
373 assert_eq!(variants[2].ty, NamedCLType::Custom("Type1".into()).into());
374 }
375 _ => panic!("Expected CustomType::Enum")
376 }
377 }
378
379 #[test]
380 fn test_event() {
381 let event = super::event("event1");
382 assert_eq!(event.name, "event1");
383 }
384
385 #[test]
386 fn test_error() {
387 let error = super::error("error1", "description", 1);
388 assert_eq!(error.name, "error1");
389 assert_eq!(error.description, Some("description".to_string()));
390 assert_eq!(error.discriminant, 1);
391 }
392
393 #[test]
394 fn test_schema() {
395 struct TestSchema;
396
397 impl SchemaEntrypoints for TestSchema {
398 fn schema_entrypoints() -> Vec<Entrypoint> {
399 vec![entry_point::<u32>(
400 "entry1",
401 "description",
402 true,
403 vec![super::argument::<u32>("arg1")]
404 )]
405 }
406 }
407
408 impl SchemaEvents for TestSchema {
409 fn schema_events() -> Vec<Event> {
410 vec![event("event1")]
411 }
412 }
413
414 impl SchemaCustomTypes for TestSchema {
415 fn schema_types() -> Vec<Option<CustomType>> {
416 vec![
417 Some(custom_struct(
418 "struct1",
419 vec![struct_member::<u32>("member1")]
420 )),
421 Some(custom_enum("struct1", vec![enum_variant("variant1", 1)])),
422 ]
423 }
424 }
425
426 impl SchemaErrors for TestSchema {
427 fn schema_errors() -> Vec<UserError> {
428 vec![]
429 }
430 }
431
432 let schema = super::schema::<TestSchema>(
433 "module_name",
434 "contract_name",
435 "contract_version",
436 vec!["author".to_string()],
437 "repository",
438 "homepage"
439 );
440
441 assert_eq!(schema.contract_name, "contract_name");
442 assert_eq!(schema.contract_version, "contract_version");
443 assert_eq!(schema.authors, vec!["author".to_string()]);
444 assert_eq!(schema.repository, Some("repository".to_string()));
445 assert_eq!(schema.homepage, Some("homepage".to_string()));
446 assert_eq!(schema.entry_points.len(), 1);
447 assert_eq!(schema.types.len(), 2);
448 assert_eq!(schema.errors.len(), 0);
449 assert_eq!(schema.events.len(), 1);
450 }
451}