rdf_mapper 0.8.5 copy "rdf_mapper: ^0.8.5" to clipboard
rdf_mapper: ^0.8.5 copied to clipboard

ORM-like mapping between plain Dart classes and RdfGraph from rdf_core. Companion library for https://pub.dev/packages/rdf_core.

example/main.dart

import 'package:rdf_core/rdf_core.dart';

import 'package:rdf_mapper/rdf_mapper.dart';
import 'package:rdf_vocabularies/schema.dart';

void main() {
  final rdf =
      // Create mapper with default registry
      RdfMapper.withDefaultRegistry()
        // Register our custom mappers
        ..registerMapper<Book>(BookMapper())
        ..registerMapper<Chapter>(ChapterMapper())
        ..registerMapper<ISBN>(ISBNMapper())
        ..registerMapper<Rating>(RatingMapper());

  // Create a book with chapters
  final book = Book(
    id: 'hobbit', // Now just the identifier, not the full IRI
    title: 'The Hobbit',
    author: 'J.R.R. Tolkien',
    published: DateTime(1937, 9, 21),
    isbn: ISBN('9780618260300'),
    rating: Rating(5),
    chapters: [
      Chapter('An Unexpected Party', 1),
      Chapter('Roast Mutton', 2),
      Chapter('A Short Rest', 3),
    ],
  );

  // --- PRIMARY API: String-based operations ---

  // Convert the book to RDF Turtle format
  final turtle = rdf.encodeObject(book, baseUri: 'http://example.org/book/');

  // Print the resulting Turtle representation
  final expectedTurtle = '''
@base <http://example.org/book/> .
@prefix schema: <https://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<hobbit> a schema:Book;
    schema:aggregateRating 5;
    schema:author "J.R.R. Tolkien";
    schema:datePublished "1937-09-20T23:00:00.000Z"^^xsd:dateTime;
    schema:hasPart [ a schema:Chapter ; schema:name "An Unexpected Party" ; schema:position 1 ], [ a schema:Chapter ; schema:name "Roast Mutton" ; schema:position 2 ], [ a schema:Chapter ; schema:name "A Short Rest" ; schema:position 3 ];
    schema:isbn <urn:isbn:9780618260300>;
    schema:name "The Hobbit" .
''';

  print('Book as RDF Turtle:');
  print(turtle);
  assert(turtle.trim() == expectedTurtle.trim());

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

  // Verify it worked correctly
  print('\nDeserialized book:');
  print('Title: ${deserializedBook.title}');
  print('Author: ${deserializedBook.author}');
  print('ISBN: ${deserializedBook.isbn}');
  print('Rating: ${deserializedBook.rating}');
  print('Chapters:');
  for (final chapter in deserializedBook.chapters) {
    print('- ${chapter.title} (${chapter.number})');
  }

  // Example with multiple books using deserializeAllOfType
  final multipleBooks = '''
@prefix schema: <https://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<http://example.org/book/hobbit> a schema:Book;
    schema:name "The Hobbit";
    schema:author "J.R.R. Tolkien";
    schema:datePublished "1937-09-20T23:00:00.000Z"^^xsd:dateTime;
    schema:isbn <urn:isbn:9780618260300>;
    schema:aggregateRating "5"^^xsd:integer .
    
<http://example.org/book/lotr> a schema:Book;
    schema:name "The Lord of the Rings";
    schema:author "J.R.R. Tolkien";
    schema:datePublished "1954-07-28T23:00:00.000Z"^^xsd:dateTime;
    schema:isbn <urn:isbn:9780618640157>;
    schema:aggregateRating "5"^^xsd:integer .
''';

  // Use the type-safe deserialization for collections
  final books = rdf.decodeObjects<Book>(multipleBooks);

  print('\nDeserialized multiple books:');
  print('Found ${books.length} books');
  for (final book in books) {
    print('- ${book.title} by ${book.author}');
  }

  // --- GRAPH API: Direct graph operations ---

  print('\n=== GRAPH API EXAMPLES ===');

  // Convert the book to an RDF graph
  final bookGraph = rdf.graph.encodeObject(book);
  print('Graph contains ${bookGraph.size} triples');

  // Deserialize from the graph
  final bookFromGraph = rdf.graph.decodeObject<Book>(bookGraph);
  print('Successfully deserialized book from graph: ${bookFromGraph.title}');

  // Create a combined graph with more data
  final anotherBook = Book(
    id: 'silmarillion', // Now just the identifier, not the full IRI
    title: 'The Silmarillion',
    author: 'J.R.R. Tolkien',
    published: DateTime(1977, 9, 15),
    isbn: ISBN('9780048231536'),
    rating: Rating(4),
    chapters: [Chapter('Ainulindalë', 1)],
  );

  // Serialize multiple books to a single graph
  final booksGraph = rdf.graph.encodeObjects([book, anotherBook]);
  print('Combined graph contains ${booksGraph.size} triples');

  // Retrieve all books from the graph with type safety
  final booksFromGraph = rdf.graph.decodeObjects<Book>(booksGraph);
  print('Found ${booksFromGraph.length} books in the graph:');
  for (final book in booksFromGraph) {
    print('- ${book.title} (${book.published.year})');
  }
}

// --- Domain Model ---

