Skip to content

Commit d80e9c0

Browse files
committed
Initial commit
0 parents commit d80e9c0

18 files changed

+506
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.stack-work

LICENSE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright 2017 Ian Sullivan
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# json-transform
2+
3+
A Haskell library for transforming json values.
4+
5+
input:
6+
{
7+
"orderId": 12345,
8+
"date": {
9+
"day":"03",
10+
"month":"04",
11+
"year":"2005"
12+
},
13+
"extra-info": {
14+
"foo":"bar",
15+
"fizz":"buzz"
16+
}
17+
}
18+
19+
transform:
20+
{
21+
"id": "$(orderId)",
22+
"dateString": "$(date.year)-$(date.month)-$(date.day)",
23+
"type": "order"
24+
}
25+
26+
output:
27+
{
28+
"id": 12345,
29+
"dateString": "2005-04-03",
30+
"type": "order"
31+
}

Setup.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Distribution.Simple
2+
main = defaultMain

app/Main.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module Main where
2+
3+
main :: IO ()
4+
main = putStrLn "TODO"

json-transform.cabal

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: json-transform
2+
version: 0.1.0.0
3+
synopsis: Transforms one json value to another
4+
description: Transforms one json value to another
5+
homepage: https://github.com/githubuser/json-transform#readme
6+
license: BSD3
7+
license-file: LICENSE
8+
author: Author name here
9+
maintainer: example@example.com
10+
copyright: 2017 Author name here
11+
category: Web
12+
build-type: Simple
13+
extra-source-files: README.md
14+
cabal-version: >=1.10
15+
16+
library
17+
hs-source-dirs: src
18+
exposed-modules: Data.JSONTransform
19+
ghc-options: -Wall -W
20+
build-depends: base >= 4.7 && < 5
21+
, parsec
22+
, aeson
23+
, text
24+
, unordered-containers
25+
, vector
26+
, scientific
27+
default-language: Haskell2010
28+
29+
executable json-transform-exe
30+
hs-source-dirs: app
31+
main-is: Main.hs
32+
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -W
33+
build-depends: base
34+
, json-transform
35+
default-language: Haskell2010
36+
37+
test-suite json-transform-test
38+
type: exitcode-stdio-1.0
39+
hs-source-dirs: test
40+
main-is: Spec.hs
41+
build-depends: base
42+
, json-transform
43+
, HUnit
44+
, aeson
45+
, directory
46+
, filepath
47+
, bytestring
48+
, text
49+
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -W
50+
default-language: Haskell2010
51+
52+
source-repository head
53+
type: git
54+
location: https://github.com/iansullivan88/json-transform

