kowainik / tomland

🏝 Bidirectional TOML serialization
https://kowainik.github.io/posts/2019-01-14-tomland
Mozilla Public License 2.0
120 stars 39 forks source link

Top-level Tables #404

Open pnotequalnp opened 2 years ago

pnotequalnp commented 2 years ago

This TOML

[foo]
field = "bar"

[baz]
field = "quux"

is equivalent to this JSON

{
  "foo": {
    "field": "bar"
  },
  "baz": {
    "field": "quux"
  }
}

This data maps cleanly to this Haskell representation

type Data = Map String Entry

data Entry = Entry { field :: String }

However, I cannot figure out how to parse this shape of data with tomland. I threw together a quick repo to demonstrate my attempts and problems. The exact errors are in the CI run. I'm not sure if I'm misunderstanding how the library is supposed to be used, or if this sort of data is simply not able to be parsed by it currently.

Additionally, Map1 fails to decode the data that it just encoded. I'm not sure why that is, or if it's something I'm missing or a bug in the library. Map3, which should work from my understanding, also has curious behavior in regard to parsing equivalent TOML files differently (in particular, one being an error with the -Exact decoding variants).

Qqwy commented 2 years ago

If I understand it correctly, you need to use the BiMap related functionality rather than the preexisting TomlCodec related functionality here, because a TomlCodec requires looking at a specific key (or multiple specific keys). c.f. https://hackage.haskell.org/package/tomland-1.3.3.1/docs/Toml-Codec-Combinator-Common.html#v:match. You can build your own alternative to match that does not look at a key however. I think that is the way to go here.

In the TOML datastructure itself, the root datastructures are introduced by the tree fields 'tomlPairs', 'tomlTables' and 'tomlTableArrays'. You are able to read/write these in a custom function that takes a bimap as input an creates a TomlCodec as output.

thomasjm commented 1 year ago

I'm having trouble coming up with the solution described here, could anyone provide an example?

I've only been able to parse a file like this by calling parse, then manually iterating over the tomlTables and calling runTomlCodec on each individual value. Which leaves something to be desired in terms of validation etc.

acowley commented 1 year ago

I had trouble with this, too. I followed @thomasjm's advice, and ended up with these helpers to get me unblocked. Pasting here as a starting point in case anyone else runs into a similar situation.

keyText :: Toml.Key -> T.Text
keyText = F.fold . cleanup . map Toml.unPiece . F.toList . Toml.unKey
  where
    -- Top-level table names are parsed as @"name" :|
    -- ["name"]@. Remove that duplication here.
    cleanup [x, y] | x == y = [x]
    cleanup x = x

-- | Parse a TOML file that is a top-level table whose values are all
-- the same type. The @tomland@ codec API is centered around starting
-- with a key, but a top-level table does not have a key, so we must
-- use the lower level 'Toml.parse' and 'Toml.tomlTables' before
-- repeatedly applying the provided 'Toml.TomlCodec'.
parseFileOf :: forall a. Toml.TomlCodec a -> T.Text -> Either [T.Text] [(T.Text, a)]
parseFileOf codec =
    first (map Toml.prettyTomlDecodeError)
        . validationToEither
        . traverse (uncurry go)
        . Toml.toList
        . Toml.tomlTables
        . either (error . show) id
        . Toml.parse
  where
    go :: Toml.Key -> Toml.TOML -> Validation [Toml.TomlDecodeError] (T.Text, a)
    go k v = (keyText k,) <$> Toml.runTomlCodec codec v