Documentation. Removed Foreign Code. No longer support YAML 1.1. Extra quote in YAML output fixed.

This commit is contained in:
Dominick Gendill 2017-05-20 15:32:55 -06:00
parent 75083ae70f
commit a03888dc97
10 changed files with 294 additions and 183 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
/.psci*
/src/.webpack.js
.pulp-cache/
.psc-ide-port

220
README.md
View File

@ -1,129 +1,127 @@
# purescript-yaml
## Install
This repo depends on js-yaml, so you'll need to install js-yaml in your
project. It is not compatible with YAML 1.1.
```
npm install js-yaml@^3.4.6
```
Then, if you're using bower, add the this repo as a project dependency, e.g.
```
bower install git://github.com/dgendill/purescript-yaml#1.0.0
```
Or you can manually add the github repo as a project dependency, e.g.
```
"dependencies": {
"purescript-yaml" : git://github.com/dgendill/purescript-yaml#tagOrBranch
}
```
## YAML to Data Type Usage
Assuming we have the following Point data type and yaml...
```purescript
data Point = Point Int Int
data Mobility
= Fix
| Flex
data GeoObject = GeoObject
{ name :: String
, scale :: Number
, points :: Array Point
, mobility :: Mobility
, coverage :: Number
}
```
## Decode YAML
Write functions to read your data from foreign values.
```purescript
readPoint :: Foreign -> F Point
readPoint value = do
x <- readInt =<< readProp "X" value
y <- readInt =<< readProp "Y" value
pure $ Point x y
readMobility :: Foreign -> F Mobility
readMobility value = do
mob <- readString value
case mob of
"Fix" -> pure Fix
"Flex" -> pure Flex
_ -> fail $ JSONError "Mobility must be either Flex or Fix"
readGeoObject :: Foreign -> F GeoObject
readGeoObject value = do
name <- readString =<< readProp "Name" value
scale <- readNumber =<< readProp "Scale" value
points <- traverse readPoint =<< readArray =<< readProp "Points" value
mobility <- readMobility =<< readProp "Mobility" value
coverage <- readNumber =<< readProp "Coverage" value
pure $ GeoObject { name, scale, points, mobility, coverage }
```
Read the YAML into your data structures.
```purescript
yamlInput :: String
yamlInput = """
- Name: House
Scale: 9.5
Points:
- X: 10
Y: 10
- X: 20
Y: 10
- X: 5
Y: 5
Mobility: Fix
Coverage: 10
- Name: Tree
Scale: 1
Points:
- X: 1
Y: 1
- X: 2
Y: 2
- X: 0
Y: 0
Mobility: Fix
Coverage: 10
yamlPoint :: String
yamlPoint = """
X: 1
Y: 1
"""
let decoded =
(parseYAML yamlInput) >>=
readArray >>=
traverse readGeoObject
```
## Encode YAML
We can read a `Point` from the yaml by convertion the YAML into JSON
and then using [purescript-argonaut](https://github.com/purescript-contrib/purescript-argonaut)'s encoding functionality to get the
type we need (specifically, [purescript-argonaut-codecs](https://github.com/purescript-contrib/purescript-argonaut-codecs)
functionality).
```
getPoint :: Either String Point
getPoint = case runExcept $ parseYAMLToJson yamlPoint of
Left err -> Left "Could not parse yaml"
Right json -> decodeJson json
instance pointJson :: DecodeJson Point where
decodeJson s = do
obj <- maybe (Left "Point is not an object.") Right (toObject s)
x <- getField obj "X"
y <- getField obj "Y"
pure $ Point x y
```
## Data Type to YAML Usage
YAML is represented with the following data type.
```
data YValue
= YObject (M.Map String YValue)
| YArray (Array YValue)
| YString String
| YNumber Number
| YInt Int
| YBoolean Boolean
| YNull
```
To convert data into YValue, create instances of the ToYAML class for your
data types.
```purescript
class ToYAML a where
toYAML :: a -> YValue
```
For example to take a `Point` to `YValue`
```purescript
import Data.YAML.Foreign.Encode (object, entry, class ToYAML)
instance pointToYAML :: ToYAML Point where
toYAML (Point x y) =
object
[ "X" := x
, "Y" := y
]
instance mobilityToYAML :: ToYAML Mobility where
toYAML Fix = toYAML "Fix"
toYAML Flex = toYAML "Flex"
instance archiObjectToYAML :: ToYAML GeoObject where
toYAML (GeoObject o) =
object
[ "Name" := o.name
, "Scale" := o.scale
, "Points" := o.points
, "Mobility" := o.mobility
, "Coverage" := o.coverage
[ "X" `entry` x
, "Y" `entry` y
]
```
You can find helper functions for converting basic types into `YValue`
in the Data.YAML.Foreign.Encode module.
Finally, if you want to convert YValue into a String, you can use the
`printYAML` function from Data.YAML.Foreign.Encode.
```purescript
data :: Array GeoObject
data =
[ GeoObject
{ coverage: 10.0
, mobility: Fix
, name: "House"
, points: [ Point 10 10, Point 20 10, Point 5 5 ]
, scale: 9.5
}
, GeoObject
{ coverage: 10.0
, mobility: Fix
, name: "Tree"
, points: [ Point 1 1, Point 2 2, Point 0 0 ]
, scale: 1.0
}
]
encoded = printYAML data
printYAML :: forall a. (ToYAML a) => a -> String
```
## Summary
Using the previous code and the type classes we defined earlier, we can go
full circle from a YAML string to a PureScript Data Type and back to a YAML string.
```
fullCircle :: String -> Either String String
fullCircle yamlString = (readPoint yamlString) >>= pure <<< printYAML
```
## Contributing
Check out the repo and make changes. You can run/add tests by running pulp test.
```
bower install
npm install
pulp test
```

View File

@ -8,7 +8,9 @@
"**/.*",
"node_modules",
"bower_components",
"output"
"output",
"test",
"docs"
],
"dependencies": {
"js-yaml": "^3.4.6",
@ -20,6 +22,7 @@
},
"devDependencies": {
"purescript-console": "^3.0.0",
"purescript-spec": "^1.0.0"
"purescript-spec": "^1.0.0",
"purescript-argonaut-codecs": "^3.0.1"
}
}

