packages feed

nirum-0.4.0: test/Nirum/Constructs/TypeDeclarationSpec.hs

{-# LANGUAGE OverloadedLists, QuasiQuotes #-}
module Nirum.Constructs.TypeDeclarationSpec where


import Data.String.QQ (s)
import qualified Data.Text as T
import Test.Hspec.Meta

import Nirum.Constructs (Construct (toCode))
import Nirum.Constructs.Annotation hiding (docs, fromList, name)
import qualified Nirum.Constructs.Annotation.Internal as AI
import Nirum.Constructs.Declaration (Declaration (name), docs)
import Nirum.Constructs.DeclarationSet hiding (empty)
import Nirum.Constructs.Service (Method (Method), Service (Service))
import Nirum.Constructs.TypeDeclaration ( EnumMember (EnumMember)
                                        , Field (Field)
                                        , JsonType (String)
                                        , PrimitiveTypeIdentifier (Text)
                                        , Tag (Tag)
                                        , Type (..)
                                        , TypeDeclaration (..)
                                        , unionType
                                        )
import Util (singleDocs)

barAnnotationSet :: AnnotationSet
barAnnotationSet = singleton $ Annotation "bar" [("val", AI.Text "baz")]

spec :: Spec
spec = do
    describe "TypeDeclaration" $ do
        context "Alias" $ do
            let alias = Alias "text"
                a = TypeDeclaration { typename = "path"
                                    , type' = alias
                                    , typeAnnotations = empty
                                    }
                b = a { typeAnnotations = singleDocs "docs" }
            specify "name" $ do
                name a `shouldBe` "path"
                name b `shouldBe` "path"
            specify "docs" $ do
                docs a `shouldBe` Nothing
                docs b `shouldBe` Just "docs"
            specify "toCode" $ do
                toCode a `shouldBe` "type path = text;"
                toCode b `shouldBe` "type path = text;\n# docs"
        context "UnboxedType" $ do
            let unboxed = UnboxedType "float64"
                a = TypeDeclaration { typename = "offset"
                                    , type' = unboxed
                                    , typeAnnotations = empty
                                    }
                b = a { typeAnnotations = singleDocs "docs" }
            specify "name" $ do
                name a `shouldBe` "offset"
                name b `shouldBe` "offset"
            specify "docs" $ do
                docs a `shouldBe` Nothing
                docs b `shouldBe` Just "docs"
            specify "toCode" $ do
                toCode a `shouldBe` "unboxed offset (float64);"
                toCode b `shouldBe` "unboxed offset (float64);\n# docs"
        context "EnumType" $ do
            let enumMembers = [ EnumMember "kr" empty
                              , EnumMember "jp" (singleDocs "Japan")
                              , EnumMember "us" (singleDocs "United States")
                              ] :: DeclarationSet EnumMember
                enum = EnumType enumMembers
                a = TypeDeclaration { typename = "country"
                                    , type' = enum
                                    , typeAnnotations = empty
                                    }
                b = a { typeAnnotations = singleDocs "country codes" }
            specify "toCode" $ do
                toCode a `shouldBe` [s|
enum country
    = kr
    | jp
    # Japan
    | us
    # United States
    ;|]
                toCode b `shouldBe` [s|
enum country
    # country codes
    = kr
    | jp
    # Japan
    | us
    # United States
    ;|]
        context "RecordType" $ do
            let fields' = [ Field "name" "text" empty
                          , Field "dob" "date" (singleDocs "date of birth")
                          , Field "gender" "gender" empty
                          ] :: DeclarationSet Field
                record = RecordType fields'
                a = TypeDeclaration { typename = "person"
                                    , type' = record
                                    , typeAnnotations = empty
                                    }
                b = a { typeAnnotations = singleDocs "person record type" }
            specify "toCode" $ do
                toCode a `shouldBe` [s|
record person (
    text name,
    date dob,
    # date of birth
    gender gender,
);|]
                toCode b `shouldBe` [s|
record person (
    # person record type

    text name,
    date dob,
    # date of birth
    gender gender,
);|]
        context "UnionType" $ do
            let circleFields = [ Field "origin" "point" empty
                               , Field "radius" "offset" empty
                               ]
                rectangleFields = [ Field "upper-left" "point" empty
                                  , Field "lower-right" "point" empty
                                  ]
                tags' = [ Tag "circle" circleFields empty
                        , Tag "rectangle" rectangleFields empty
                        , Tag "none" [] empty
                        ]
            let Right union' = unionType tags' Nothing
                a = TypeDeclaration { typename = "shape"
                                    , type' = union'
                                    , typeAnnotations = empty
                                    }
                b = a { typeAnnotations = singleDocs "shape type" }
            specify "toCode" $ do
                toCode a `shouldBe` [s|
union shape
    = circle (point origin, offset radius,)
    | rectangle (point upper-left, point lower-right,)
    | none
    ;|]
                toCode b `shouldBe` [s|
union shape
    # shape type
    = circle (point origin, offset radius,)
    | rectangle (point upper-left, point lower-right,)
    | none
    ;|]
        context "PrimitiveType" $ do
            let primitiveType = PrimitiveType Text String
                decl = TypeDeclaration "text" primitiveType empty
            specify "toCode" $
                T.lines (toCode decl) `shouldSatisfy`
                    all (T.isPrefixOf "//" . T.stripStart)
        context "ServiceDeclaration" $ do
            let nullService = Service []
                nullDecl = ServiceDeclaration "null-service" nullService empty
                nullDecl' =
                    ServiceDeclaration "null-service" nullService
                                       (singleDocs "Null service declaration.")
                pingService = Service
                    [Method "ping" [] (Just "bool") Nothing empty]
                pingDecl = ServiceDeclaration "ping-service" pingService empty
                pingDecl' =
                    ServiceDeclaration "ping-service" pingService
                                       (singleDocs "Ping service declaration.")
                annoDecl = ServiceDeclaration "anno-service" pingService
                                              barAnnotationSet
            specify "toCode" $ do
                toCode nullDecl `shouldBe` "service null-service ();"
                toCode nullDecl' `shouldBe` [s|
