rdf_mapper_annotations 0.10.2 copy "rdf_mapper_annotations: ^0.10.2" to clipboard
rdf_mapper_annotations: ^0.10.2 copied to clipboard

Dart annotations for declarative RDF graph mapping and code generation.

example/main.dart

import 'package:rdf_core/rdf_core.dart';
import 'package:rdf_mapper/rdf_mapper.dart';
import 'package:rdf_mapper_annotations/rdf_mapper_annotations.dart';
import 'package:rdf_vocabularies_schema/schema.dart';

/// This file demonstrates how the annotations can be used to mark up model classes
/// for automatic mapper generation.

// --- Annotated Model Classes ---

@RdfGlobalResource(
    SchemaBook.classIri, IriStrategy('http://example.org/book/{id}'))
class Book {
  @RdfIriPart('id')
  final String id;

  @RdfProperty(SchemaBook.name)
  final String title;

  @RdfProperty(SchemaBook.author,
      iri: IriMapping('http://example.org/author/{authorId}'))
  final String authorId;

  @RdfProperty(SchemaBook.datePublished)
  final DateTime published;

  // The mode is automatically detected from the @RdfIri annotation on the ISBN class, we do not need to
  // know here that it is actually mapped to a IriTerm and not a LiteralTerm.
  @RdfProperty(SchemaBook.isbn)
  final ISBN isbn;

  // Note how we use an (annotated) custom class for the rating which essentially is an int.
  @RdfProperty(SchemaBook.aggregateRating)
  final Rating rating;

  // And even an enum can be used here, serialized as IRI in this case.
  @RdfProperty(SchemaBook.bookFormat)
  final BookFormat format;

  // Iterable type is automatically detected, Chapter is also automatically detected.
  @RdfProperty(SchemaBook.hasPart)
  final Iterable<Chapter> chapters;

  Book({
    required this.id,
    required this.title,
    required this.authorId,
    required this.published,
    required this.isbn,
    required this.rating,
    required this.format,
    required this.chapters,
  });
}

@RdfLocalResource(SchemaChapter.classIri)
class Chapter {
  @RdfProperty(SchemaChapter.name)
  final String title;

  @RdfProperty(SchemaChapter.position)
  final int number;

  Chapter(this.title, this.number);
}

@RdfIri('urn:isbn:{value}')
class ISBN {
  @RdfIriPart() // marks this property as the value source
  final String value;

  ISBN(this.value);
}

@RdfLiteral()
class Rating {
  @RdfValue()
  final int stars;

  Rating(this.stars) {
    if (stars < 0 || stars > 5) {
      throw ArgumentError('Rating must be between 0 and 5 stars');
    }
  }
}

@RdfIri("https://schema.org/{value}")
enum BookFormat {
  @RdfEnumValue("AudiobookFormat")
  audiobook,
  @RdfEnumValue("Hardcover")
  hardcover,
  @RdfEnumValue("Paperback")
  paperback,
  @RdfEnumValue("Ebook")
  ebook,
  @RdfEnumValue("GraphicNovel")
  graphicNovel,
}

// --- The mappers below demonstrate what would be generated ---
// --- See below the main() function for an example of how to use them ---
class BookAuthorIdMapper implements IriTermMapper<String> {
  static final RegExp _regex = RegExp(
    r'^http://example\.org/author/(?<authorId>[^/]*)$',
  );

  /// Constructor
  const BookAuthorIdMapper();

  @override
  String fromRdfTerm(IriTerm term, DeserializationContext context) {
    /// Parses IRI parts from a complete IRI using a template.
    final RegExpMatch? match = _regex.firstMatch(term.iri);

    final iriParts = {
      for (var name in match?.groupNames ?? const <String>[])
        name: match?.namedGroup(name) ?? '',
    };
    return iriParts['authorId']!;
  }

  @override
  IriTerm toRdfTerm(
    String iriTermValue,
    SerializationContext context, {
    RdfSubject? parentSubject,
  }) {
    final authorId = iriTermValue.toString();
    return IriTerm('http://example.org/author/${authorId}');
  }
}

/// Generated mapper for [Book] global resources.
///
/// This mapper handles serialization and deserialization between Dart objects
/// and RDF triples for resources of type Book.
class BookMapper implements GlobalResourceMapper<Book> {
  static final RegExp _regex = RegExp(
    r'^http://example\.org/book/(?<id>[^/]*)$',
  );

  final IriTermMapper<String> _authorIdMapper;

  /// Constructor
  const BookMapper({
    IriTermMapper<String> authorIdMapper = const BookAuthorIdMapper(),
  }) : _authorIdMapper = authorIdMapper;