src/Data/JSONTransform.hs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
module Data.JSONTransform
4+
( JSONTransform, transform, parseTransform
5+
) where
6+
7+
import Control.Monad
8+
import Data.Aeson
9+
import qualified Data.HashMap.Lazy as H
10+
import Data.List
11+
import Data.Monoid
12+
import Data.Scientific
13+
import qualified Data.Text as T
14+
import qualified Data.Vector as V
15+
import Text.Parsec
16+
import Text.Parsec.Error
17+
18+
-- | A type to represent a transform from one json value to another
19+
newtype JSONTransform = JSONTransform TValue
20+
21+
-- | Transform analog of Aeson Value. Instead of String values, there are expressions
22+
data TValue = TObject !(H.HashMap T.Text TValue)
23+
| TArray !(V.Vector TValue)
24+
| TExp ![ValueExp]
25+
| TNumber !Scientific
26+
| TBool !Bool
27+
| TNull deriving(Show)
28+
29+
-- | A value expression which can either be a literal text value or
30+
-- an Accessor which is a list of keys.
31+
data ValueExp = Literal T.Text | Accessor [T.Text] deriving(Show)
32+
33+
-- | Parses a json 'Value' and returns either a parse error or and error
34+
-- about why parsing failed.
35+
parseTransform :: Value -> Either T.Text JSONTransform
36+
parseTransform v = JSONTransform <$> fromJSONValue v
37+
38+
-- | Transforms a json 'Value' using the supplied 'JSONTransform'
39+
transform :: JSONTransform -> Value -> Either T.Text Value
40+
transform (JSONTransform t) v = fromTValue v t
41+
42+
fromJSONValue :: Value -> Either T.Text TValue
43+
fromJSONValue (Object o) = TObject <$> traverse fromJSONValue o
44+
fromJSONValue (Array vs) = TArray <$> traverse fromJSONValue vs
45+
fromJSONValue (String t) = either (Left . formatError t) (Right . TExp) (parse valueExpParser "" t)
46+
fromJSONValue (Number n) = Right $ TNumber n
47+
fromJSONValue (Bool b) = Right $ TBool b
48+
fromJSONValue Null = Right TNull
49+
50+
fromTValue :: Value -> TValue -> Either T.Text Value
51+
fromTValue v (TObject o) = Object <$> traverse (fromTValue v) o
52+
fromTValue v (TArray vs) = Array <$> traverse (fromTValue v) vs
53+
fromTValue v (TExp es) = valueFromExpressions v es
54+
fromTValue _ (TNumber n) = Right $ Number n
55+
fromTValue _ (TBool b) = Right $ Bool b
56+
fromTValue _ TNull = Right Null
57+
58+
formatError :: T.Text -> ParseError -> T.Text
59+
formatError expression e = T.concat ["Error parsing accessor: "
60+
, expression
61+
, "\n"
62+
, "at position "
63+
, T.pack $ show $ sourceColumn $ errorPos e
64+
, "\n"
65+
, T.pack parsecError] where
66+
-- Taken from parsec source - format error without line number
67+
parsecError = showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input" (errorMessages e)
68+
69+
valueFromExpressions :: Value -> [ValueExp] -> Either T.Text Value
70+
valueFromExpressions v es = case traverse (valueFromExpression v) es of
71+
(Left err) -> Left err
72+
(Right vs) -> case foldM combineJSONValue Null vs of
73+
Nothing -> Left "Could not combine JSON values"
74+
Just c -> Right c
75+
76+
valueFromExpression :: Value -> ValueExp -> Either T.Text Value
77+
valueFromExpression _ (Literal t) = Right $ String t
78+
valueFromExpression v (Accessor ks) = maybe (Left $ T.append "Can't read value: " (formatKeys ks)) Right (valueForKeys v ks)
79+
80+
-- | Try to extract a 'Value' using a list of keys
81+
-- and a source json 'Value'
82+
valueForKeys :: Value -> [T.Text] -> Maybe Value
83+
valueForKeys o aks = go o ([],aks) where
84+
go v (_,[]) = Just v
85+
go (Object m) (ps,k:ks) = maybe Nothing (\v -> go v (k:ps,ks)) (H.lookup k m)
86+
go _ _ = Nothing
87+
88+
formatKeys :: [T.Text] -> T.Text
89+
formatKeys = T.concat . intersperse "."
90+
91+
-- | Combines two json 'Value's into a single Value.
92+
-- This might fail eg a bool and a number can't be
93+
-- sensibly combined
94+
combineJSONValue :: Value -> Value -> Maybe Value
95+
combineJSONValue v Null = Just v
96+
combineJSONValue Null v = Just v
97+
combineJSONValue (String t1) (String t2) = Just $ String $ t1 <> t2
98+
combineJSONValue _ _ = Nothing
99+
100+
--------------
101+
-- Parsing
102+
--------------
103+
104+
valueExpParser :: Parsec T.Text () [ValueExp]
105+
valueExpParser = many (try accessorParser <|> literalParser) <* eof
106+
107+
accessorParser :: Parsec T.Text () ValueExp
108+
accessorParser = do void $ char '$'
109+
between (char '(') (char ')') $ do
110+
ks <- sepBy1 (many1 $ noneOf ").") (char '.')
111+
return $ Accessor $ T.pack <$> ks
112+
113+
literalParser :: Parsec T.Text () ValueExp
114+
literalParser = Literal . T.pack <$> litChars where
115+
litChars = many1 (try (string "$$" *> pure '$') <|> noneOf "$")