View File

@ -0,0 +1,19 @@
## Module Data.YAML.Foreign.Decode
#### `parseYAMLToJson`
``` purescript
parseYAMLToJson :: String -> F Json
```
Attempt to parse a YAML string, returning the result as Json
#### `readYAMLGeneric`
``` purescript
readYAMLGeneric :: forall a rep. Generic a rep => GenericDecode rep => Options -> String -> F a
```
Automatically generate a YAML parser for your data from a generic instance.

View File

@ -0,0 +1,66 @@
## Module Data.YAML.Foreign.Encode
#### `YValue`
``` purescript
data YValue
```
##### Instances
``` purescript
Show YValue
Eq YValue
```
#### `ToYAML`
``` purescript
class ToYAML a where
toYAML :: a -> YValue
```
##### Instances
``` purescript
(ToYAML a) => ToYAML (StrMap a)
(ToYAML a) => ToYAML (Map String a)
ToYAML Boolean
ToYAML Int
ToYAML Number
ToYAML String
(ToYAML a) => ToYAML (Array a)
(ToYAML a) => ToYAML (Maybe a)
```
#### `entry`
``` purescript
entry :: forall a. ToYAML a => String -> a -> Pair
```
Helper function to create a key-value tuple for a YAML object.
`name = "Name" := "This is the name"`
#### `(:=)`
``` purescript
infixl 4 entry as :=
```
#### `object`
``` purescript
object :: Array Pair -> YValue
```
Helper function to create a YAML object.
`obj = object [ "Name" := "This is the name", "Size" := 1.5 ]`
#### `printYAML`
``` purescript
printYAML :: forall a. ToYAML a => a -> String
```

View File

