Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/cmeeren/fsharp.jsonapi
Use F# to create and consume flexible, strongly typed web APIs following the JSON:API specification
https://github.com/cmeeren/fsharp.jsonapi
api api-client api-rest api-server asp-net-core fsharp giraffe json-api jsonapi rest rest-api web-api
Last synced: 3 months ago
JSON representation
Use F# to create and consume flexible, strongly typed web APIs following the JSON:API specification
- Host: GitHub
- URL: https://github.com/cmeeren/fsharp.jsonapi
- Owner: cmeeren
- License: mit
- Created: 2019-09-04T09:27:30.000Z (over 5 years ago)
- Default Branch: master
- Last Pushed: 2020-02-17T13:09:34.000Z (almost 5 years ago)
- Last Synced: 2024-05-29T06:57:51.594Z (8 months ago)
- Topics: api, api-client, api-rest, api-server, asp-net-core, fsharp, giraffe, json-api, jsonapi, rest, rest-api, web-api
- Language: F#
- Homepage:
- Size: 545 KB
- Stars: 20
- Watchers: 6
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: .github/CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
FSharp.JsonApi
==============---
### No longer actively maintained, please check out [Felicity](https://github.com/cmeeren/Felicity)
---
FSharp.JsonApi is a [library (not framework)](http://tomasp.net/blog/2015/library-frameworks/) that allows you to use F# to easily create and consume flexible, strongly typed web APIs following the [JSON:API specification](https://jsonapi.org/). 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](https://github.com/cmeeren/FSharp.JsonSkippable) for strong typing of whether JSON properties are included or excluded
* Plays very nicely with the robust error handling of [FsToolkit.ErrorHandling](https://github.com/demystifyfp/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 moreThe 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 `HttpHandler`s that may be useful if using [Giraffe](https://github.com/giraffe-fsharp/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](https://github.com/cmeeren/FSharp.JsonApi/blob/master/.github/CONTRIBUTING.md) for details.
Quick start
-----------I highly recommend you check out the [sample API](https://github.com/cmeeren/FSharp.JsonApi/tree/master/src/FSharp.JsonApi.SampleApp) 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](https://github.com/cmeeren/FSharp.JsonSkippable/)).
```f#
type ArticleType =
| personal = 0
| commercial = 1[]
type ArticleAttrs = {
title: string Skippable
articleType: ArticleType Skippable
[] updated: DateTimeOffset option Skippable
}[]
type ArticleRels = {
[]
author: ToOne Skippable
[]
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.
```f#
type ResourceDiscriminator =
| []
Article of Resource
```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.
```f#
let jsonApiCtx = JsonApiContext.create
```### 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.
```f#
module Article =let private getIdentifier (a: Article) =
ResourceIdentifier.create TypeNames.article a.Idlet 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.StartChildlet! 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 resourceslet 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:
```f#
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:
```f#
let result =
jsonApiCtx
.WithNoIdForPost()
.Parse(Article, ctx)
```This returns `Async, RequestDocumentError list>>`. See the [sample API](https://github.com/cmeeren/FSharp.JsonApi/tree/master/src/FSharp.JsonApi.SampleApp) 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](https://github.com/demystifyfp/FsToolkit.ErrorHandling/)):
```f#
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
```f#
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](https://github.com/cmeeren/FSharp.JsonApi/tree/master/src/FSharp.JsonApi.SampleApp) 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 `Uri`s to canonical format using `Uri.ToString()` instead of `Uri.OriginalString` which is normally used by Newtonsoft.Json. See [dotnet/corefx#41679](https://github.com/dotnet/corefx/issues/41679) and [JamesNK/Newtonsoft.Json/2190](https://github.com/JamesNK/Newtonsoft.Json/issues/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