Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • mkb22174/cs316-coursework-2024
  • jjb15109/cs316-coursework-2024
2 results
Show changes
Commits on Source (3)
module Main where
import System.Environment (getArgs)
import System.Directory (doesFileExist)
import JSON
import JSONInput
import JSONOutput
import JSONTransformer
import Result
query :: Transformer
query = getElements `pipe` select (binaryOp equal (getField "Country") (string "S"))
{-| Main function that It gets filenames from the command line, reads and parses the JSON data and
then enters a loop to process queries on the data.
-}
main :: IO ()
main = do
do -- Get the JSON filename to read from the command line arguments.
--
-- FIXME: This is not robust. Can you alter it so that it reports
-- a user friendly error if the filename is not present? What if
-- we want to include additional command line options?
[filename] <- getArgs
-- Read the raw data in from the file given.
--
-- FIXME: What if the user wants to query several JSON files?
rawText <- readFile filename
-- Parse the raw text of the input into a structured JSON
-- representation.
--
-- FIXME: what if the user wants to
inputJSON <- abortOnError (stringToJSON rawText)
-- Run the query on the parsed JSON to a list of JSON values
--
-- FIXME: What if the user wants a different query? the query
-- should be taken as an input as well.
--
-- FIXME: the query langauge is quite inexpressive. What if the
-- user wants all hills over 1000 metres in Scotland and Wales?
-- or something else? What if they want to transform the input
-- and not just filter it?
--
-- FIXME: The query might be incompatible with the input data. It
-- might mention fields that the input does not have. Can these
-- errors be reported back to the user nicely?
let outputJSONs = query inputJSON
-- Print the output, one per line.
--
-- FIXME: what if the user wants the JSON output to be nicely
-- formatted? Or in colour? Or in another format, like HTML or
-- CSV?
mapM_ (putStrLn . renderJSON) outputJSONs
-- | Get the JSON filename to read from the command line.
filenames <- getFilenames
-- | Read the raw data in from the files.
rawTexts <- mapM readFile filenames
-- | Parse the raw text and handle errors.
inputJSONs <- mapM (abortOnError . stringToJSON) rawTexts
-- | Keep asking for queries and applying them until users types anything but y.
processqueries inputJSONs
{-| Function to repeatedly ask for queries and process them.
It processes each query on the data and either prints the results
or prompts the user to enter a new query if no results are found.
-}
processqueries :: [JSON] -> IO ()
processqueries inputJSONs = do
query <- getQuery
-- Run the query and collect the output.
let outputJSONs = concatMap query inputJSONs
if null outputJSONs
then do
putStrLn "No results found."
-- Ask the user for another query
processqueries inputJSONs
else do
-- Print the output
mapM_ (putStrLn . renderJSON) outputJSONs
putStrLn "Do you want to enter another query (y/n)"
continue <- getLine
if continue == "y" || continue == "Y"
then processqueries inputJSONs
else putStrLn "Exiting Program."
-- | Function to get a query from the user.
-- It loops until a valid query is entered by the user.
getQuery :: IO Transformer
getQuery = do
getValidQuery
-- | Helper function to repeatedly prompt the user until a valid query is entered.
getValidQuery :: IO Transformer
getValidQuery = do
putStrLn "Enter the query expression for example (Height > 1000 or Country = S)"
queryStr <- getLine
let query = parseQuery queryStr
case query of
Nothing -> do
putStrLn "Invalid query format. Please use 'field operator value' for example (Height > 1000)"
getValidQuery
Just transformer -> return transformer
-- | Function to manually parse query expressions into a transformer.
-- Supports operators like '=', '>', '<', '>=', '<=','sub'.
parseQuery :: String -> Maybe Transformer
parseQuery queryStr =
case words queryStr of
[field, "=", value] ->
-- | Check if the value is a quoted string.
if isQuoted value then
let stringv = read value :: String
in Just $ getElements `pipe` select (binaryOp equal (getField field) (string stringv))
else
case reads value of
[(v, "")] -> Just $ getElements `pipe` select (binaryOp equal (getField field) (integer v))
_ -> Nothing
[field, ">", value] ->
case reads value of
[(v, "")] -> Just $ getElements `pipe` select (binaryOp greaterThan (getField field) (integer v))
_ -> Nothing
[field, "<", value] ->
case reads value of
[(v, "")] -> Just $ getElements `pipe` select (binaryOp lessThan (getField field) (integer v))
_ -> Nothing
[field, ">=", value] ->
case reads value of
[(v, "")] -> Just $ getElements `pipe` select (binaryOp greaterEqual (getField field) (integer v))
_ -> Nothing
[field, "<=", value] ->
case reads value of
[(v, "")] -> Just $ getElements `pipe` select (binaryOp lessEqual (getField field) (integer v))
_ -> Nothing
[field, "sub", value] ->
if isQuoted value then
let stringv = read value :: String
in Just $ getElements `pipe` select (binaryOp containsSubstring (getField field) (string stringv))
else
Nothing
_ -> Nothing
-- | Helper function to check if a string is quoted
isQuoted :: String -> Bool
isQuoted ('"':xs) | last xs == '"' = True
isQuoted _ = False
-- | Function to safely get the filenames, with error handling.
-- It checks whether the files exist and prompts the user if any files are invalid.
getFilenames :: IO [String]
getFilenames = do
args <- getArgs
if null args then askfiles else checkFilesExist args
-- | Function to check if the given list of files exists.
-- If any files do not exist it prints an error and asks for filenames again.
-- zip puts two seperate lists into one this list has a filename and true or false if doesnt exists
checkFilesExist :: [String] -> IO [String]
checkFilesExist filenames = do
list <- mapM doesFileExist filenames
let invalidFiles = [f | (f, False) <- zip filenames list]
if null invalidFiles
then return filenames
else do
putStrLn $ "Error: The following files do not exist: " ++ unwords invalidFiles
askfiles
-- | Function to repeatedly ask the user for valid filenames.
-- If invalid files are entered, it will prompt the user to try again.
-- words allow me to get the filename for each space
askfiles :: IO [String]
askfiles = do
putStrLn "Please enter the filenames (separate with spaces):"
filenames <- words <$> getLine
checkFilesExist filenames
......@@ -74,6 +74,7 @@ library
-- Other library packages from which modules are imported.
build-depends: base ^>=4.17.2.1
, directory >= 1.3.0.0
-- Directories containing source files.
hs-source-dirs: lib
......@@ -98,6 +99,7 @@ executable json-query
build-depends:
base ^>=4.17.2.1,
cs316-coursework2024
, directory >= 1.3.0.0
-- Directories containing source files.
hs-source-dirs: app
......@@ -132,3 +134,4 @@ test-suite cs316-coursework2024-test
base ^>=4.17.2.1,
cs316-coursework2024,
HUnit >= 1.6.0.0
, directory >= 1.3.0.0
[
{
"Name": "Carn Breagach NE Top",
"Country": "A",
"Height": 143
},
{
"Name": "Sidhean Buidhe",
"Country": "B",
"Height": 141
},
{
"Name": "Druim nan Gobhar",
"Country": "C",
"Height": 141
}
]
\ No newline at end of file
[
{
"Name": "Seafield Hill",
"Country": "D",
"Height": 76
},
{
"Name": "Rubha na Stiure",
"Country": "E",
"Height": 75
},
{
"Name": "Dubh Leathad",
"Country": "F",
"Height": 75
}
]
\ No newline at end of file
......@@ -16,8 +16,7 @@ data JSON
= JsonString String
-- ^ Constructor for JSON String values.
| JsonInteger Integer
-- ^ Constructor for JSON Number values (just integers for this
-- simple program).
-- ^ Constructor for JSON Number values (just integers for this simple program).
| JsonBoolean Bool
-- ^ Constructor for JSON Boolean values.
| JsonNull
......@@ -28,59 +27,100 @@ data JSON
-- ^ Constructor for JSON objects, represented as lists of (name,value) pairs
deriving (Show, Eq, Ord)
-- | An example piece of JSON, with the heights of some hills from
-- Britain and Ireland.
hillsExample :: JSON
hillsExample =
JsonArray
[ JsonObject [ ("Name", JsonString "Ben Chonzie"),
("Country", JsonString "S"),
("Height", JsonInteger 931)
],
JsonObject [ ("Name", JsonString "Lonscale Fell"),
("Country", JsonString "E"),
("Height", JsonInteger 715)
],
JsonObject [ ("Name", JsonString "Iwerddon"),
("Country", JsonString "W"),
("Height", JsonInteger 583)
],
JsonObject [ ("Name", JsonString "Maolan Bui"),
("Country", JsonString "I"),
("Height", JsonInteger 968)
]
]
-- | Determines "truthy" JSON values.
--
-- A truthy JSON value is one that is exactly @JSONBoolean
-- True@. Anything else is considered falsy.
--
-- “Truthy” and “Falsy” are concepts used languages that don't throw
-- errors when trying to use non-boolean values as truth values. Some
-- rule is needed to determine *for every type* which elements are
-- "true" and which are "false". This can be a great way to hide bugs
-- when the things being tested are mistaken for actual boolean values.
--
-- Python and JavaScript are examples of languages with "truthiness":
-- * https://docs.python.org/3/library/stdtypes.html#truth-value-testing
-- * https://developer.mozilla.org/en-US/docs/Glossary/Truthy
isTruthy :: JSON -> Bool
isTruthy = error "UNIMPLEMENTED: isTruthy"
isTruthy (JsonBoolean b) = b
isTruthy (JsonInteger n) = n /= 0
isTruthy (JsonString s) = not (s == "")
isTruthy (JsonArray arr) = not (length arr == 0)
isTruthy (JsonObject obj) = not (length obj == 0)
isTruthy JsonNull = False
-- | Equality comparison on JSON values.
--
-- Returns @JsonBoolean True@ if the two values are equal, and
-- @JsonBoolean False@ otherwise.
--
-- Whether or not two JSON objects with the same fields in different
-- orders are considered equal is not specified.
{-| Equality comparison on JSON values.
Returns @JsonBoolean True@ if the two values are equal, and
@JsonBoolean False@ otherwise.
Whether or not two JSON objects with the same fields in different
orders are considered equal is not specified.
-}
equal :: JSON -> JSON -> JSON
equal = error "UNIMPLEMENTED: equal"
equal x y = if x == y then JsonBoolean True else JsonBoolean False
-- | Disequality comparison on JSON values.
--
-- Returns @JsonBoolean False@ if the two values are equal, and
-- @JsonBoolean True@ otherwise.
{-| Equality comparison on JSON values.
Returns @JsonBoolean False@ if the two values are equal, and
@JsonBoolean True@ otherwise.
Whether or not two JSON objects with the same fields in different
orders are considered equal is not specified.
-}
notEqual :: JSON -> JSON -> JSON
notEqual = error "UNIMPLEMENTED: notEqual"
notEqual x y = if x == y then JsonBoolean False else JsonBoolean True
{-| Equality comparison on JSON values.
Returns @JsonBoolean True@ if x is greater y and
@JsonBoolean False@ otherwise.
Whether or not two JSON objects with the same fields in different
orders are considered equal is not specified.
-}
greaterThan :: JSON -> JSON -> JSON
greaterThan x y = if x > y then JsonBoolean True else JsonBoolean False
{-| Equality comparison on JSON values.
Returns @JsonBoolean True@ if x less greater y and
@JsonBoolean False@ otherwise.
Whether or not two JSON objects with the same fields in different
orders are considered equal is not specified.
-}
lessThan :: JSON -> JSON -> JSON
lessThan x y = if x < y then JsonBoolean True else JsonBoolean False
{-| Equality comparison on JSON values.
Returns @JsonBoolean True@ if x is greater or equal to y and
@JsonBoolean False@ otherwise.
Whether or not two JSON objects with the same fields in different
orders are considered equal is not specified.
-}
greaterEqual :: JSON -> JSON -> JSON
greaterEqual x y = if x >= y then JsonBoolean True else JsonBoolean False
{-| Equality comparison on JSON values.
Returns @JsonBoolean True@ if x is less or equal to y and
@JsonBoolean False@ otherwise.
Whether or not two JSON objects with the same fields in different
orders are considered equal is not specified.
-}
lessEqual :: JSON -> JSON -> JSON
lessEqual x y = if x <= y then JsonBoolean True else JsonBoolean False
{-| String comparison on JSON values.
Returns @JsonBoolean True@ if s contains sub very similar to .contains in java and
@JsonBoolean False@ otherwise or if not @JsonString@ return Null to show no results from query.
Whether or not two JSON objects with the same fields in different
orders are considered equal is not specified.
-}
containsSubstring :: JSON -> JSON -> JSON
containsSubstring (JsonString s) (JsonString sub)
| substringExists s sub = JsonBoolean True
| otherwise = JsonBoolean False
containsSubstring _ _ = JsonNull
{-| Helper function for @containsSubstring@.
uses @take@ to find the length n of sub and use it to compare n
length of the string.
If both have same characters and length
return @True@ otherwise if not same characters or empty then return
@False@
-}
substringExists :: String -> String -> Bool
substringExists [] _ = False
substringExists _ [] = False
substringExists (x:xs) sub
| take (length sub) (x:xs) == sub = True
| otherwise = substringExists xs sub
......@@ -8,79 +8,83 @@ import Data.List (intersperse)
-- | Returns the JSON escaped version of a character.
--
-- Escaping is used to mark characters that would otherwise be
-- interpreted as control codes. Strings in JSON are begun and ended
-- by double-quotes, so if we want to put a double-quote in a string
-- then we need to mark it as special. JSON (and Haskell and Java and
-- ...) do this by putting a backslash in front of it. This means that
-- backslashes are also treated as special, so if we want to have a
-- backslash in a string, then we need to represent it with two
-- backslashes.
--
-- JSON also escapes common control characters such as newline, tabs,
-- carriage return, etc., and optionally non-ASCII unicode
-- characters. This function also escapes these. See the JSON
-- standards documentation for information on what characters to
-- escape.
--
-- Any character that is not escaped is return as-is in a one
-- character string.
{-| Escaping is used to mark characters that would otherwise be
interpreted as control codes. Strings in JSON are begun and ended
by double-quotes, so if we want to put a double-quote in a string
then we need to mark it as special. JSON (and Haskell and Java and
...) do this by putting a backslash in front of it. This means that
backslashes are also treated as special, so if we want to have a
backslash in a string, then we need to represent it with two
backslashes.
JSON also escapes common control characters such as newline, tabs,
carriage return, etc., and optionally non-ASCII unicode
characters. This function also escapes these. See the JSON
standards documentation for information on what characters to
escape.
Any character that is not escaped is return as-is in a one
character string.
-}
escapeChar :: Char -> String
escapeChar = error "UNIMPLEMENTED: escapeChar"
escapeChar '\n' = "\\n"
escapeChar '"' = "\\\""
escapeChar '\\' = "\\\\"
escapeChar x = [x]
-- | Convert every character in a String to its escaped
-- representation and concatenate them all together.
--
-- For example:
-- >>> escapeString "Hello \"world\""
-- "Hello \\\"world\\\""
-- (note that both JSON and Haskell escaping are happening here!)
{-| Convert every character in a String to its escaped
representation and concatenate them all together.
-}
escapeString :: String -> String
escapeString = error "UNIMPLEMENTED: escapeString"
escapeString [] = []
escapeString (x:xs) = escapeChar x ++ escapeString xs
-- | Quote a string by placing double-quotation marks before and after.
quote :: String -> String
quote = error "UNIMPLEMENTED: quote"
quote [] = "\"\""
quote xs = ['"'] ++ xs ++ ['"']
-- | Render a string as its JSON representation by escaping every
-- character and placing double-quotes around it.
--
-- For example,
-- >>> renderString "Hello \"world\""
-- "\"Hello \\\"world\\\"\""
{-| Render a string as its JSON representation by escaping every
character and placing double-quotes around it.
-}
renderString :: String -> String
renderString = error "UNIMPLEMENTED: renderString"
renderString [] = quote []
renderString (x:xs) = quote (escapeChar x ++ escapeString xs)
-- | Put square brackets [ ] around a string.
sqbracket :: String -> String
sqbracket = error "UNIMPLEMENTED: sqbracket"
sqbracket [] = "[]"
sqbracket xs = "[" ++ xs ++ "]"
-- | Put curly brackets { } around a string.
curlybracket :: String -> String
curlybracket = error "UNIMPLEMENTED: curlybracket"
curlybracket [] = "{}"
curlybracket xs = "{" ++ xs ++ "}"
-- | Concatenate a list of items with a separator between each one.
concatWith :: Monoid m => m -> [m] -> m
concatWith = error "UNIMPLEMENTED: concatWith"
concatWith _ [] = mempty
concatWith sep xs = mconcat (intersperse sep xs)
-- | Render a JSON-style object field as @"field name": <value>@.
--
-- For example,
-- >>> renderField ("myNull", JsonNull)
-- "\"myNull\":null"
-- | Render a JSON-style object field
renderField :: (String, JSON) -> String
renderField = error "UNIMPLEMENTED: renderField"
renderField (xs,JsonNull) = renderString (xs) ++ ":" ++ "null"
renderField (xs,JsonString ys) = renderString (xs) ++ ":" ++ renderString ys
renderField (xs,JsonInteger y) = renderString (xs) ++ ":" ++ show y
renderField (xs, JsonBoolean True) = renderString(xs) ++ ":" ++ "true"
renderField (xs, JsonBoolean False) = renderString(xs) ++ ":" ++ "false"
renderField (xs, JsonArray ys) = renderString xs ++ ":" ++ sqbracket (concatWith "," (map renderJSON ys))
renderField (xs, JsonObject ys) = renderString xs ++ ":" ++ curlybracket (concatWith "," (map renderField ys))
-- | Converts a JSON value into its string representation.
--
--
renderJSON :: JSON -> String
renderJSON (JsonString s) = renderString s
renderJSON (JsonInteger i) = show i
renderJSON (JsonBoolean True) = "true"
renderJSON (JsonBoolean False) = "false"
renderJSON JsonNull = "null"
renderJSON (JsonArray jsons) =
error "UNIMPLEMENTED: renderJSON for arrays"
renderJSON (JsonObject fields) =
error "UNIMPLEMENTED: renderJSON for objects"
renderJSON (JsonArray jsons) = sqbracket (concatWith "," (map renderJSON jsons))
renderJSON (JsonObject fields) = curlybracket (concatWith "," (map renderField fields))
......@@ -15,167 +15,86 @@
module JSONTransformer where
import JSON
-- | A converter from JSON values to zero or more JSON values.
type Transformer = JSON -> [JSON]
------------------------------------------------------------------------------
-- Transformers for constructing JSON values
-- | The @constant@ transformer is one that always outputs a fixed
-- value for all transformer inputs.
{-|
The @constant@ transformer is one that always outputs a fixed
value for all transformer inputs.
-}
constant :: JSON -> Transformer
constant = error "UNIMPLEMENTED: constant"
-- HINT: you can use 'constant' to implement the next four functions
-- more easily.
constant x = \_ -> [x]
-- | A transformer that always generates a fixed string value.
string :: String -> Transformer
string = error "UNIMPLEMENTED: string"
string xs = constant (JsonString xs)
-- | A transformer that always generates a fixed integer value.
integer :: Integer -> Transformer
integer = error "UNIMPLEMENTED: integer"
integer x = constant (JsonInteger x)
-- | A transformer that always generates a fixed boolean value.
bool :: Bool -> Transformer
bool = error "UNIMPLEMENTED: bool"
bool x = constant (JsonBoolean x)
-- | A transformer that always generates the null value.
jnull :: Transformer
jnull = error "UNIMPLEMENTED: jnull"
jnull = constant JsonNull
-- | Filters the input using another transformer. If the transformer
-- argument returns a truthy value (as determined by 'JSON.isTruthy')
-- for the input, then return the input in a single element
-- list. Otheriwse, return the empty list.
--
-- For example, if the condition is always true, then you get back the
-- input:
-- >>> select (bool True) (JsonArray [JsonInteger 1, JsonInteger 2])
-- [JsonArray [JsonInteger 1,JsonInteger 2]]
--
-- If the condition is never true, then you get back the empty list:
-- >>> select (bool False) (JsonArray [JsonInteger 1, JsonInteger 2])
-- []
--
-- Selecting for the `"a"` field being `1`, when it is:
-- >>> select (binaryOp equal (getField "a") (integer 1)) (JsonObject [("a", JsonInteger 1)])
-- [JsonObject [("a", JsonInteger 1)]]
--
-- Selecting for the `"a"` field being `1`, when it isn't:
-- >>> select (binaryOp equal (getField "a") (integer 1)) (JsonObject [("a", JsonInteger 2)])
-- []
--
-- If the `equal` returns multiple values, then only one of them needs
-- to be `True` for it to select that thing, so we can check to see if
-- a certain element is in an array:
-- >>> select (binaryOp equal getElements (integer 1)) (JsonArray [JsonInteger 1, JsonInteger 3, JsonInteger 4])
-- [JsonArray [JsonInteger 1,JsonInteger 3,JsonInteger 4]]
--
-- Same test, but this time with an array that doesn't contain `1`:
-- >>> select (binaryOp equal getElements (integer 1)) (JsonArray [JsonInteger 3, JsonInteger 4])
-- []
--
-- The following example tests to see whether or not the @"a"@ field
-- of the input contains an array that contains the value @1@:
-- > select (binaryOp equal (pipe (getField "a") getElements) (integer 1))
--
-- In Jq syntax, this is @select(.a | .[] == 1)@
{-| Filters the input using another transformer. If the transformer
argument returns a truthy value (as determined by 'JSON.isTruthy')
for the input, then return the input in a single element
list. Otheriwse, return the empty list.
-}
select :: Transformer -> Transformer
select = error "UNIMPLEMENTED: select"
select condition xs =
if any JSON.isTruthy (condition xs)
then [xs]
else []
-- HINT: you'll need to check to see if the transformer argument
-- returns an isTruthy value at any point in its list for the
-- input. You can use the 'any' function (Week 05) to do this.
-- | Converts any binary operation (i.e. a two argument function) from
-- working on 'JSON' values to work on transformers. The same input is
-- fed to the two transformers and all pairs of their outputs are
-- combined using the operation.
--
-- >>> binaryOp JSON.equal (string "a") (string "a") JsonNull
-- [JsonBoolean True]
-- >>> binaryOp JSON.equal (string "a") (integer 5) JsonNull
-- [JsonBoolean False]
-- >>> binaryOp JSON.equal (integer 1) getElements (JsonArray [JsonInteger 1, JsonString "a"])
-- [JsonBoolean True,JsonBoolean False]
-- >>> binaryOp JSON.notEqual (integer 1) getElements (JsonArray [JsonInteger 1, JsonString "a"])
-- [JsonBoolean False,JsonBoolean True]
-- >>> binaryOp JSON.equal getElements getElements (JsonArray [JsonInteger 1, JsonString "a"])
-- [JsonBoolean True,JsonBoolean False,JsonBoolean False,JsonBoolean True]
binaryOp :: (JSON -> JSON -> JSON) ->
Transformer -> Transformer -> Transformer
binaryOp = error "UNIMPLEMENTED: binaryOp"
-- | Connects two transformers together, feeding the output of the
-- first into the input of the second, and then flattening all the
-- results.
--
-- A picture, where 'x' is the input, 'f' is the first transformer,
-- and 'g' is the second.
--
-- >
-- > [v1, --g--> [[x1, [x1,
-- > x2], x2,
-- >x --f--> v2, --g--> [x3, --> x3,
-- > x4], x4,
-- > v3] --g--> [x5, x5,
-- > x6]] x6]
-- >
--
-- Connecting 'getElements' to 'getElements' via a pipe "unwraps" two
-- levels of arrays.
-- >>> pipe getElements getElements (JsonArray [JsonArray [JsonInteger 1, JsonInteger 2], JsonArray [JsonInteger 3, JsonInteger 4]])
-- [JsonInteger 1,JsonInteger 2,JsonInteger 3,JsonInteger 4]
--
-- Connecting 'getElements' to @field "a"@ via a pipe takes everything
-- from an array, and then all the @"a"@ fields.
-- >>> pipe getElements (getField "a") (JsonArray [JsonObject [("a", JsonInteger 1)],JsonObject [("a", JsonInteger 2)], JsonObject []])
-- [JsonInteger 1,JsonInteger 2]
--
-- Connecting @field "a"@ to elements via a pipe will look up the
-- field @"a"@ in an object and then get all the elements from the
-- array stored in that field.
-- >>> pipe (getField "a") getElements (JsonObject [("a", JsonArray [JsonInteger 1, JsonString "abc", JsonNull])])
-- [JsonInteger 1,JsonString "abc",JsonNull]
{-| Converts any binary operation (i.e. a two argument function) from
working on 'JSON' values to work on transformers. The same input is
fed to the two transformers and all pairs of their outputs are
combined using the operation.
-}
binaryOp :: (JSON -> JSON -> JSON) -> Transformer -> Transformer -> Transformer
binaryOp bop t1 t2 input = [bop v1 v2 | v1 <- t1 input, v2 <- t2 input ]
{-| Connects two transformers together, feeding the output of the
first into the input of the second, and then flattening all the
results.
-}
pipe :: Transformer -> Transformer -> Transformer
pipe = error "UNIMPLEMENTED: pipe"
pipe t1 t2 xs = concatMap t2 (t1 xs)
-- HINT: this function is very similar to the 'o' function in the
-- paper linked above.
-- | Extracts the elements of a @JsonArray@. If the input is not an
-- array, then the empty list of results is returned.
--
-- >>> getElements (JsonArray [JsonInteger 1, JsonString "a"])
-- [JsonInteger 1, JsonString "a"]
-- >>> getElements (JsonObject [])
-- []
--
{-| Extracts the elements of a @JsonArray@. If the input is not an
array, then the empty list of results is returned.
-}
getElements :: Transformer
getElements = error "UNIMPLEMENTED: getElements"
-- | Extracts the value of a named field from the input JSON, if it is
-- a 'JsonObject'. If the field does not exist, or the input is not an
-- object, then the empty list of results is returned.
--
-- Examples:
--
-- >>> getField "a" (JsonObject [("a", JsonInteger 5)])
-- [JsonInteger 5]
-- >>> getField "b" (JsonObject [("a", JsonInteger 5)]
-- []
-- >>> getField "a" (JsonArray [JsonInteger 1, JsonNull])
-- []
--
-- The behaviour when the same field appears multiple times is
-- unspecified.
getElements xs =
case xs of
JsonArray elements -> elements
_ -> []
{-| Extracts the value of a named field from the input JSON, if it is
a 'JsonObject'. If the field does not exist, or the input is not an
object, then the empty list of results is returned.
-}
getField :: String -> Transformer
getField = error "UNIMPLEMENTED: getField"
getField field xs =
case xs of
JsonObject fields ->
case lookup field fields of
Just v -> [v]
Nothing -> []
_ -> []
-- HINT: the 'lookup' function from the standard library will do the
-- lookup in the list of (name,value) pairs inside a JsonObject for
-- you.
module Main (main) where
import Test.HUnit
import JSON
import JSONOutput
import JSONTransformer
-- | Helper function to add @JsonInteger@
addIntegers :: JSON -> JSON -> JSON
addIntegers (JsonInteger x) (JsonInteger y) = JsonInteger (x + y)
addIntegers _ _ = JsonInteger 0
-- HINT: import the modules you are testing here
-- | Test case for @constant@ function
testConstant :: Test
testConstant = TestCase $
assertEqual "constant should return hello"
[JsonString "hello"]
(constant (JsonString "hello") (JsonString "world"))
-- | Test case for @string@ function
testString :: Test
testString = TestCase $
assertEqual "string should return hello"
[JsonString "hello"]
(string "hello" JsonNull)
-- | Test case for @integer@ function
testInteger :: Test
testInteger = TestCase $
assertEqual "integer should return 42"
[JsonInteger 42]
(integer 42 JsonNull)
-- | Test case for @bool@ function
testBool :: Test
testBool = TestCase $
assertEqual "bool should return a JsonBoolean True"
[JsonBoolean True]
(bool True JsonNull)
-- | Test case for @jnull@ function
testJNull :: Test
testJNull = TestCase $
assertEqual "jnull should return JsonNull"
[JsonNull]
(jnull JsonNull)
-- | Test case for @select@ function
testSelect :: Test
testSelect = TestList
[
TestCase $
assertEqual "should return 42 if condition true"
[JsonInteger 42]
(select (constant (JsonBoolean True)) (JsonInteger 42)),
aFailingTest :: Test
aFailingTest =
TestCase (assertFailure "I am an unhappy test, and I can naught but fail")
TestCase $
assertEqual "should return [] if condition false"
[]
(select (constant (JsonBoolean False)) (JsonInteger 42)),
TestCase $
assertEqual "select should be able to handle more complex problems"
[JsonObject [("k", JsonInteger 21)]]
(select (constant (JsonBoolean True)) (JsonObject [("k", JsonInteger 21)])),
TestCase $
assertEqual "should return [] for more complex problems"
[]
(select (constant (JsonBoolean False)) (JsonObject [("k", JsonInteger 42)]))
]
-- | Test case for @binaryOp@ function
testBinaryOp :: Test
testBinaryOp = TestCase $ assertEqual "binaryOp should be able to add JsonIntegers"[JsonInteger 5] (binaryOp addIntegers (integer 2) (integer 3) JsonNull)
-- | Test case for @pipe@ function
testPipe :: Test
testPipe = TestList [
TestCase $
assertEqual "should be able to flatten this"
[JsonInteger 1, JsonInteger 2, JsonInteger 3, JsonInteger 4]
(pipe getElements getElements (JsonArray [JsonArray [JsonInteger 1, JsonInteger 2], JsonArray [JsonInteger 3, JsonInteger 4]])),
TestCase $
assertEqual "want to get all fields that equals a"
[JsonInteger 1, JsonInteger 2]
(pipe getElements (getField "a")
(JsonArray [JsonObject [("a", JsonInteger 1)], JsonObject [("a", JsonInteger 2)], JsonObject []])),
TestCase $
assertEqual "work with multipe types"
[JsonInteger 1, JsonString "har", JsonNull]
(pipe (getField "a") getElements
(JsonObject [("a", JsonArray [JsonInteger 1, JsonString "har", JsonNull])])),
TestCase $
assertEqual "handle empty array well"
[]
(pipe getElements getElements (JsonArray [])),
TestCase $
assertEqual "be able to get value in more complex problems"
[JsonInteger 42]
(pipe getElements (pipe (getField "n") getElements)
(JsonArray [JsonObject [("n", JsonArray [JsonInteger 42])]]))
]
-- | Test case for @getElements@ function
testGetElements :: Test
testGetElements = TestList
[ TestCase $ assertEqual "getElements should get all values in array"
[JsonInteger 1, JsonInteger 2, JsonInteger 3]
(getElements (JsonArray [JsonInteger 1, JsonInteger 2, JsonInteger 3])),
TestCase $ assertEqual "if not array then empty list"
[]
(getElements (JsonInteger 42)),
TestCase $ assertEqual "getElements should handle more complex pronlems"
[JsonArray [JsonInteger 1, JsonInteger 2], JsonArray [JsonInteger 3, JsonInteger 4]]
(getElements (JsonArray [JsonArray [JsonInteger 1, JsonInteger 2], JsonArray [JsonInteger 3, JsonInteger 4]]))
]
-- | Test case for @getField@ function
testGetField :: Test
testGetField = TestList
[ TestCase $
assertEqual "getField should return the field from a JsonObject"
[JsonString "v"]
(getField "k" (JsonObject [("k", JsonString "v"), ("other", JsonInteger 10)])),
TestCase $
assertEqual "getField should return an empty list if the field is missing"
[]
(getField "null" (JsonObject [("k", JsonString "v")]))
]
-- | Test case for @isTruthy@ function
testIsTruthy :: Test
testIsTruthy = TestList [
TestCase $ assertEqual "JsonBoolean True is truthy" True (isTruthy (JsonBoolean True)),
TestCase $ assertEqual "JsonInteger 0 is not truthy" False (isTruthy (JsonInteger 0)),
TestCase $ assertEqual "not empty JsonString is truthy" True (isTruthy (JsonString "hello")),
TestCase $ assertEqual "empty JsonArray is not truthy" False (isTruthy (JsonArray [])),
TestCase $ assertEqual "Not empty JsonObject is truthy" True (isTruthy (JsonObject [("k", JsonInteger 1)])),
TestCase $ assertEqual "JsonNull is not truthy" False (isTruthy JsonNull)
]
-- | Test case for @equal@ function
testEqual :: Test
testEqual = TestList [
TestCase $ assertEqual "equal JsonIntegers" (JsonBoolean True) (equal (JsonInteger 1) (JsonInteger 1)),
TestCase $ assertEqual "not equal JsonString" (JsonBoolean False) (equal (JsonString "hello") (JsonString "world"))
]
-- | Test case for @greaterThan@ function
testGreaterThan :: Test
testGreaterThan = TestList [
TestCase $ assertEqual "greater JsonInteger" (JsonBoolean True) (greaterThan (JsonInteger 2) (JsonInteger 1)),
TestCase $ assertEqual "less greater JsonInteger" (JsonBoolean False) (greaterThan (JsonInteger 1) (JsonInteger 2)),
TestCase $ assertEqual "equal JsonInteger" (JsonBoolean False) (greaterThan (JsonInteger 1) (JsonInteger 1))
]
-- | Test case for @lessThan@ function
testLessThan :: Test
testLessThan = TestList [
TestCase $ assertEqual "less JsonInteger" (JsonBoolean True) (lessThan (JsonInteger 1) (JsonInteger 2)),
TestCase $ assertEqual "greater JsonInteger" (JsonBoolean False) (lessThan (JsonInteger 2) (JsonInteger 1)),
TestCase $ assertEqual "equal JsonIntegers" (JsonBoolean False) (lessThan (JsonInteger 1) (JsonInteger 1))
]
-- | Test case for @greaterEqual@ function
testGreaterEqual :: Test
testGreaterEqual = TestList [
TestCase $ assertEqual "greater or equal JsonInteger" (JsonBoolean True) (greaterEqual (JsonInteger 2) (JsonInteger 1)),
TestCase $ assertEqual "not greater or equal JsonInteger" (JsonBoolean False) (greaterEqual (JsonInteger 1) (JsonInteger 2)),
TestCase $ assertEqual "equal JsonIntegers" (JsonBoolean True) (greaterEqual (JsonInteger 1) (JsonInteger 1))
]
-- | Test case for @lessEqual@ function
testLessEqual :: Test
testLessEqual = TestList [
TestCase $ assertEqual "less or equal JsonInteger" (JsonBoolean True) (lessEqual (JsonInteger 1) (JsonInteger 2)),
TestCase $ assertEqual "not less or equal JsonInteger" (JsonBoolean False) (lessEqual (JsonInteger 2) (JsonInteger 1)),
TestCase $ assertEqual "equal JsonIntegers" (JsonBoolean True) (lessEqual (JsonInteger 1) (JsonInteger 1))
]
-- | Test case for @containsSubstring@ function
testContainsSubstring :: Test
testContainsSubstring = TestList [
TestCase $ assertEqual "substring exists" (JsonBoolean True) (containsSubstring (JsonString "hello world") (JsonString "world")),
TestCase $ assertEqual "substring does not exist" (JsonBoolean False) (containsSubstring (JsonString "hello world") (JsonString "foo")),
TestCase $ assertEqual "empty substring" (JsonBoolean False) (containsSubstring (JsonString "hello world") (JsonString "")),
TestCase $ assertEqual "invalid input types" JsonNull (containsSubstring (JsonInteger 1) (JsonString "world"))
]
-- | Test case for @escapeChar@ function
testEscapeChar :: Test
testEscapeChar = TestList [
TestCase $ assertEqual "escape newline" "\\n" (escapeChar '\n'),
TestCase $ assertEqual "escape double quote" "\\\"" (escapeChar '"'),
TestCase $ assertEqual "escape backslash" "\\\\" (escapeChar '\\'),
TestCase $ assertEqual "escape regular character" "a" (escapeChar 'a')
]
-- | Test case for @escapeString@ function
testEscapeString :: Test
testEscapeString = TestList [
TestCase $ assertEqual "Escape empty string" "" (escapeString ""),
TestCase $ assertEqual "Escape string with special characters" "Hello \\\"world\\\"" (escapeString "Hello \"world\"")
]
-- | Test case for @quote@ function
testQuote :: Test
testQuote = TestList [
TestCase $ assertEqual "Quote empty string" "\"\"" (quote ""),
TestCase $ assertEqual "Quote non-empty string" "\"hello\"" (quote "hello")
]
-- | Test case for @renderString@ function
testRenderString :: Test
testRenderString = TestList [
TestCase $ assertEqual "Render empty string" "\"\"" (renderString ""),
TestCase $ assertEqual "Render string with special characters" "\"Hello \\\"world\\\"\"" (renderString "Hello \"world\"")
]
-- | Test case for @sqbracket@ function
testSqbracket :: Test
testSqbracket = TestList [
TestCase $ assertEqual "Sqbracket empty string" "[]" (sqbracket ""),
TestCase $ assertEqual "Sqbracket non-empty string" "[hello]" (sqbracket "hello")
]
-- | Test case for @curlybracket@ function
testCurlybracket :: Test
testCurlybracket = TestList [
TestCase $ assertEqual "Curlybracket empty string" "{}" (curlybracket ""),
TestCase $ assertEqual "Curlybracket non-empty string" "{hello}" (curlybracket "hello")
]
-- | Test case for @renderField@ function
testRenderField :: Test
testRenderField = TestList [
TestCase $ assertEqual "JsonNull" "\"myNull\":null" (renderField ("myNull", JsonNull)),
TestCase $ assertEqual "JsonString" "\"myString\":\"hello\"" (renderField ("myString", JsonString "hello")),
TestCase $ assertEqual "JsonInteger" "\"myInt\":123" (renderField ("myInt", JsonInteger 123)),
TestCase $ assertEqual "JsonBoolean True" "\"myBool\":true" (renderField ("myBool", JsonBoolean True))
]
-- | Test case for @renderJSON@ function
testRenderJSON :: Test
testRenderJSON = TestList [
TestCase $ assertEqual "JsonString" "\"hello\"" (renderJSON (JsonString "hello")),
TestCase $ assertEqual "JsonInteger" "123" (renderJSON (JsonInteger 123)),
TestCase $ assertEqual "JsonBoolean True" "true" (renderJSON (JsonBoolean True)),
TestCase $ assertEqual "JsonNull" "null" (renderJSON JsonNull),
TestCase $ assertEqual "JsonArray" "[1,2,3]" (renderJSON (JsonArray [JsonInteger 1, JsonInteger 2, JsonInteger 3])),
TestCase $ assertEqual "JsonObject" "{\"a\":1,\"b\":2}" (renderJSON (JsonObject [("a", JsonInteger 1), ("b", JsonInteger 2)]))
]
-- Running the tests
main :: IO ()
main = runTestTTAndExit aFailingTest
main = runTestTTAndExit $ TestList [
testConstant,
testString,
testInteger,
testBool,
testJNull,
testSelect,
testBinaryOp,
testPipe,
testGetElements,
testGetField,
testIsTruthy,
testEqual,
testGreaterThan,
testLessThan,
testGreaterEqual,
testLessEqual,
testContainsSubstring,
testEscapeChar,
testEscapeString,
testQuote,
testRenderString,
testSqbracket,
testCurlybracket,
testRenderField,
testRenderJSON
]
\ No newline at end of file