@ -1,4 +1,7 @@
module Data.YAML.Foreign.Decode (parseYAML, readYAMLGeneric, parseYAMLToJson) where
module Data.YAML.Foreign.Decode (
readYAMLGeneric,
parseYAMLToJson
) where
import Data.Foreign (F, Foreign, ForeignError(..), fail)
import Data.Foreign.Generic (genericDecode)

View File

@ -1,23 +1,18 @@
"use strict";
// module Data.YAML.Foreign.Encode
var yaml = require('js-yaml');
exports.jsNull = null;
exports.objToHash =
function(valueToYAMLImpl, fst, snd, obj)
{
var hash = {};
for(var i = 0; i < obj.length; i++) {
hash[fst(obj[i])] = valueToYAMLImpl(snd(obj[i]));
}
return hash;
};
exports.objToHash = function(valueToYAMLImpl, fst, snd, obj) {
var hash = {};
for(var i = 0; i < obj.length; i++) {
hash[fst(obj[i])] = valueToYAMLImpl(snd(obj[i]));
}
return hash;
};
exports.toYAMLImpl =
function(a)
{
return yaml.safeDump(a);
}
exports.toYAMLImpl = function(a) {
// noCompatMode does not support YAML 1.1
return yaml.safeDump(a, {noCompatMode : true});
}

View File

@ -1,4 +1,11 @@
module Data.YAML.Foreign.Encode where
module Data.YAML.Foreign.Encode (
YValue,
class ToYAML,
toYAML,
entry, (:=),
object,
printYAML
) where
import Data.Map as M
import Data.Map (Map)

View File

@ -1,11 +1,13 @@
module Test.Instances where
import Prelude (class Eq, class Show, bind, pure, ($), (=<<))
import Data.Traversable (traverse)
import Data.Foreign (readArray, readNumber, readString, readInt, F, Foreign, ForeignError(..), fail)
import Data.Foreign.Index (readProp)
import Data.Generic (class Generic, gShow, gEq)
import Data.YAML.Foreign.Encode
import Data.Argonaut.Core (toObject, toString)
import Data.Argonaut.Decode (getField)
import Data.Argonaut.Decode.Class (class DecodeJson)
import Data.Either (Either(..))
import Data.Generic (class Generic, gShow, gEq)
import Data.Maybe (maybe)
import Prelude (class Eq, class Show, bind, pure, ($))
data Point = Point Int Int
@ -33,28 +35,31 @@ derive instance genericMobility :: Generic Mobility
instance showMobility :: Show Mobility where show = gShow
instance eqMobility :: Eq Mobility where eq = gEq
readGeoObject :: Foreign -> F GeoObject
readGeoObject value = do
name <- readString =<< readProp "Name" value
scale <- readNumber =<< readProp "Scale" value
points <- traverse readPoint =<< readArray =<< readProp "Points" value
mobility <- readMobility =<< readProp "Mobility" value
coverage <- readNumber =<< readProp "Coverage" value
pure $ GeoObject { name, scale, points, mobility, coverage }
instance geoJson :: DecodeJson GeoObject where
decodeJson s = do
obj <- maybe (Left "GeoObject is not an object.") Right (toObject s)
name <- getField obj "Name"
scale <- getField obj "Scale"
points <- getField obj "Points"
mobility <- getField obj "Mobility"
coverage <- getField obj "Coverage"
pure $ GeoObject { name, scale, points, mobility, coverage }
readPoint :: Foreign -> F Point
readPoint value = do
x <- readInt =<< readProp "X" value
y <- readInt =<< readProp "Y" value
pure $ Point x y
instance mobilityJson :: DecodeJson Mobility where
decodeJson s = do
mob <- maybe (Left "Mobility is not a string.") Right (toString s)
case mob of
"Fix" -> pure Fix
"Flex" -> pure Flex
_ -> Left "Mobility must be either Flex or Fix"
instance pointJson :: DecodeJson Point where
decodeJson s = do
obj <- maybe (Left "Point is not an object.") Right (toObject s)
x <- getField obj "X"
y <- getField obj "Y"
pure $ Point x y
readMobility :: Foreign -> F Mobility
readMobility value = do
mob <- readString value
case mob of
"Fix" -> pure Fix
"Flex" -> pure Flex
_ -> fail $ JSONError "Mobility must be either Flex or Fix"
instance pointToYAML :: ToYAML Point where
toYAML (Point x y) =