  @override
  IriTerm? get typeIri => SchemaBook.classIri;

  @override
  Book fromRdfResource(IriTerm subject, DeserializationContext context) {
    final reader = context.reader(subject);

    final RegExpMatch? match = _regex.firstMatch(subject.iri);

    final iriParts = {
      for (var name in (match?.groupNames ?? const <String>[]))
        name: match?.namedGroup(name) ?? '',
    };

    final id = iriParts['id']!;
    final String title = reader.require(SchemaBook.name);
    final String authorId = reader.require(
      SchemaBook.author,
      deserializer: _authorIdMapper,
    );
    final DateTime published = reader.require(SchemaBook.datePublished);
    final ISBN isbn = reader.require(SchemaBook.isbn);
    final Rating rating = reader.require(SchemaBook.aggregateRating);
    final BookFormat format = reader.require(SchemaBook.bookFormat);
    final Iterable<Chapter> chapters = reader.getValues<Chapter>(
      SchemaBook.hasPart,
    );

    return Book(
      id: id,
      title: title,
      authorId: authorId,
      published: published,
      isbn: isbn,
      rating: rating,
      format: format,
      chapters: chapters,
    );
  }

  @override
  (IriTerm, Iterable<Triple>) toRdfResource(
    Book resource,
    SerializationContext context, {
    RdfSubject? parentSubject,
  }) {
    final subject = IriTerm(_buildIri(resource));

    return context
        .resourceBuilder(subject)
        .addValue(SchemaBook.name, resource.title)
        .addValue(
          SchemaBook.author,
          resource.authorId,
          serializer: _authorIdMapper,
        )
        .addValue(SchemaBook.datePublished, resource.published)
        .addValue(SchemaBook.isbn, resource.isbn)
        .addValue(SchemaBook.aggregateRating, resource.rating)
        .addValue(SchemaBook.bookFormat, resource.format)
        .addValues<Chapter>(SchemaBook.hasPart, resource.chapters)
        .build();
  }

  /// Builds the IRI for a resource instance using the IRI template.
  String _buildIri(Book resource) {
    final id = resource.id;
    return 'http://example.org/book/${id}';
  }
}

/// Generated mapper for [Chapter] global resources.
///
/// This mapper handles serialization and deserialization between Dart objects
/// and RDF triples for resources of type Chapter.
class ChapterMapper implements LocalResourceMapper<Chapter> {
  /// Constructor
  const ChapterMapper();

  @override
  IriTerm? get typeIri => SchemaChapter.classIri;

  @override
  Chapter fromRdfResource(
    BlankNodeTerm subject,
    DeserializationContext context,
  ) {
    final reader = context.reader(subject);

    final String title = reader.require(SchemaChapter.name);
    final int number = reader.require(SchemaChapter.position);

    return Chapter(title, number);
  }

  @override
  (BlankNodeTerm, Iterable<Triple>) toRdfResource(
    Chapter resource,
    SerializationContext context, {
    RdfSubject? parentSubject,
  }) {
    final subject = BlankNodeTerm();

    return context
        .resourceBuilder(subject)
        .addValue(SchemaChapter.name, resource.title)
        .addValue(SchemaChapter.position, resource.number)
        .build();
  }
}

/// Generated mapper for [ISBN] global resources.
///
/// This mapper handles serialization and deserialization between Dart objects
/// and RDF terms for iri terms of type ISBN.
class ISBNMapper implements IriTermMapper<ISBN> {
  static final RegExp _regex = RegExp(r'^urn:isbn:(?<value>[^/]*)$');

  /// Constructor
  const ISBNMapper();

  @override
  ISBN fromRdfTerm(IriTerm term, DeserializationContext context) {
    /// Parses IRI parts from a complete IRI using a template.
    final RegExpMatch? match = _regex.firstMatch(term.iri);

    final iriParts = {
      for (var name in match?.groupNames ?? const <String>[])
        name: match?.namedGroup(name) ?? '',
    };
    final value = iriParts['value']!;

    return ISBN(value);
  }

  @override
  IriTerm toRdfTerm(
    ISBN iriTermValue,
    SerializationContext context, {
    RdfSubject? parentSubject,
  }) {
    final value = iriTermValue.value;
    return IriTerm('urn:isbn:${value}');
  }
}

/// Generated mapper for [Rating] global resources.
///
/// This mapper handles serialization and deserialization between Dart objects
/// and RDF terms for iri terms of type Rating.
class RatingMapper implements LiteralTermMapper<Rating> {
  const RatingMapper();

  @override
  IriTerm? get datatype => null;