// Primary entity with an identifier that will be part of the IRI
class Book {
  final String id;
  final String title;
  final String author;
  final DateTime published;
  final ISBN isbn;
  final Rating rating;
  // TODO: once we properly support rdf:List, we can use List<Chapter>
  final Iterable<Chapter> chapters;

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

// Value object using blank nodes (no identifier)
class Chapter {
  final String title;
  final int number;

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

// Custom identifier type using IRI mapping
class ISBN {
  final String value;

  ISBN(this.value);

  @override
  String toString() => value;
}

// Custom value type using literal mapping
class Rating {
  final int stars;

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

  @override
  String toString() => '$stars stars';
}

// --- Mappers ---

// IRI-based entity mapper
class BookMapper implements GlobalResourceMapper<Book> {
  static final titlePredicate = SchemaBook.name;
  static final authorPredicate = SchemaBook.author;
  static final publishedPredicate = SchemaBook.datePublished;
  static final isbnPredicate = SchemaBook.isbn;
  static final ratingPredicate = SchemaBook.aggregateRating;
  static final chapterPredicate = SchemaBook.hasPart;

  // Base IRI prefix for book resources
  static const String bookIriPrefix = 'http://example.org/book/';

  @override
  final IriTerm typeIri = SchemaBook.classIri;

  /// Converts an ID to a full IRI
  String _createIriFromId(String id) => '$bookIriPrefix$id';

  /// Extracts the identifier from a full IRI
  String _extractIdFromIri(String iri) {
    if (!iri.startsWith(bookIriPrefix)) {
      throw ArgumentError('Invalid Book IRI format: $iri');
    }
    return iri.substring(bookIriPrefix.length);
  }

  @override
  Book fromRdfResource(IriTerm subject, DeserializationContext context) {
    final reader = context.reader(subject);
    return Book(
      // Extract just the identifier part from the IRI
      id: _extractIdFromIri(subject.iri),
      title: reader.require<String>(titlePredicate),
      author: reader.require<String>(authorPredicate),
      published: reader.require<DateTime>(publishedPredicate),
      isbn: reader.require<ISBN>(isbnPredicate),
      rating: reader.require<Rating>(ratingPredicate),
      chapters: reader.getValues<Chapter>(chapterPredicate),
    );
  }

  @override
  (IriTerm, List<Triple>) toRdfResource(
    Book book,
    SerializationContext context, {
    RdfSubject? parentSubject,
  }) {
    return context
        .resourceBuilder(IriTerm(_createIriFromId(book.id)))
        .addValue(titlePredicate, book.title)
        .addValue(authorPredicate, book.author)
        .addValue<DateTime>(publishedPredicate, book.published)
        .addValue<ISBN>(isbnPredicate, book.isbn)
        .addValue<Rating>(ratingPredicate, book.rating)
        .addValues(chapterPredicate, book.chapters)
        .build();
  }
}

// Blank node-based entity mapper
class ChapterMapper implements LocalResourceMapper<Chapter> {
  static final titlePredicate = SchemaChapter.name;
  static final numberPredicate = SchemaChapter.position;

  @override
  final IriTerm typeIri = SchemaChapter.classIri;

  @override
  Chapter fromRdfResource(BlankNodeTerm term, DeserializationContext context) {
    final reader = context.reader(term);
    return Chapter(
      reader.require<String>(titlePredicate),
      reader.require<int>(numberPredicate),
    );
  }

  @override
  (BlankNodeTerm, List<Triple>) toRdfResource(
    Chapter chapter,
    SerializationContext ctxt, {
    RdfSubject? parentSubject,
  }) {
    return ctxt
        .resourceBuilder(BlankNodeTerm())
        .addValue(titlePredicate, chapter.title)
        .addValue<int>(numberPredicate, chapter.number)
        .build();
  }
}

// Custom IRI mapper
class ISBNMapper implements IriTermMapper<ISBN> {
  static const String ISBN_URI_PREFIX = 'urn:isbn:';

  @override
  IriTerm toRdfTerm(ISBN isbn, SerializationContext context) {
    return IriTerm('$ISBN_URI_PREFIX${isbn.value}');
  }

  @override
  ISBN fromRdfTerm(IriTerm term, DeserializationContext context) {
    final uri = term.iri;
    if (!uri.startsWith(ISBN_URI_PREFIX)) {
      throw ArgumentError('Invalid ISBN URI format: $uri');
    }
    return ISBN(uri.substring(ISBN_URI_PREFIX.length));
  }
}

// Custom literal mapper
class RatingMapper implements LiteralTermMapper<Rating> {
  const RatingMapper();

  @override
  LiteralTerm toRdfTerm(Rating rating, SerializationContext context) {
    return LiteralTerm.typed(rating.stars.toString(), 'integer');
  }

  @override
  Rating fromRdfTerm(LiteralTerm term, DeserializationContext context,
      {bool bypassDatatypeCheck = false}) {
    return Rating(int.parse(term.value));
  }
}
3
likes
160
points
600
downloads

Publisher

unverified uploader

Weekly Downloads

ORM-like mapping between plain Dart classes and RdfGraph from rdf_core. Companion library for https://pub.dev/packages/rdf_core.

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_vocabularies

More

Packages that depend on rdf_mapper