stack.yaml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# This file was automatically generated by 'stack init'
2+
#
3+
# Some commonly used options have been documented as comments in this file.
4+
# For advanced use and comprehensive documentation of the format, please see:
5+
# https://docs.haskellstack.org/en/stable/yaml_configuration/
6+
7+
# Resolver to choose a 'specific' stackage snapshot or a compiler version.
8+
# A snapshot resolver dictates the compiler version and the set of packages
9+
# to be used for project dependencies. For example:
10+
#
11+
# resolver: lts-3.5
12+
# resolver: nightly-2015-09-21
13+
# resolver: ghc-7.10.2
14+
# resolver: ghcjs-0.1.0_ghc-7.10.2
15+
# resolver:
16+
# name: custom-snapshot
17+
# location: "./custom-snapshot.yaml"
18+
resolver: lts-9.10
19+
20+
# User packages to be built.
21+
# Various formats can be used as shown in the example below.
22+
#
23+
# packages:
24+
# - some-directory
25+
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
26+
# - location:
27+
# git: https://github.com/commercialhaskell/stack.git
28+
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
29+
# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a
30+
# extra-dep: true
31+
# subdirs:
32+
# - auto-update
33+
# - wai
34+
#
35+
# A package marked 'extra-dep: true' will only be built if demanded by a
36+
# non-dependency (i.e. a user package), and its test suites and benchmarks
37+
# will not be run. This is useful for tweaking upstream packages.
38+
packages:
39+
- .
40+
# Dependency packages to be pulled from upstream that are not in the resolver
41+
# (e.g., acme-missiles-0.3)
42+
extra-deps: []
43+
44+
# Override default flag values for local packages and extra-deps
45+
flags: {}
46+
47+
# Extra package databases containing global packages
48+
extra-package-dbs: []
49+
50+
# Control whether we use the GHC we find on the path
51+
# system-ghc: true
52+
#
53+
# Require a specific version of stack, using version ranges
54+
# require-stack-version: -any # Default
55+
# require-stack-version: ">=1.5"
56+
#
57+
# Override the architecture used by stack, especially useful on Windows
58+
# arch: i386
59+
# arch: x86_64
60+
#
61+
# Extra directories used by stack for building
62+
# extra-include-dirs: [/path/to/dir]
63+
# extra-lib-dirs: [/path/to/dir]
64+
#
65+
# Allow a newer minor version of GHC than the snapshot specifies
66+
# compiler-check: newer-minor
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"suiteTransform": {
3+
"key": "$(level1.level2)"
4+
},
5+
"cases": [{
6+
"input": null
7+
},{
8+
"input": {"level1":"value"}
9+
},{
10+
"input": {"level2":"value"}
11+
},{
12+
"input": {"level1":{"foo":"bar"}}
13+
},{
14+
"input": {"level1":{"level2":"expected"}},
15+
"expected": {"key":"expected"}
16+
}]
17+
}
18+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"suiteTransform": {
3+
"id": "$(orderId)",
4+
"dateString": "$(date.year)-$(date.month)-$(date.day)",
5+
"type": "order"
6+
},
7+
"cases": [{
8+
"input": {
9+
"orderId": 12345,
10+
"date": {
11+
"day":"03",
12+
"month":"04",
13+
"year":"2005"
14+
},
15+
"extra-info": {
16+
"foo":"bar",
17+
"fizz":"buzz"
18+
}
19+
},
20+
"expected": {
21+
"id": 12345,
22+
"dateString": "2005-04-03",
23+
"type": "order"
24+
}
25+
}]
26+
}
27+

0 commit comments

Comments
 (0)