packages feed

knit-0.1.0.0: README.md

# knit

[![CircleCI](https://circleci.com/gh/pkamenarsky/knit.svg?style=svg)](https://circleci.com/gh/pkamenarsky/knit)

`knit` ties the knot on data structures that reference each other by unique keys. Above all it aims to be easy to use - boilerplate is kept to a minimum and its API is as simple as it gets.

## Example

```haskell
data Person model m = Person
  { name        :: Id model m String
  , loves       :: ForeignId model m "persons" "name" --
  , isPresident :: Bool                               --
  } deriving (Generic, KnitRecord Model)              --
                                                      -- 
data Model m = Model                                  --
  { persons :: Table Model m Person -- <----------------
  } deriving (Generic, KnitTables)
```

Let's break that down: when defining a domain type, like `Person`, we'll need two additional type parameters that will determine the final shape of that type: the `model` `Person` belongs to (it may belong to multiple models), and its "mode" (`m`) - whether it's *resolved* or *unresolved*. Additionally, we need to derive `KnitRecord` for every domain type, supplying it with a concrete `model` type.

`Id model m t` will define a key this type is referenced by (multiple keys are possible).

`ForeignId` is where the magic happens - in addition to the two generic parameters from above it takes a "table" name (which is just a field in the `model`) and a field name in the referenced domain type; the final type of the `ForeignId` field (both resolved and unresolved) can then be inferred from this information alone!

To define a `model`, wrap each domain type with a `Table` and autoderive the `KnitTables` typeclass.

Let's take a look:

```haskell
alice :: Person Model 'Unresolved
alice = Person
  { name        = Id "Alice
  , loves       = ForeignId "Bob"  -- this must be a String, since Model.persons.name is a String!
  , isPresident = False
  }

bob :: Person Model 'Unresolved
bob = Person
  { name        = Id "Bob"
  , loves       = ForeignId "Alice"
  , isPresident = False
  }

model :: Model 'Unresolved
model = Model
  { persons = [alice, bob]  -- `Table` is just a regular list
  }
```

So far so good. Resolving an unresolved model is just a matter of calling `knit`:

```haskell
knitModel :: Model Resolved
knitModel = case knit model of
  Right resolved -> resolved
  Left e -> error (show e)
```

(`knit` may fail due to invalid or duplicate keys). If all goes well, we'll get the following *resolved* model, if we were to do it by hand:

```haskell
manualAlice :: Person Model 'Resolved
manualAlice = Person
  { name        = "Alice"
  , loves       = Lazy manualBob
  , isPresident = False
  }

manualBob :: Person Model 'Resolved
manualBob = Person
  { name        = "Bob"
  , loves       = Lazy manualAlice
  , isPresident = False
  }

manualModel :: Model 'Resolved
manualModel = Model
  { persons = [manualAlice, manualBob]
  }
```

`Lazy` is just a simple wrapper with a `get` field. Simplified it's just:

```
data Lazy a = { get :: a }
```

And here it is, a nicely knit model:

```haskell
name $ get $ loves (persons knitModel !! 0) -- "Bob"
```

The `test` directory contains more examples, with multiple domain types.

## Cascading deletes

By supplying a `Remove` key instead the regular `Id` a record is marked for deletion:

```
alice :: Person Model 'Unresolved
alice = Person
  { name        = Remove "Alice  -- mark the record for deletion
  , loves       = ForeignId "Bob"
  , isPresident = False
  }
```

This will remove the record from the resolved result, as well as *all other records that depend transitively on it*. Invalid keys (i.e. `ForeignId`s that reference non-existent `Id`s) will still throw an error when `knit`-ting a model.