{"id":23613652,"url":"https://github.com/freckle/interpolated-data","last_synced_at":"2026-02-15T16:32:05.631Z","repository":{"id":187290838,"uuid":"676655618","full_name":"freckle/interpolated-data","owner":"freckle","description":null,"archived":false,"fork":false,"pushed_at":"2024-12-23T00:29:53.000Z","size":54,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":8,"default_branch":"main","last_synced_at":"2024-12-27T17:18:32.047Z","etag":null,"topics":["terraform-managed"],"latest_commit_sha":null,"homepage":"","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/freckle.png","metadata":{"files":{"readme":"README.lhs","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-08-09T17:41:57.000Z","updated_at":"2024-12-19T22:29:27.000Z","dependencies_parsed_at":"2024-12-12T20:31:59.339Z","dependency_job_id":"cecca585-f70d-44f7-ac0e-1b15369a094d","html_url":"https://github.com/freckle/interpolated-data","commit_stats":null,"previous_names":["freckle/interpolated-data"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/freckle%2Finterpolated-data","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/freckle%2Finterpolated-data/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/freckle%2Finterpolated-data/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/freckle%2Finterpolated-data/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/freckle","download_url":"https://codeload.github.com/freckle/interpolated-data/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239491352,"owners_count":19647811,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["terraform-managed"],"created_at":"2024-12-27T17:18:37.032Z","updated_at":"2026-02-15T16:32:05.625Z","avatar_url":"https://github.com/freckle.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# interpolated-data\n\n[![Hackage](https://img.shields.io/hackage/v/interpolated-data.svg?style=flat)](https://hackage.haskell.org/package/interpolated-data)\n[![Stackage Nightly](http://stackage.org/package/interpolated-data/badge/nightly)](http://stackage.org/nightly/package/interpolated-data)\n[![Stackage LTS](http://stackage.org/package/interpolated-data/badge/lts)](http://stackage.org/lts/package/interpolated-data)\n[![CI](https://github.com/freckle/interpolated-data/actions/workflows/ci.yml/badge.svg)](https://github.com/freckle/interpolated-data/actions/workflows/ci.yml)\n\n:warning: **Work in progress**\n\nThere are [many, many interpolation libraries][hackage-search] on Hackage, but\nthey are exclusively:\n\n[hackage-search]: https://hackage.haskell.org/packages/search?terms=interpolate\n\n1. For building interpolated strings at compile-time, through quasi-quotation\n2. For building only string (or string-like) types\n\nThis library is different. It aims to better support cases where:\n\n1. The interpolated data may be provided at runtime, such as from a\n   configuration file or web request\n2. The interpolated data is structured, such as a record of fields of\n   interpolated data\n3. You can state statically in the types what interpolation keys (called a\n   \"context\") will be available, so we can validate the runtime input\n\nLet's build a motivating example.\n\n\u003c!--\n\n```haskell\n{-# LANGUAGE DeriveAnyClass #-}\n{-# LANGUAGE DeriveGeneric #-}\n{-# LANGUAGE DuplicateRecordFields #-}\n{-# LANGUAGE GeneralizedNewtypeDeriving #-}\n{-# LANGUAGE TypeOperators #-}\n\nmodule Main (main) where\n\nimport Prelude\n\nimport Control.Monad (void)\nimport Data.Aeson (FromJSON)\nimport Data.Bifunctor (first)\nimport qualified Data.Set as Set\nimport Data.String (IsString(..))\nimport Data.Text\nimport qualified Data.Yaml as Yaml\nimport GHC.Generics (Generic)\nimport Test.Hspec\n\n```\n--\u003e\n\n\n```haskell\nimport Data.Interpolated\n```\n\nLet's say you're building some deployment tooling. Applications can describe\nsome settings about how they're deployed in a configuration file. Deploys occur\nfor some *App* and in an *Environment*. Therefore, we want to support\ninterpolating those (and only those) runtime values into such settings.\n\nAn example configuration would look like:\n\n```yaml\nstackName: \"{env}-{app}\"\n\nrepository:\n  registry: \"my-registry\"\n  name: \"apps/{app}\"\n\ndockerfile: \"./{app}.dockerfile\"\n```\n\nThe `stackName` key supports the `env` and `app` interpolations, while the\nfields of `repository` support only `app` (deployment images are reused from\n`env` to `env`, of course). One key feature of our library is making that safe\nthrough a combination of compile- and runtime validations.\n\n## `InterpolationContext`\n\nTo supply values for interpolations, we have to define types that are instances\nof `InterpolationContext`:\n\n\u003c!--\n```haskell\nnewtype AppName = AppName\n  { unAppName :: Text\n  }\n  deriving newtype IsString\n\nnewtype Environment = Environment\n  { unEnvironment :: Text\n  }\n  deriving newtype IsString\n```\n--\u003e\n\n```haskell\ndata AppEnvContext = AppEnvContext\n  { app :: AppName\n  , env :: Environment\n  }\n\ninstance InterpolationContext AppEnvContext where\n```\n\nA valid `InterpolationContext` can say statically what keys it provides. It does\nthis by defining `interpolationVariables :: Proxy context -\u003e Set Text`:\n\n```haskell\n  interpolationVariables _ = Set.fromList [\"app\", \"env\"]\n```\n\nThis function operates on `Proxy` so that we can use it at construction-time,\nbefore we have any supplied context, to verify we're constructing an\ninterpolation that can indeed be satisfied.\n\nAnd when it comes time to provide the values, that would be through the\n`interpolationValues :: context -\u003e [(Text, Text)]` member:\n\n```haskell\n  interpolationValues AppEnvContext {..} =\n    [ (\"app\", unAppName app)\n    , (\"env\", unEnvironment env)\n    ]\n```\n\nAnd to satisfy our hypothetical use-case, we'll make a second type for the\ncontext where only `app` is available:\n\n```haskell\nnewtype AppContext = AppContext\n  { app :: AppName\n  }\n\ninstance InterpolationContext AppContext where\n  interpolationVariables _ = Set.singleton \"app\"\n  interpolationValues AppContext {..} = [(\"app\", unAppName app)]\n```\n\n## `ToInterpolated`\n\nThe `ToInterpolated` class is used for input values that contain interpolations.\nIf using a basic string-like type (e.g. `Text`) we provide that instance.\n`GeneralizedNewtypeDeriving` can be used to supply an instance for your own\nstring-like types as well:\n\n```haskell\nnewtype StackName = StackName Text\n  deriving stock (Eq, Show)\n  deriving newtype (FromJSON, ToInterpolated)\n\nnewtype EcrRegistry = EcrRegistry Text\n  deriving stock (Eq, Show)\n  deriving newtype (FromJSON, ToInterpolated)\n\nnewtype Dockerfile = Dockerfile FilePath\n  deriving stock (Eq, Show)\n  deriving newtype (FromJSON, ToInterpolated)\n```\n\n`EcrRepository` is an example of structured data that supports interpolation.\n\n```haskell\ndata EcrRepository = EcrRepository\n  { registry :: EcrRegistry\n  , name :: Text\n  }\n  deriving stock (Eq, Show, Generic)\n  deriving anyclass FromJSON\n```\n\nSince it's not a string-like type, we'll need to create an instance by hand:\n\n```haskell\ninstance ToInterpolated EcrRepository where\n```\n\nThe `parseVariables :: a -\u003e Either String (Set Text)` member says how to get all\nin-use interpolation variables from the given value. In this case, that means to\ntake all the keys across its two fields by the same function:\n\n```haskell\n  parseVariables EcrRepository {..} = (\u003c\u003e)\n    \u003c$\u003e parseVariables registry\n    \u003c*\u003e parseVariables name\n```\n\nThe second member is `runReplacement :: (Text -\u003e Text) -\u003e a -\u003e a` and it says\nhow to replace the interpolations across the structure. In this case, that means\nto replace them in each field by the same mechanism:\n\n```haskell\n  runReplacement f er = er\n    { registry = runReplacement f $ registry er\n    , name = runReplacement f $ name er\n    }\n```\n\n## `InterpolatedBy`\n\nDefining a type as ``a `InterpolatedBy` context`` is how we ensure safe\nconstruction (and use). We validate that the `context` we specify supplies the\nvariables `a` uses by calling the type-class functions described above.\n\n```haskell\ndata Settings = Settings\n  { stackName :: StackName `InterpolatedBy` AppEnvContext\n  , repository :: EcrRepository `InterpolatedBy` AppContext\n  , dockerfile :: Maybe (Dockerfile `InterpolatedBy` AppContext)\n  }\n  deriving stock (Show, Generic)\n  deriving anyclass FromJSON\n```\n\nConveniently, construction via generic `FromJSON` will include this validation.\nTherefore, when we parse our user's configuration, they'll receive an\ninformative error:\n\n```haskell\nspec1 :: Spec\nspec1 = do\n  it \"fails informatively\" $ do\n    let\n      result =\n        void\n          $ first Yaml.prettyPrintParseException\n          $ Yaml.decodeEither' @Settings $ mconcat\n            [ \"stackName: '{app}-{env}-{region}'\\n\"\n            , \"repository:\\n\"\n            , \"  registry: 'hub.docker.io'\\n\"\n            , \"  name: 'apps/{app}'\\n\"\n            ]\n\n    result `shouldBe` Left (mconcat\n      [ \"Aeson exception:\\n\"\n      , \"Error in $.stackName: Interpolation uses the variable region, \"\n      , \"which is not available in the provided context (app, env)\"\n      ])\n```\n\n## Type-safety\n\nAs authors of this deployment tool, we will be required to interpolate these\nvalues to get what we need to perform our logic. Since the values are tagged\nwith `context`, if we make a mistake and provide the wrong one, that is a\ntype-error. Providing the right context compiles and works as expected:\n\n```haskell\nspec2 :: Spec\nspec2 = do\n  it \"interpolates throughout\" $ do\n    let\n      context1 = AppEnvContext { app = \"my-app\" , env=\"prod\" }\n      context2 = AppContext { app=\"my-app\" }\n      result =\n        first Yaml.prettyPrintParseException\n          $ Yaml.decodeEither' @Settings $ mconcat\n            [ \"stackName: '{app}-{env}'\\n\"\n            , \"repository:\\n\"\n            , \"  registry: 'hub.docker.io'\\n\"\n            , \"  name: 'apps/{app}'\\n\"\n            ]\n\n    -- Incorrectly using context2 here would fail to compile\n    (interpolate context1 . stackName \u003c$\u003e result)\n      `shouldBe` Right (StackName \"my-app-prod\")\n\n    (interpolate context2 . repository \u003c$\u003e result)\n      `shouldBe` Right (EcrRepository\n        { registry = EcrRegistry \"hub.docker.io\"\n        , name = \"apps/my-app\"\n        })\n```\n\n\u003c!--\n```haskell\nmain :: IO ()\nmain = hspec $ do\n  spec1\n  spec2\n```\n--\u003e\n\n---\n\n[LICENSE](./LICENSE) | [CHANGELOG](./CHANGELOG.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffreckle%2Finterpolated-data","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffreckle%2Finterpolated-data","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffreckle%2Finterpolated-data/lists"}