https://github.com/freckle/interpolated-data
https://github.com/freckle/interpolated-data
terraform-managed
Last synced: 3 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/freckle/interpolated-data
- Owner: freckle
- License: mit
- Created: 2023-08-09T17:41:57.000Z (almost 2 years ago)
- Default Branch: main
- Last Pushed: 2024-12-23T00:29:53.000Z (5 months ago)
- Last Synced: 2024-12-27T17:18:32.047Z (5 months ago)
- Topics: terraform-managed
- Language: Haskell
- Homepage:
- Size: 52.7 KB
- Stars: 0
- Watchers: 8
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.lhs
- License: LICENSE
Awesome Lists containing this project
README
# interpolated-data
[](https://hackage.haskell.org/package/interpolated-data)
[](http://stackage.org/nightly/package/interpolated-data)
[](http://stackage.org/lts/package/interpolated-data)
[](https://github.com/freckle/interpolated-data/actions/workflows/ci.yml):warning: **Work in progress**
There are [many, many interpolation libraries][hackage-search] on Hackage, but
they are exclusively:[hackage-search]: https://hackage.haskell.org/packages/search?terms=interpolate
1. For building interpolated strings at compile-time, through quasi-quotation
2. For building only string (or string-like) typesThis library is different. It aims to better support cases where:
1. The interpolated data may be provided at runtime, such as from a
configuration file or web request
2. The interpolated data is structured, such as a record of fields of
interpolated data
3. You can state statically in the types what interpolation keys (called a
"context") will be available, so we can validate the runtime inputLet's build a motivating example.
```haskell
import Data.Interpolated
```Let's say you're building some deployment tooling. Applications can describe
some settings about how they're deployed in a configuration file. Deploys occur
for some *App* and in an *Environment*. Therefore, we want to support
interpolating those (and only those) runtime values into such settings.An example configuration would look like:
```yaml
stackName: "{env}-{app}"repository:
registry: "my-registry"
name: "apps/{app}"dockerfile: "./{app}.dockerfile"
```The `stackName` key supports the `env` and `app` interpolations, while the
fields of `repository` support only `app` (deployment images are reused from
`env` to `env`, of course). One key feature of our library is making that safe
through a combination of compile- and runtime validations.## `InterpolationContext`
To supply values for interpolations, we have to define types that are instances
of `InterpolationContext`:```haskell
data AppEnvContext = AppEnvContext
{ app :: AppName
, env :: Environment
}instance InterpolationContext AppEnvContext where
```A valid `InterpolationContext` can say statically what keys it provides. It does
this by defining `interpolationVariables :: Proxy context -> Set Text`:```haskell
interpolationVariables _ = Set.fromList ["app", "env"]
```This function operates on `Proxy` so that we can use it at construction-time,
before we have any supplied context, to verify we're constructing an
interpolation that can indeed be satisfied.And when it comes time to provide the values, that would be through the
`interpolationValues :: context -> [(Text, Text)]` member:```haskell
interpolationValues AppEnvContext {..} =
[ ("app", unAppName app)
, ("env", unEnvironment env)
]
```And to satisfy our hypothetical use-case, we'll make a second type for the
context where only `app` is available:```haskell
newtype AppContext = AppContext
{ app :: AppName
}instance InterpolationContext AppContext where
interpolationVariables _ = Set.singleton "app"
interpolationValues AppContext {..} = [("app", unAppName app)]
```## `ToInterpolated`
The `ToInterpolated` class is used for input values that contain interpolations.
If using a basic string-like type (e.g. `Text`) we provide that instance.
`GeneralizedNewtypeDeriving` can be used to supply an instance for your own
string-like types as well:```haskell
newtype StackName = StackName Text
deriving stock (Eq, Show)
deriving newtype (FromJSON, ToInterpolated)newtype EcrRegistry = EcrRegistry Text
deriving stock (Eq, Show)
deriving newtype (FromJSON, ToInterpolated)newtype Dockerfile = Dockerfile FilePath
deriving stock (Eq, Show)
deriving newtype (FromJSON, ToInterpolated)
````EcrRepository` is an example of structured data that supports interpolation.
```haskell
data EcrRepository = EcrRepository
{ registry :: EcrRegistry
, name :: Text
}
deriving stock (Eq, Show, Generic)
deriving anyclass FromJSON
```Since it's not a string-like type, we'll need to create an instance by hand:
```haskell
instance ToInterpolated EcrRepository where
```The `parseVariables :: a -> Either String (Set Text)` member says how to get all
in-use interpolation variables from the given value. In this case, that means to
take all the keys across its two fields by the same function:```haskell
parseVariables EcrRepository {..} = (<>)
<$> parseVariables registry
<*> parseVariables name
```The second member is `runReplacement :: (Text -> Text) -> a -> a` and it says
how to replace the interpolations across the structure. In this case, that means
to replace them in each field by the same mechanism:```haskell
runReplacement f er = er
{ registry = runReplacement f $ registry er
, name = runReplacement f $ name er
}
```## `InterpolatedBy`
Defining a type as ``a `InterpolatedBy` context`` is how we ensure safe
construction (and use). We validate that the `context` we specify supplies the
variables `a` uses by calling the type-class functions described above.```haskell
data Settings = Settings
{ stackName :: StackName `InterpolatedBy` AppEnvContext
, repository :: EcrRepository `InterpolatedBy` AppContext
, dockerfile :: Maybe (Dockerfile `InterpolatedBy` AppContext)
}
deriving stock (Show, Generic)
deriving anyclass FromJSON
```Conveniently, construction via generic `FromJSON` will include this validation.
Therefore, when we parse our user's configuration, they'll receive an
informative error:```haskell
spec1 :: Spec
spec1 = do
it "fails informatively" $ do
let
result =
void
$ first Yaml.prettyPrintParseException
$ Yaml.decodeEither' @Settings $ mconcat
[ "stackName: '{app}-{env}-{region}'\n"
, "repository:\n"
, " registry: 'hub.docker.io'\n"
, " name: 'apps/{app}'\n"
]result `shouldBe` Left (mconcat
[ "Aeson exception:\n"
, "Error in $.stackName: Interpolation uses the variable region, "
, "which is not available in the provided context (app, env)"
])
```## Type-safety
As authors of this deployment tool, we will be required to interpolate these
values to get what we need to perform our logic. Since the values are tagged
with `context`, if we make a mistake and provide the wrong one, that is a
type-error. Providing the right context compiles and works as expected:```haskell
spec2 :: Spec
spec2 = do
it "interpolates throughout" $ do
let
context1 = AppEnvContext { app = "my-app" , env="prod" }
context2 = AppContext { app="my-app" }
result =
first Yaml.prettyPrintParseException
$ Yaml.decodeEither' @Settings $ mconcat
[ "stackName: '{app}-{env}'\n"
, "repository:\n"
, " registry: 'hub.docker.io'\n"
, " name: 'apps/{app}'\n"
]-- Incorrectly using context2 here would fail to compile
(interpolate context1 . stackName <$> result)
`shouldBe` Right (StackName "my-app-prod")(interpolate context2 . repository <$> result)
`shouldBe` Right (EcrRepository
{ registry = EcrRegistry "hub.docker.io"
, name = "apps/my-app"
})
```---
[LICENSE](./LICENSE) | [CHANGELOG](./CHANGELOG.md)