Skip to content

Latest commit

 

History

History
285 lines (194 loc) · 14.1 KB

README.md

File metadata and controls

285 lines (194 loc) · 14.1 KB

FSharp.JsonApi


No longer actively maintained, please check out Felicity


FSharp.JsonApi is a library (not framework) that allows you to use F# to easily create and consume flexible, strongly typed web APIs following the JSON:API specification. There’s even an almost-production-ready API implementation sample to get you started on the right foot!

Core features:

  • Full support for sparse fieldsets and included resources
  • Support for loading included resources asynchronously on-demand, in parallel
  • Uses FSharp.JsonSkippable for strong typing of whether JSON properties are included or excluded
  • Plays very nicely with the robust error handling of FsToolkit.ErrorHandling, whether monadic or applicative (the latter works perfectly to return multiple JSON:API errors at once)
  • Lots of utilities to allow you to easily wire up your perfect domain snowflakes (single-case DU wrappers with smart constructors, Result-returning record field setters, etc.) with the raw resource attributes and relationships present in a JSON:API request
  • And much more

The focus is on server implementations, but it may also be useful when implementing clients (please get in touch!).

Production readiness

We use this library for at least three mission-critical production APIs. I have developed and tweaked it internally for around one and a half years before finally polishing it and publishing to NuGet. I’m not claiming it’s perfect, or even bug-free, but it’s battle-tested, and I have a vested interest in keeping this library working properly.

Installation

FSharp.JsonApi consists of three NuGet packages:

  • FSharp.JsonApi contains all the core stuff: JSON:API document models for serialization/deserialization, resource builders, parsing and validation of query parameters and documents, helpers for calling domain code based on a JSON:API request, etc. If you don’t use ASP.NET Core, you can easily use this library to build your own abstractions.
  • FSharp.JsonApi.AspNetCore contains lots of useful helpers and additional overloads for parsing and validating requests using ASP.NET Core’s HttpContext.
  • FSharp.JsonApi.Giraffe contains a few simple HttpHandlers that may be useful if using Giraffe.

Install all packages that are relevant for you. You can get away with installing only the highest-level package – e.g. FSharp.JsonApi.Giraffe – and have the rest installed automatically as transitive dependencies, but depending on your package manager, you might not be able to easily update the lower-level (transitive) packages (Paket is better at this than NuGet).

Contributing

Contributions and ideas are welcome! Please see Contributing.md for details.

Quick start

I highly recommend you check out the sample API in this repo, which is a simple but complete and almost-production-ready example API implementation. Open the main solution in VS, start at the topmost file, and read through the project in compilation order. There are lots of comments along the way to explain what’s going on.

As a very short introduction, I hope the steps below are useful, but bear in mind that they only skim the surface.

1. Define resources

Define each resource’s attributes and relationships using records, enums, and any relevant .NET attributes. The names will be used as-is; use double backticks for special names. Use option for nullable attributes. Use ToOne and ToMany for relationships. All attributes and relationships must be wrapped in Skippable (see FSharp.JsonSkippable).

type ArticleType =
  | personal = 0
  | commercial = 1

[<CLIMutable>]
type ArticleAttrs = {
  title: string Skippable
  articleType: ArticleType Skippable
  [<ReadOnly>] updated: DateTimeOffset option Skippable
}

[<CLIMutable>]
type ArticleRels = {
  [<NotNull; AllowedTypes("person")>]
  author: ToOne Skippable
  [<ReadOnly; AllowedTypes("comment")>]
  comments: ToMany Skippable
}

2. Define the resource discriminator and the JsonApiContext

The resource discriminator is a DU where each case is a Resource<'attrs, 'rels>. It’s used to provide FSharp.JsonApi with all relevant information about the resources, and allows for a nice and friendly syntax when parsing resources.

type ResourceDiscriminator =
  | [<ResourceName("article")>]
    Article of Resource<ArticleAttrs, ArticleRels>

The JsonApiContext is what actually contains all information about the resources, obtained from the resource discriminator. It is used to serialize, deserialize, create, parse, and validate documents. Define it once and use it everywhere.

