1use async_std::task;
4use contextual::{DisplayWithContext, WithContext};
5use iref::{IriBuf, IriRefBuf};
6use json_ld_next::{Expand, FsLoader, LoadError, ValidId};
7use proc_macro2::TokenStream;
8use proc_macro_error::proc_macro_error;
9use quote::quote;
10use rdf_types::{
11 dataset::IndexedBTreeDataset,
12 vocabulary::{IriVocabulary, IriVocabularyMut, LiteralIndex},
13 Quad,
14};
15use std::collections::HashMap;
16use std::fmt;
17use std::path::PathBuf;
18use syn::parse::ParseStream;
19use syn::spanned::Spanned;
20
21mod vocab;
22use vocab::{BlankIdIndex, IndexQuad, IndexTerm, IriIndex, Vocab};
23mod ty;
24use ty::{Type, UnknownType};
25
26type IndexVocabulary = rdf_types::vocabulary::IndexVocabulary<IriIndex, BlankIdIndex>;
27
28struct MountAttribute {
29 _paren: syn::token::Paren,
30 prefix: IriBuf,
31 _comma: syn::token::Comma,
32 target: PathBuf,
33}
34
35impl syn::parse::Parse for MountAttribute {
36 fn parse(input: ParseStream) -> syn::Result<Self> {
37 let content;
38 let _paren = syn::parenthesized!(content in input);
39
40 let prefix: syn::LitStr = content.parse()?;
41 let prefix = IriBuf::new(prefix.value())
42 .map_err(|e| content.error(format!("invalid IRI `{}`", e.0)))?;
43
44 let _comma = content.parse()?;
45
46 let target: syn::LitStr = content.parse()?;
47
48 Ok(Self {
49 _paren,
50 prefix,
51 _comma,
52 target: target.value().into(),
53 })
54 }
55}
56
57struct IriAttribute {
58 _paren: syn::token::Paren,
59 iri: IriBuf,
60}
61
62impl syn::parse::Parse for IriAttribute {
63 fn parse(input: ParseStream) -> syn::Result<Self> {
64 let content;
65 let _paren = syn::parenthesized!(content in input);
66
67 let iri: syn::LitStr = content.parse()?;
68 let iri = IriBuf::new(iri.value())
69 .map_err(|e| content.error(format!("invalid IRI `{}`", e.0)))?;
70
71 Ok(Self { _paren, iri })
72 }
73}
74
75struct IriArg {
76 iri: IriBuf,
77}
78
79impl syn::parse::Parse for IriArg {
80 fn parse(input: ParseStream) -> syn::Result<Self> {
81 let iri: syn::LitStr = input.parse()?;
82 let iri =
83 IriBuf::new(iri.value()).map_err(|e| input.error(format!("invalid IRI `{}`", e.0)))?;
84
85 Ok(Self { iri })
86 }
87}
88
89struct PrefixBinding {
90 _paren: syn::token::Paren,
91 prefix: String,
92 _eq: syn::token::Eq,
93 iri: IriBuf,
94}
95
96impl syn::parse::Parse for PrefixBinding {
97 fn parse(input: ParseStream) -> syn::Result<Self> {
98 let content;
99 let _paren = syn::parenthesized!(content in input);
100
101 let prefix: syn::LitStr = content.parse()?;
102
103 let _eq = content.parse()?;
104
105 let iri: syn::LitStr = content.parse()?;
106 let iri = IriBuf::new(iri.value())
107 .map_err(|e| content.error(format!("invalid IRI `{}`", e.0)))?;
108
109 Ok(Self {
110 _paren,
111 prefix: prefix.value(),
112 _eq,
113 iri,
114 })
115 }
116}
117
118struct IgnoreAttribute {
119 _paren: syn::token::Paren,
120 iri_ref: IriRefBuf,
121 _comma: syn::token::Comma,
122 _see: syn::Ident,
123 _eq: syn::token::Eq,
124 link: String,
125}
126
127impl syn::parse::Parse for IgnoreAttribute {
128 fn parse(input: ParseStream) -> syn::Result<Self> {
129 let content;
130 let _paren = syn::parenthesized!(content in input);
131
132 let iri_ref: syn::LitStr = content.parse()?;
133 let iri_ref = IriRefBuf::new(iri_ref.value())
134 .map_err(|e| content.error(format!("invalid IRI reference `{}`", e.0)))?;
135
136 let _comma = content.parse()?;
137
138 let _see = content.parse()?;
139
140 let _eq = content.parse()?;
141
142 let link: syn::LitStr = content.parse()?;
143 let link = link.value();
144
145 Ok(Self {
146 _paren,
147 iri_ref,
148 _comma,
149 _see,
150 _eq,
151 link,
152 })
153 }
154}
155
156struct TestSpec {
157 id: syn::Ident,
158 prefix: String,
159 suite: IriIndex,
160 types: HashMap<syn::Ident, ty::Definition>,
161 type_map: HashMap<IriIndex, syn::Ident>,
162 ignore: HashMap<IriIndex, String>,
163}
164
165struct InvalidIri(String);
166
167fn expand_iri(
168 vocabulary: &mut IndexVocabulary,
169 bindings: &mut HashMap<String, IriIndex>,
170 iri: IriBuf,
171) -> Result<IriIndex, InvalidIri> {
172 match iri.as_str().split_once(':') {
173 Some((prefix, suffix)) => match bindings.get(prefix) {
174 Some(prefix) => {
175 let mut result = vocabulary.iri(prefix).unwrap().to_string();
176 result.push_str(suffix);
177
178 match iref::Iri::new(&result) {
179 Ok(iri) => Ok(vocabulary.insert(iri)),
180 Err(_) => Err(InvalidIri(iri.to_string())),
181 }
182 }
183 None => Ok(vocabulary.insert(iri.as_iri())),
184 },
185 None => Ok(vocabulary.insert(iri.as_iri())),
186 }
187}
188
189#[proc_macro_attribute]
190#[proc_macro_error]
191pub fn test_suite(
192 args: proc_macro::TokenStream,
193 input: proc_macro::TokenStream,
194) -> proc_macro::TokenStream {
195 let mut input = syn::parse_macro_input!(input as syn::ItemMod);
196 let mut vocabulary = IndexVocabulary::new();
197
198 match task::block_on(derive_test_suite(&mut vocabulary, &mut input, args)) {
199 Ok(tokens) => quote! { #input #tokens }.into(),
200 Err(e) => {
201 proc_macro_error::abort_call_site!(
202 "test suite generation failed: {}",
203 (*e).with(&vocabulary)
204 )
205 }
206 }
207}
208
209async fn derive_test_suite(
210 vocabulary: &mut IndexVocabulary,
211 input: &mut syn::ItemMod,
212 args: proc_macro::TokenStream,
213) -> Result<TokenStream, Box<Error>> {
214 let mut loader = FsLoader::default();
215 let spec = parse_input(vocabulary, &mut loader, input, args)?;
216 generate_test_suite(vocabulary, loader, spec).await
217}
218
219fn parse_input(
220 vocabulary: &mut IndexVocabulary,
221 loader: &mut FsLoader,
222 input: &mut syn::ItemMod,
223 args: proc_macro::TokenStream,
224) -> Result<TestSpec, Box<Error>> {
225 let suite: IriArg = syn::parse(args).map_err(|e| Box::new(e.into()))?;
226 let base = suite.iri;
227 let suite = vocabulary.insert(base.as_iri());
228
229 let mut bindings: HashMap<String, IriIndex> = HashMap::new();
230 let mut ignore: HashMap<IriIndex, String> = HashMap::new();
231
232 let attrs = std::mem::take(&mut input.attrs);
233 for attr in attrs {
234 if attr.path.is_ident("mount") {
235 let mount: MountAttribute = syn::parse2(attr.tokens).map_err(|e| Box::new(e.into()))?;
236 loader.mount(mount.prefix.as_iri().to_owned(), mount.target)
237 } else if attr.path.is_ident("iri_prefix") {
238 let attr: PrefixBinding = syn::parse2(attr.tokens).map_err(|e| Box::new(e.into()))?;
239 bindings.insert(attr.prefix, vocabulary.insert(attr.iri.as_iri()));
240 } else if attr.path.is_ident("ignore_test") {
241 let attr: IgnoreAttribute = syn::parse2(attr.tokens).map_err(|e| Box::new(e.into()))?;
242 let resolved = attr.iri_ref.resolved(base.as_iri());
243 ignore.insert(vocabulary.insert(resolved.as_iri()), attr.link);
244 } else {
245 input.attrs.push(attr)
246 }
247 }
248
249 let mut type_map = HashMap::new();
250 let mut types = HashMap::new();
251 if let Some((_, items)) = input.content.as_mut() {
252 for item in items {
253 match item {
254 syn::Item::Struct(s) => {
255 types.insert(
256 s.ident.clone(),
257 ty::Definition::Struct(parse_struct_type(
258 vocabulary,
259 &mut bindings,
260 &mut type_map,
261 s,
262 )?),
263 );
264 }
265 syn::Item::Enum(e) => {
266 types.insert(
267 e.ident.clone(),
268 ty::Definition::Enum(parse_enum_type(
269 vocabulary,
270 &mut bindings,
271 &mut type_map,
272 e,
273 )?),
274 );
275 }
276 _ => (),
277 }
278 }
279 }
280
281 let prefix = test_prefix(&input.ident.to_string());
282 Ok(TestSpec {
283 id: input.ident.clone(),
284 prefix,
285 suite,
286 types,
287 type_map,
288 ignore,
289 })
290}
291
292fn parse_struct_type(
293 vocabulary: &mut IndexVocabulary,
294 bindings: &mut HashMap<String, IriIndex>,
295 type_map: &mut HashMap<IriIndex, syn::Ident>,
296 s: &mut syn::ItemStruct,
297) -> Result<ty::Struct, Box<Error>> {
298 let mut fields = HashMap::new();
299
300 let attrs = std::mem::take(&mut s.attrs);
301 for attr in attrs {
302 if attr.path.is_ident("iri") {
303 let attr: IriAttribute = syn::parse2(attr.tokens).map_err(|e| Box::new(e.into()))?;
304 let iri = expand_iri(vocabulary, bindings, attr.iri).map_err(|e| Box::new(e.into()))?;
305 type_map.insert(iri, s.ident.clone());
306 } else {
307 s.attrs.push(attr)
308 }
309 }
310
311 for field in &mut s.fields {
312 let span = field.span();
313
314 let id = match field.ident.clone() {
315 Some(id) => id,
316 None => {
317 proc_macro_error::abort!(span, "only named fields are supported")
318 }
319 };
320
321 let mut iri: Option<IriIndex> = None;
322 let attrs = std::mem::take(&mut field.attrs);
323 for attr in attrs {
324 if attr.path.is_ident("iri") {
325 let attr: IriAttribute =
326 syn::parse2(attr.tokens).map_err(|e| Box::new(e.into()))?;
327 iri = Some(
328 expand_iri(vocabulary, bindings, attr.iri).map_err(|e| Box::new(e.into()))?,
329 )
330 } else {
331 field.attrs.push(attr)
332 }
333 }
334
335 match iri {
336 Some(iri) => {
337 let ty_span = field.ty.span();
338 match ty::parse(field.ty.clone()) {
339 Ok(ty::Parsed {
340 ty,
341 required,
342 multiple,
343 }) => {
344 fields.insert(
345 iri,
346 ty::Field {
347 id,
348 ty,
349 required,
350 multiple,
351 },
352 );
353 }
354 Err(UnknownType) => {
355 proc_macro_error::abort!(ty_span, "unknown type")
356 }
357 }
358 }
359 None => {
360 proc_macro_error::abort!(span, "no IRI specified for field")
361 }
362 }
363 }
364
365 Ok(ty::Struct { fields })
366}
367
368fn parse_enum_type(
369 vocabulary: &mut IndexVocabulary,
370 bindings: &mut HashMap<String, IriIndex>,
371 type_map: &mut HashMap<IriIndex, syn::Ident>,
372 e: &mut syn::ItemEnum,
373) -> Result<ty::Enum, Box<Error>> {
374 let mut variants = HashMap::new();
375
376 let attrs = std::mem::take(&mut e.attrs);
377 for attr in attrs {
378 if attr.path.is_ident("iri") {
379 let attr: IriAttribute = syn::parse2(attr.tokens).map_err(|e| Box::new(e.into()))?;
380 let iri = expand_iri(vocabulary, bindings, attr.iri).map_err(|e| Box::new(e.into()))?;
381 type_map.insert(iri, e.ident.clone());
382 } else {
383 e.attrs.push(attr)
384 }
385 }
386
387 for variant in &mut e.variants {
388 let span = variant.span();
389 let mut iri: Option<IriIndex> = None;
390 let attrs = std::mem::take(&mut variant.attrs);
391 for attr in attrs {
392 if attr.path.is_ident("iri") {
393 let attr: IriAttribute =
394 syn::parse2(attr.tokens).map_err(|e| Box::new(e.into()))?;
395 iri = Some(
396 expand_iri(vocabulary, bindings, attr.iri).map_err(|e| Box::new(e.into()))?,
397 )
398 } else {
399 variant.attrs.push(attr)
400 }
401 }
402
403 match iri {
404 Some(iri) => {
405 let mut fields = HashMap::new();
406
407 for field in &mut variant.fields {
408 let field_span = field.span();
409 let id = match field.ident.clone() {
410 Some(id) => id,
411 None => {
412 proc_macro_error::abort!(field_span, "only named fields are supported")
413 }
414 };
415
416 let mut field_iri: Option<IriIndex> = None;
417 let attrs = std::mem::take(&mut field.attrs);
418 for attr in attrs {
419 if attr.path.is_ident("iri") {
420 let attr: IriAttribute =
421 syn::parse2(attr.tokens).map_err(|e| Box::new(e.into()))?;
422 field_iri = Some(
423 expand_iri(vocabulary, bindings, attr.iri)
424 .map_err(|e| Box::new(e.into()))?,
425 )
426 } else {
427 field.attrs.push(attr)
428 }
429 }
430
431 let field_iri = match field_iri {
432 Some(iri) => iri,
433 None => {
434 proc_macro_error::abort!(field_span, "no IRI specified for field")
435 }
436 };
437
438 let ty_span = field.ty.span();
439 match ty::parse(field.ty.clone()) {
440 Ok(ty::Parsed {
441 ty,
442 required,
443 multiple,
444 }) => {
445 fields.insert(
446 field_iri,
447 ty::Field {
448 id,
449 ty,
450 required,
451 multiple,
452 },
453 );
454 }
455 Err(UnknownType) => {
456 proc_macro_error::abort!(ty_span, "unknown type")
457 }
458 }
459 }
460
461 variants.insert(
462 iri,
463 ty::Variant {
464 id: variant.ident.clone(),
465 data: ty::Struct { fields },
466 },
467 );
468 }
469 None => {
470 proc_macro_error::abort!(span, "no IRI specified for variant")
471 }
472 }
473 }
474
475 Ok(ty::Enum { variants })
476}
477
478enum Error {
479 Parse(syn::Error),
480 Load(LoadError),
481 Expand(json_ld_next::expansion::Error),
482 InvalidIri(String),
483 InvalidValue(
484 Type,
485 json_ld_next::rdf::Value<IriIndex, BlankIdIndex, LiteralIndex>,
486 ),
487 InvalidTypeField,
488 NoTypeVariants(IndexTerm),
489 MultipleTypeVariants(IndexTerm),
490}
491
492impl From<syn::Error> for Error {
493 fn from(e: syn::Error) -> Self {
494 Self::Parse(e)
495 }
496}
497
498impl From<InvalidIri> for Error {
499 fn from(InvalidIri(s): InvalidIri) -> Self {
500 Self::InvalidIri(s)
501 }
502}
503
504impl DisplayWithContext<IndexVocabulary> for Error {
505 fn fmt_with(&self, vocabulary: &IndexVocabulary, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506 use fmt::Display;
507 match self {
508 Self::Parse(e) => e.fmt(f),
509 Self::Load(e) => e.fmt(f),
510 Self::Expand(e) => e.fmt(f),
511 Self::InvalidIri(i) => write!(f, "invalid IRI `{i}`"),
512 Self::InvalidValue(ty, value) => {
513 write!(f, "invalid value {} for type {ty}", value.with(vocabulary))
514 }
515 Self::InvalidTypeField => write!(f, "invalid type field"),
516 Self::NoTypeVariants(r) => {
517 write!(f, "no type variants defined for `{}`", r.with(vocabulary))
518 }
519 Self::MultipleTypeVariants(r) => write!(
520 f,
521 "multiple type variants defined for `{}`",
522 r.with(vocabulary)
523 ),
524 }
525 }
526}
527
528async fn generate_test_suite(
529 vocabulary: &mut IndexVocabulary,
530 loader: FsLoader,
531 spec: TestSpec,
532) -> Result<TokenStream, Box<Error>> {
533 use json_ld_next::{Loader, RdfQuads};
534
535 let json_ld = loader
536 .load_with(vocabulary, spec.suite)
537 .await
538 .map_err(Error::Load)?;
539
540 let mut expanded_json_ld: json_ld_next::ExpandedDocument<IriIndex, BlankIdIndex> = json_ld
541 .expand_with(vocabulary, &loader)
542 .await
543 .map_err(Error::Expand)?;
544
545 let mut generator = rdf_types::generator::Blank::new();
546 expanded_json_ld.identify_all_with(vocabulary, &mut generator);
547
548 let rdf_quads = expanded_json_ld.rdf_quads_with(vocabulary, &mut generator, None);
549 let dataset: IndexedBTreeDataset<IndexTerm> = rdf_quads.map(quad_to_owned).collect();
550
551 let mut tests = HashMap::new();
552
553 for Quad(subject, predicate, object, graph) in &dataset {
554 if graph.is_none() {
555 if let IndexTerm::Id(ValidId::Iri(id)) = subject {
556 if *predicate
557 == IndexTerm::Id(ValidId::Iri(IriIndex::Iri(Vocab::Rdf(vocab::Rdf::Type))))
558 {
559 if let json_ld_next::rdf::Value::Id(ValidId::Iri(ty)) = object {
560 if let Some(type_id) = spec.type_map.get(ty) {
561 match spec.ignore.get(id) {
562 Some(link) => {
563 println!(
564 " {} test `{}` (see {})",
565 yansi::Paint::yellow("Ignoring").bold(),
566 vocabulary.iri(id).unwrap(),
567 link
568 );
569 }
570 None => {
571 tests.insert(*id, type_id);
572 }
573 }
574 }
575 }
576 }
577 }
578 }
579 }
580
581 let id = &spec.id;
582 let mut tokens = TokenStream::new();
583 for (test, type_id) in tests {
584 let ty = spec.types.get(type_id).unwrap();
585 let cons = ty.generate(
586 vocabulary,
587 &spec,
588 &dataset,
589 IndexTerm::iri(test),
590 quote! { #id :: #type_id },
591 )?;
592
593 let func_name = func_name(
594 &spec.prefix,
595 vocabulary.iri(&test).unwrap().fragment().unwrap().as_str(),
596 );
597 let func_id = quote::format_ident!("{}", func_name);
598
599 tokens.extend(quote! {
600 #[test]
601 fn #func_id() {
602 #cons.run()
603 }
604 })
605 }
606
607 Ok(tokens)
608}
609
610fn test_prefix(name: &str) -> String {
611 let mut segments = Vec::new();
612 let mut buffer = String::new();
613
614 for c in name.chars() {
615 if c.is_uppercase() && !buffer.is_empty() {
616 segments.push(buffer);
617 buffer = String::new();
618 }
619
620 buffer.push(c.to_lowercase().next().unwrap())
621 }
622
623 if !buffer.is_empty() {
624 segments.push(buffer)
625 }
626
627 if segments.len() > 1 && segments.last().unwrap() == "test" {
628 segments.pop();
629 }
630
631 let mut result = String::new();
632
633 for segment in segments {
634 result.push_str(&segment);
635 result.push('_')
636 }
637
638 result
639}
640
641fn func_name(prefix: &str, id: &str) -> String {
642 let mut name = prefix.to_string();
643 name.push_str(id);
644 name
645}
646
647fn quad_to_owned(
648 rdf_types::Quad(subject, predicate, object, graph): json_ld_next::rdf::QuadRef<
649 IriIndex,
650 BlankIdIndex,
651 LiteralIndex,
652 >,
653) -> IndexQuad {
654 Quad(
655 IndexTerm::Id(*subject.as_ref()),
656 IndexTerm::Id(*predicate.as_ref()),
657 object,
658 graph.copied().map(IndexTerm::Id),
659 )
660}