Custom derive: How to decide what to do?

tl;dr

I’m writting a custom derive that should, depending on a struct’s field’s type, either generate one or the other code.

What is the most ergonomic way to accomplish such behavior?

Problem: Macros can’t check if a type implements a specific trait because type information is gathered after macro expansion.

My approaches:

  1. Tell the compiler by specific attributes
  2. provide a static HashMap containing the required information (could this work?)
  3. Implement a trait on all types that returns an enum
    trait ImplInfo {
        fn trait_info() -> TraitInfo;
    }
    enum TraitInfo {
        ImplsTraitA,
        ImplsTraitB,
        None
    }
    

Concrete problem

I’m still working on my library for convenient conversion between RDF-data and Rust types.

Now I started to write custom derives to accomplish some serde flavor, i.e. I aim to have the following code running:

use sophia::ns::rdf;
const HAS_B: sophia::term::Term<&'static str> = ...;

#[derive(FromGraph)]
struct A {
   #[crdf(predicate = HAS_B)]
   b: B,
   #[crdf(predicate = rdf::value)]
   value: i32
}

#[derive(FromGraph)]
struct B (
    #[crdf(predicate = rdf::value)]
    String
)

The problem is there are two cases to turn RDF into a Rust type:

  1. The type is simple like i32 or String that can be built from a single RDF-term.
  2. The type is complex like A where the graph has to be traversed to get all information that is needed.

In the end, I have to check if a field’s type implements FromGraph or FromTerm to call the right fucntion.However, as far as I understand this is not possible because macro expansion is done before any type information is gathered.

Luckily RDF itself destinguishes between object- and data-predicates which are exactly what I need. But I don’t know how to put it into my derive's syntax.

  1. Should I use explicit syntax like:

    #[crdf(object_predicate = ...)] // and
    #[crdf(data_predicate = ...)]
    

    I’m reluctent to do that because this would be handled by the user which is always a good source of errors.

  2. Or can I add some other metadata to the predicates passed into the attribute, e.g. I could imagine a HashMap where the terms mapped are either object- or data-predicates.

Has someone experience about this problem?

I don’t think you need to generate two sets of code here, I’m not familiar with RDF, but aren’t those just two implementations of some FromRDF trait which tries to build a T given a reference to the graph and a particular term?

Sure, you’d need to write some boring impls for the primitive types and less-boring ones for custom containers, but once the building blocks exist you’ll be able to use your custom derive to build up more interesting things.

I’m envisioning something like this:

trait FromRDF: Sized {
  fn from_rdf(full_graph: &Graph, term: &Term) -> Result<Self, ParseError>;
}

impl FromRDF for String {
  fn from_rdf(full_graph: &Graph, term: &Term) -> Result<Self, ParseError> {
    match term {
      Term::String(s) => Ok(s.clone()),
      _ => Err(...),
    }
  }

impl FromRDF for HashMap<String, String> {
  fn from_rdf(full_graph: &Graph, term: &Term) -> Result<Self, ParseError> {
    ...
  }
} 

I guess you could say I’ve just merged FromGraph and FromTerm into a single FromRDF trait.

Yes of course you are right the FromGraph-side can be simplyfied.

I guess I picked the wrong example. The real problem occures when building a graph that represents a Rust type. In RDF knwoledge is represented as subject-predicate-object triples (RDF-triples).
Now to stich with the example of A and B a graph for A would for example be:

_:a has_b _:b .
_:a rdf:value 42^^xsd:integer .
_:b rdf:value "rust is awsome"^^^xsd:string .

Three RDF-triples, two with the subject URI of a, one with the root subject of b.
For this case I use the Iterator presented in this Topic.

I.e. an implementation for GetTriplesIter for A would look like:

impl GetTriplesIter for A {
    fn triples_iter<'a, TD>(&'a self, s: Term<TD>) -> TriplesIter<'a, TD> 
    where 
        TD: TermData,
    {
        let mut creators = VecDeque::new();
        let c = Box::new(move |s: Term<TD>| {
             [s, rdf::value.clone(), self.value.to_term()]
        }) as _;
        creators.push(c);
        let b_uri = create_uri_for_b();
        let b_uri1 = b_uri.clone();
        let c = Box::new(move |s: Term<TD>| {
            [s, HAS_B.clone(), b_uri1]
        }) as _;
        creators.push(c);
        let mut children = VecDeque::new();
        let child = self.b.triples_iter(b_uri);
        children.push(child)
        TriplesIter::new_with_children(s, creators, children)
    }
}

And now you can no longer build a simplyfied trait covering everything because in one case a boxed closure has to be build and in the other case a boxed closure and an iterator.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.