let jsonApiCtx = JsonApiContext.create<ResourceDiscriminator>

3. Define the resource builders

The ResourceBuildContext is your friend when building attributes, relationships, links, and meta. It contains all information about sparse fieldsets, the current resource being built, its place in the include path, etc. and provides many helpers to get attributes, relationships, and related resources. Most ResourceBuildContext methods have many overloads for convenience; only some are used below.

module Article =

  let private getIdentifier (a: Article) =
    ResourceIdentifier.create TypeNames.article a.Id

  let private getAttributes (ctx: ResourceBuildContext) (a: Article) =
    {
      title = ctx.GetAttribute("title", a.Title)
      articleType = ctx.GetAttribute("articleType", a.Type, ArticleType.toApi)
      updated = ctx.GetAttribute("updated", a.Updated)
    }

  let private getRelationships baseUrl (ctx: ResourceBuildContext) (a: Article) =
    async {

      // Use Async.StartChild to fetch the included resources in parallel

      let! authorComp =
        ctx.IncludeToOne("author", a.Id, Db.Person.authorForArticle, Person.getBuilder baseUrl)
        |> Async.StartChild

      let! commentsComp =
        ctx.IncludeToMany("comments", a.Id, Db.Comment.allForArticle, Comment.getBuilder baseUrl)
        |> Async.StartChild

      // Actually wait for them to be fetched

      let! authorRelationship, authorBuilder = authorComp
      let! commentsRelationship, commentsBuilders = commentsComp

      // Return the relationships and the builders that will be
      // used to build the related resources

      let relationships = {
        author = authorRelationship
        comments = commentsRelationship
      }

      let builders = [
        yield! authorBuilder |> Option.toList
        yield! commentsBuilders
      ]

      return relationships, builders
    }

  // Here is the function to actually get the resource builder.
  // It must be cheap (do not eagerly evaluate attributes or resources)
  // and the two final parameters must be the ResourceBuildContext
  // and the domain object to be built.

  let getBuilder baseUrl (ctx: ResourceBuildContext) a =
    ResourceBuilder
      .Create(Article, getIdentifier a)
      .WithAttributes(fun () -> getAttributes ctx a)
      .WithRelationships(getRelationships baseUrl ctx a)
      // You can add links and meta, too; check out the sample API

Profit!

Build documents

You can now build JSON:API documents like so:

jsonApiCtx.BuildDocument(article, Article.getBuilder, ctx)

where ctx is the ASP.NET Core HttpContext. Your resource document will be built and all sparse fieldsets, included resources etc. will be handled automatically for you.

Receive documents

For example:

let result =
  jsonApiCtx
    .WithNoIdForPost()
    .Parse(Article, ctx)

This returns Async<Result<Resource<ArticleAttrs, ArticleRels>, RequestDocumentError list>>. See the sample API for more information on simple and robust error handling, and more deserialization/parsing/validation options.

Parse query parameters

Example of applicative error handling (using operators from FsToolkit.ErrorHandling):

ArticleSearchArgs.create
<!> Query.GetSingle("filter[title]", ctx)
<*> Query.GetBoundInt("page[offset]", 0, ctx, min=0)
<*> Query.GetBoundInt("page[limit]", 10, ctx, min=1)

Call domain logic

result {
  let (a: ArticleAttrs) = Resource.attributesOrDefault result
  let! article =
    Ok article
    |> set.Optional(Article.setTitle, Attribute.Get(a.title))
    |> set.Required(Article.setBody, Attribute.Require(a.body))
}

Return useful errors to API clients

Many functions in FSharp.JsonApi return Result<_, SomeError list> where SomeError is one of a few discriminated union types having cases that contain all the information you need to provide useful errors to the client in whatever manner you desire.

You can, for example, define your own DU for all errors returned through your API, and map FSharp.JsonApi’s errors to this type. You can then have a function that takes your error DU and returns a JSON:API error object. This allows you to easily take an FSharp.JsonApi error (or a list of errors) and produce a JSON:API error response. See ErrorHandling.fs as well as the error-related functions in the first part of HttpHandlers.fs in the sample API. (I basically copy-paste all of that whenever I create a new JSON:API.)