  @override
  Rating fromRdfTerm(
    LiteralTerm term,
    DeserializationContext context, {
    bool bypassDatatypeCheck = false,
  }) {
    final int stars = context.fromLiteralTerm(
      term,
      bypassDatatypeCheck: bypassDatatypeCheck,
    );

    return Rating(stars);
  }

  @override
  LiteralTerm toRdfTerm(
    Rating value,
    SerializationContext context, {
    RdfSubject? parentSubject,
  }) {
    return context.toLiteralTerm(value.stars);
  }
}

/// Generated mapper for [BookFormat] enum IRIs.
///
/// This mapper handles serialization and deserialization between enum constants
/// and RDF IRI terms for enum type BookFormat.
class BookFormatMapper implements IriTermMapper<BookFormat> {
  static final RegExp _regex = RegExp(r'^https://schema\.org/(?<value>[^/]*)$');

  /// Constructor
  const BookFormatMapper();

  @override
  BookFormat fromRdfTerm(IriTerm term, DeserializationContext context) {
    /// Parses IRI parts from a complete IRI using a template.
    final RegExpMatch? match = _regex.firstMatch(term.iri);

    if (match == null) {
      throw DeserializationException('Unknown BookFormat IRI: ${term.iri}');
    }

    final iriParts = {
      for (var name in match.groupNames) name: match.namedGroup(name) ?? '',
    };
    final enumValue = iriParts['value']!;

    return switch (enumValue) {
      'AudiobookFormat' => BookFormat.audiobook,
      'Hardcover' => BookFormat.hardcover,
      'Paperback' => BookFormat.paperback,
      'Ebook' => BookFormat.ebook,
      'GraphicNovel' => BookFormat.graphicNovel,
      _ => throw DeserializationException(
          'Unknown BookFormat IRI: ${term.iri}',
        ),
    };
  }

  @override
  IriTerm toRdfTerm(
    BookFormat value,
    SerializationContext context, {
    RdfSubject? parentSubject,
  }) =>
      switch (value) {
        BookFormat.audiobook => IriTerm(_buildIri('AudiobookFormat')),
        BookFormat.hardcover => IriTerm(_buildIri('Hardcover')),
        BookFormat.paperback => IriTerm(_buildIri('Paperback')),
        BookFormat.ebook => IriTerm(_buildIri('Ebook')),
        BookFormat.graphicNovel => IriTerm(_buildIri('GraphicNovel')),
      };

  /// Generates the complete IRI for a given enum value
  String _buildIri(String value) {
    return 'https://schema.org/${value}';
  }
}

// This would normally happen at build time via code generation
// Register all the generated mappers
RdfMapper initRdfMapper() {
  final rdfMapper = RdfMapper.withDefaultRegistry();

  var registry = rdfMapper.registry;

  registry.registerMapper<Book>(BookMapper());
  registry.registerMapper<Chapter>(ChapterMapper());
  registry.registerMapper<ISBN>(ISBNMapper());
  registry.registerMapper<Rating>(RatingMapper());
  registry.registerMapper<BookFormat>(BookFormatMapper());

  return rdfMapper;
}

// --- The code below demonstrates how you work with the generated code ---

void main() {
  // Initialize the RDF mapper with the generated initRdfMapper function
  final rdfMapper = initRdfMapper();

  // Create a sample book
  final book = Book(
    id: 'hobbit',
    title: 'The Hobbit',
    authorId: 'J.R.R. Tolkien',
    published: DateTime(1937, 9, 21),
    isbn: ISBN('9780618260300'),
    rating: Rating(5),
    format: BookFormat.hardcover,
    chapters: [
      Chapter('An Unexpected Party', 1),
      Chapter('Roast Mutton', 2),
      Chapter('A Short Rest', 3),
    ],
  );

  // Convert to RDF and print
  final turtle = rdfMapper.encodeObject(
    book,
    baseUri: 'http://example.org/book/',
  );
  print('Book as RDF Turtle:');
  print(turtle);

  // Deserialize back to a Book object
  final deserializedBook = rdfMapper.decodeObject<Book>(turtle);

  // Verify it worked correctly
  print('\nDeserialized book:');
  print('Title: ${deserializedBook.title}');
  print('Author: ${deserializedBook.authorId}');
  print('Chapters:');
  for (final chapter in deserializedBook.chapters) {
    print('- ${chapter.title} (${chapter.number})');
  }
}
2
likes
150
points
335
downloads

Publisher

unverified uploader

Weekly Downloads

Dart annotations for declarative RDF graph mapping and code generation.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#rdf #orm #linked-data #semantic-web

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

logging, rdf_core, rdf_mapper, rdf_vocabularies_core, rdf_vocabularies_schema

More

Packages that depend on rdf_mapper_annotations