service null-service (
    # Null service declaration.
);|]
                toCode pingDecl `shouldBe`
                    "service ping-service (bool ping ());"
                toCode pingDecl' `shouldBe` [s|
service ping-service (
    # Ping service declaration.
    bool ping ()
);|]
                toCode annoDecl `shouldBe`
                    "@bar(val = \"baz\")\nservice anno-service (bool ping ());"
                -- TODO: more tests
        context "Import" $ do
            let import' = Import ["foo", "bar"] "baz" "baz" empty
            let importAliasing = Import ["foo", "bar"] "qux" "baz" empty
            specify "name" $
                name import' `shouldBe` "baz"
            specify "docs" $
                docs import' `shouldBe` Nothing
            specify "toCode" $
                toCode import' `shouldBe` "import foo.bar (baz);\n"
            specify "name" $
                name importAliasing `shouldBe` "qux"
            specify "docs" $
                docs importAliasing `shouldBe` Nothing
            specify "toCode" $
                toCode importAliasing `shouldBe`
                    "import foo.bar (baz as qux);\n"

        context "member/tag name shadowing" $ do
            let fromRight either' = head [v | Right v <- [either']]
            let unionType' t a = fromRight $ unionType t a
            let unionFoo = TypeDeclaration
                    { typename = "foo"
                    , type' = unionType'
                          [ Tag "bar" [Field "a" "text" empty] empty
                          , Tag "baz" [Field "b" "text" empty] empty
                          ]
                          Nothing
                    , typeAnnotations = empty
                    }
            let enumQux = TypeDeclaration
                    { typename = "qux"
                    , type' = EnumType
                          [ EnumMember "quux" empty
                          , EnumMember "bar" empty
                          ]
                    , typeAnnotations = empty
                    }
            let unboxedBar = TypeDeclaration "bar" (UnboxedType "text") empty
            specify "a union tag is disallowed to shadow a type name" $ do
                fromList [unionFoo, unboxedBar]
                    `shouldBe` Left (FacialNameDuplication "bar")
                fromList
                    [ TypeDeclaration
                          { typename = "bar"
                          , type' = unionType'
                                [ Tag "foo" [Field "a" "text" empty] empty
                                , Tag "bar" [Field "b" "text" empty] empty
                                ]
                                Nothing
                          , typeAnnotations = empty
                          }
                    ]
                    `shouldBe` Left (FacialNameDuplication "bar")
            specify "an enum member is disallowed to shadow a type name" $ do
                fromList [enumQux, unboxedBar]
                    `shouldBe` Left (FacialNameDuplication "bar")
                fromList
                    [ TypeDeclaration
                          { typename = "foo"
                          , type' = EnumType
                                [ EnumMember "foo" empty
                                , EnumMember "bar" empty
                                ]
                          , typeAnnotations = empty
                          }
                    ]
                    `shouldBe` Left (FacialNameDuplication "foo")
            it "is disallowed two unions to have a tag of the same name" $
                fromList
                    [ unionFoo
                    , TypeDeclaration
                          { typename = "qux"
                          , type' = unionType'
                                [ Tag "quux" [Field "c" "text" empty] empty
                                , Tag "baz" [Field "d" "text" empty] empty
                                ]
                                Nothing
                          , typeAnnotations = empty
                          }
                    ]
                    `shouldBe` Left (FacialNameDuplication "baz")
            it "is disallowed two enums to have a member of the same name" $
                fromList
                    [ TypeDeclaration
                          { typename = "foo"
                          , type' = EnumType
                                [ EnumMember "bar" empty
                                , EnumMember "baz" empty
                                ]
                          , typeAnnotations = empty
                          }
                    , enumQux
                    ]
                    `shouldBe` Left (FacialNameDuplication "bar")
            it ("is disallowed an enum member and a union tag to " ++
                "have the same name") $
                fromList [unionFoo, enumQux] `shouldBe`
                    Left (FacialNameDuplication "bar")

    describe "EnumMember" $ do
        let kr = EnumMember "kr" empty
            jp = EnumMember "jp" (singleDocs "Japan")
        specify "toCode" $ do
            toCode kr `shouldBe` "kr"
            toCode jp `shouldBe` "jp\n# Japan"
        specify "fromString" $
            "test" `shouldBe` EnumMember "test" empty
    describe "Field" $
        specify "toCode" $ do
            toCode (Field "name" "text" empty) `shouldBe` "text name,"
            toCode (Field "dob" "date" (singleDocs "date of birth")) `shouldBe`
                "date dob,\n# date of birth"
    describe "Tag" $
        specify "toCode" $ do
            let fields' = [ Field "origin" "point" empty
                          , Field "radius" "offset" empty
                          ]
            toCode (Tag "circle" fields' empty)
                `shouldBe` "circle (point origin, offset radius,)"
            toCode (Tag "circle" fields' (singleDocs "docs"))
                `shouldBe` "circle (point origin, offset radius,)\n# docs"
            toCode (Tag "unit" [] empty) `shouldBe` "unit"
            toCode (Tag "unit" [] (singleDocs "docs")) `shouldBe` "unit\n# docs"