Skip to content
This repository has been archived by the owner on Apr 13, 2019. It is now read-only.

library search

Konstantin Sobolev edited this page Apr 28, 2017 · 4 revisions

Adding search operation

Lets add a search by author operation which should find all the books by author pattern. It will expose the same map[BookId,BookRecord] type but will take an input parameter of type AuthorRecord specifying search pattern.

Schema changes

Output projection will be the same as for the read operation, so lets extract and reuse it. Resulting schema:

resource books: map[BookId, BookRecord] {
  // named output projection, to be reused by multiple operations
  outputProjection bookProjection: BookRecord = (
    title,
    author :(
      id,
      `record` (firstName, middleName, lastName)
    ),
    text :plain {
      ;offset: Long,                 // input parameter
      ;count: Long,                  // input parameter
      meta: (offset, count)          // supported meta-data projection
    }
  )

  // default read operatoin
  read {
    outputProjection [ required ] (  // map keys are required
      $bookProjection                // book record projection defined above
    )
  }

  // search operation
  read searchByAuthor {
    outputProjection {
      // parameter declaration
      ;+author: AuthorRecord (firstName, middleName, lastName)
    } [ forbidden ] (                // map keys are forbidden
      $bookProjection
    )
  }
}

Backend

Every field of theauthor parameter works as a filter if present. For instance if firstName is not specified, then any first name matches, otherwise only authors with this specific first name will be selected. Lets add corresponsing lookup function to the AuthorsBackend:

  /**
   * Finds authors by name
   *
   * @param firstName  author first name or {@code null} for any
   * @param middleName author middle name or {@code null} for any
   * @param lastName   author last name or {@code null} for any
   *
   * @return collection of matching author's IDs
   */
  public static @NotNull Collection<@NotNull AuthorId> findAuthors(
      @Nullable Optional<String> firstName,
      @Nullable Optional<String> middleName,
      @Nullable Optional<String> lastName) {

    return authors.entrySet().stream()
        .filter(e ->
            (firstName != null && Objects.equals(firstName.orElse(null), e.getValue().firstName)) ||
            (middleName != null && Objects.equals(middleName.orElse(null), e.getValue().middleName)) ||
            (lastName != null && Objects.equals(lastName.orElse(null), e.getValue().lastName)) ||
            (firstName == null && middleName == null && lastName == null)
        )
        .map(Map.Entry::getKey)
        .collect(Collectors.toList());
  }

Operation implementation

With this in place search action implementation is quite straightforward. It looks up the authors and then reuses BookBuilder.buildBook to populate results map.

/**
 * Search by author custom operation implementation
 */
public class SearchByAuthorOperation extends AbstractReadSearchByAuthorOperation {

  protected SearchByAuthorOperation(final @NotNull ReadOperationDeclaration declaration) {
    super(declaration);
  }

  /**
   * Runs the operation
   *
   * @param booksDataBuilder result builder to be populated, initially empty
   * @param outputProjection request projection
   *
   * @return {@code Future} of the books map data
   */
  @Override
  protected @NotNull CompletableFuture<BookId_BookRecord_Map.Data> process(
      final @NotNull BookId_BookRecord_Map.Builder.@NotNull Data booksDataBuilder,
      final @NotNull OutputBooksFieldProjection outputProjection) {


    final BookId_BookRecord_Map.Builder booksMap = BookId_BookRecord_Map.create();
    final OutputBookId_BookRecord_MapProjection booksMapProjection = outputProjection.dataProjection();
    final OutputBookRecordProjection bookRecordProjection = booksMapProjection.itemsProjection();


    for (AuthorId author : findAuthors(booksMapProjection.getAuthorParameter())) {
      for (BooksBackend.BookData book : BooksBackend.findByAuthor(author)) {
        booksMap.put_(book.id, BookBuilder.buildBook(book.id, bookRecordProjection));
      }
    }

    booksDataBuilder.set(booksMap);
    return CompletableFuture.completedFuture(booksDataBuilder);
  }

  /**
   * Find authors by pattern
   *
   * @param authorData author pattern. Absent fields are ignored
   *
   * @return collection of matching author IDs
   */
  private @NotNull Collection<AuthorId> findAuthors(@NotNull AuthorRecord authorData) {
      return AuthorsBackend.findAuthors(
          authorData.getFirstName_() == null ? null : Optional.ofNullable(authorData.getFirstName()),
          authorData.getMiddleName_() == null ? null : Optional.ofNullable(authorData.getMiddleName()),
          authorData.getLastName_() == null ? null : Optional.ofNullable(authorData.getLastName())
      );
  }
}

Last step is to add new operation to BooksResourceFactory

  @Override
  protected @NotNull ReadOperation<BookId_BookRecord_Map.Data> constructSearchByAuthorReadOperation(
      final @NotNull ReadOperationDeclaration operationDeclaration) throws ServiceInitializationException {
    return new SearchByAuthorOperation(operationDeclaration);
  }

Running

Lets see how it works

curl -s -g -H "Epigraph-Operation: searchByAuthor" "http://localhost:8888/books;author={firstName:'Arthur'}[](title,author:record(firstName,middleName,lastName),text:plain;count=80)" | jq
[
  {
    "K": 2,
    "V": {
      "title": "A Study In Scarlet",
      "author": {
        "firstName": "Arthur",
        "middleName": "Conan",
        "lastName": "Doyle"
      },
      "text": "IN the year 1878 I took my degree of Doctor of Medicine of the University of Lon"
    }
  }
]

We have explicitly specifid operation name using Epigraph-Operation HTTP header to ease up router's task, but it would work just fine without it too since author parameter presence allows to correctly pick the right operation.

Lets find books by all authors without a middle name

curl -s -g "http://localhost:8888/books;author={middleName:null}[](title,author:record(firstName,middleName,lastName),text:plain;count=80)" | jq
[
  {
    "K": 3,
    "V": {
      "title": "The Adventures of Tom Sawyer",
      "author": {
        "firstName": "Mark",
        "lastName": "Twain"
      },
      "text": "\"TOM!\"\nNo answer.\n\"TOM!\"\nNo answer.\n\"What's gone with that boy, I wonder? You TO"
    }
  },
  {
    "K": 1,
    "V": {
      "title": "The Gold Bug",
      "author": {
        "firstName": "Allan",
        "lastName": "Poe"
      },
      "text": "MANY years ago, I contracted an intimacy with a Mr. William Legrand. He was of a"
    }
  }
]

Notice how framework requires author parameter to be present:

curl -s -g -H "Epigraph-Operation: searchByAuthor" "http://localhost:8888/books[](title,author:record(firstName,middleName,lastName),text:plain;count=80)"
Failed to parse read operation 'searchByAuthor' request in resource 'books'

Required parameter 'author' is missing

/books[](title,author:record(firstName,middleName,lastName),text:plain;count=80)
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Next section: adding create operation