json_ld_testing_next/
lib.rs

1//! This library provides the `test_suite` derive macro
2//! that can generate Rust test suites from a JSON-LD document.
3use 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}