View File

@ -1,23 +1,21 @@
module Test.Main where
import Data.Map as Map
import Data.StrMap as StrMap
import Control.Monad.Eff (Eff)
import Control.Monad.Except (runExcept)
import Data.Argonaut.Decode (class DecodeJson, decodeJson)
import Data.Either (Either(..))
import Data.Foreign (F, readArray)
import Data.YAML.Foreign.Decode (parseYAML)
import Data.Map (Map)
import Data.StrMap (StrMap)
import Data.YAML.Foreign.Decode (parseYAMLToJson)
import Data.YAML.Foreign.Encode (printYAML)
import Data.Traversable (traverse)
import Prelude (Unit, bind, ($), void, discard, (>>=))
import Test.Instances (readGeoObject, readMobility, readPoint, GeoObject(..), Mobility(..), Point(..))
import Prelude (Unit, discard, pure, ($), (<<<), (>>=))
import Test.Instances (GeoObject(..), Mobility(..), Point(..))
import Test.Spec (describe, it)
import Test.Spec.Assertions (shouldEqual)
import Test.Spec.Reporter.Console (consoleReporter)
import Test.Spec.Runner (RunnerEffects, run)
import Control.Monad.Eff.Console (log, CONSOLE)
import Data.Map as Map
import Data.Map (Map)
import Data.StrMap as StrMap
import Data.StrMap (StrMap)
yamlInput :: String
yamlInput = """
@ -50,22 +48,22 @@ yamlOutput :: String
yamlOutput = """- Mobility: Fix
Points:
- X: 10
'Y': 10
Y: 10
- X: 20
'Y': 10
Y: 10
- X: 5
'Y': 5
Y: 5
Coverage: 10
Name: House
Scale: 9.5
- Mobility: Fix
Points:
- X: 1
'Y': 1
Y: 1
- X: 2
'Y': 2
Y: 2
- X: 0
'Y': 0
Y: 0
Coverage: 10
Name: Tree
Scale: 1
@ -77,27 +75,37 @@ yamlMapOutput = """key:
- Mobility: Fix
Points:
- X: 10
'Y': 10
Y: 10
- X: 20
'Y': 10
Y: 10
- X: 5
'Y': 5
Y: 5
Coverage: 10
Name: House
Scale: 9.5
- Mobility: Fix
Points:
- X: 1
'Y': 1
Y: 1
- X: 2
'Y': 2
Y: 2
- X: 0
'Y': 0
Y: 0
Coverage: 10
Name: Tree
Scale: 1
"""
pointYaml :: String
pointYaml = """X: 1
Y: 1
"""
yamlToData :: forall a. (DecodeJson a) => String -> Either String a
yamlToData s = case runExcept $ parseYAMLToJson s of
Left err -> Left "Could not parse yaml"
Right json -> decodeJson json
testStrMap :: StrMap (Array GeoObject)
testStrMap = StrMap.singleton "key" parsedData
@ -123,16 +131,19 @@ parsedData =
}
]
readPoint :: String -> Either String Point
readPoint = yamlToData
fullCircle :: String -> Either String String
fullCircle yamlString = (readPoint yamlString) >>= pure <<< printYAML
main :: Eff (RunnerEffects ()) Unit
main = run [consoleReporter] do
describe "purescript-yaml" do
describe "decode" do
it "Decodes YAML" do
let decoded =
(parseYAML yamlInput) >>=
readArray >>=
traverse readGeoObject
(runExcept decoded) `shouldEqual` (Right parsedData)
let decoded = yamlToData yamlInput
decoded `shouldEqual` (Right parsedData)
describe "encode" do
it "Encodes YAML" $ do
let encoded = printYAML parsedData
@ -143,3 +154,6 @@ main = run [consoleReporter] do
let encodedMap = printYAML testMap
encodedMap `shouldEqual` yamlMapOutput
let s = fullCircle pointYaml
s `shouldEqual` (Right pointYaml)