Never worry about null in requests

Nulls are only allowed for option-wrapped properties. Any other null in the response body will make JsonApiContext.Parse return errors you can use in an error response as described above.

You can also easily require that option-wrapped attributes be non-null for a specific request, giving you a syntactically concise way to get either the inner value of an Option or an error you can return to the API client. It can, for example, be easily chained along with other setters as shown above.

And more

Check out the sample API y’all 😉

Documentation

It would be nice to have complete API documentation, but I have no immediate plans to work on that.

In the meantime, I highly recommend you check out the sample API in this repo, which is a simple but complete and almost-production-ready example API implementation. Open the main solution in VS, start at the topmost file, and read through the project in compilation order. There are lots of comments along the way to explain what’s going on.

Release notes

2.1.1

  • Improved Attribute overload resolution

2.1.0

  • Added Attribute overloads for async parsers, plus a few more
  • Added Setter overloads accepting async arguments

2.0.3

  • Fixed overload resolution for Async<'a> overloads of ResourceBuildContext methods GetAttribute and GetExplicitAttribute

2.0.1

  • Breaking: Removed SimpleResource and related methods/extensions on JsonApiContext. Use Resource.attributesOrDefault and Resource.relationshipsOrDefault instead to get a (possibly default) attribute/relationship instance from a resource.
  • Breaking: Added RequestDocumentError.UnknownMainResourceType
  • Breaking: JsonApiContext.GetResource and the JsonApiContext.Parse overload returning a resource discriminator now return errors if the resource type is unknown. The signature of the former method is changed; the latter is a behaviour change only.
  • Breaking: Renamed Query to QueryParser, added static methods to create an instance with a query parameter map or HttpContext, and changed most methods to instance members that do not depend on a query parameter map or HttpContext
  • Breaking: JsonApiContext.create now throws on invalid attribute and relationship names. Use the new AllowIllegalNameAttribute to disable the check for specific attributes/relationships.
  • Added Setter type with helper methods to chain and lift normal "immutable setter" functions to accept parsed, possibly optional arguments, combining any errors
  • Added JsonApiContext methods RequireResource, WithAllReadOnly, ToDiscriminator, FromDiscriminator, and SerializeAndGetBytes
  • Added JsonApiContext overloads for ParseRequired, WithReadOnly, WithWriteOnly, and WithNotNull
  • Added async overloads for ResourceBuildContext methods GetAttribute and GetExplicitAttribute
  • Added an HttpContext.WriteJsonApiAsync extension overload accepting a byte array
  • Added new Giraffe HTTP handlers jsonApiETag, jsonApiETagWith, and jsonApiBytes
  • Fixed Uri.addQuery and Uri.setQuery not behaving correctly for multiple identical query keys and for query keys that only differ by case
  • Fixed validation when links collection is null (JSON: "links": null)
  • Fixed validation when a relationship is null (JSON: "relationships": {"myRel": null})
  • Serializes Uris to canonical format using Uri.ToString() instead of Uri.OriginalString which is normally used by Newtonsoft.Json. See dotnet/corefx#41679 and JamesNK/Newtonsoft.Json/2190.
  • Made order of included resources deterministic (needed to get stable hashes of response for ETag)

FSharp.JsonApi 1.4.1

  • Fixed Attribute.GetNonNull returning errors for skipped values

FSharp.JsonApi.AspNetCore 1.1.0

  • Add validate optional parameter to all JsonApiContext.Parse extensions
  • Add ParseRequired and ParseSimpleRequired extensions members for JsonApiContext

FSharp.JsonApi 1.4.0

  • Add ParseRequired and ParseSimpleRequired to JsonApiContext

FSharp.JsonApi 1.3.0

  • Add more Attribute overloads

FSharp.JsonApi 1.1.0

  • Add more Relationship overloads

FSharp.JsonApi 1.0.0, FSharp.JsonApi.AspNetCore 1.0.0, FSharp.JsonApi.Giraffe 1.0.0

  • Initial release