packages feed

notion-client-0.8.0.0: src/Notion/V1/Common.hs

-- | Common Notion API types
module Notion.V1.Common
  ( -- * Common types
    UUID (..),
    BlockID,
    ObjectType (..),
    Parent (..),
    ParentID,
    Color (..),
    Icon (..),
    NoticonColor (..),
    Cover (..),
    File (..),
    ExternalFile (..),
    CustomEmojiRef (..),
  )
where

import Data.Aeson (Object, object, withText, (.:), (.:?), (.=))
import Data.Aeson.Types (Parser)
import Data.Foldable (asum)
import Data.Maybe (fromMaybe)
import Data.Tuple (swap)
import Notion.Prelude

-- | UUID type for Notion resource IDs
newtype UUID = UUID {text :: Text}
  deriving newtype (Eq, FromJSON, IsString, Show, ToHttpApiData, ToJSON)

-- | Block ID
type BlockID = UUID

-- | Possible Notion object types
data ObjectType
  = Database
  | DataSource
  | Page
  | Block
  | User
  | Comment
  | View
  | -- | @file_upload@
    FileUploadObjectType
  | -- | @page_markdown@
    PageMarkdownObjectType
  | -- | @async_task@
    AsyncTaskObjectType
  | -- | @group@
    GroupObjectType
  | -- | An object type this library does not know yet; holds the raw string.
    UnknownObjectType Text
  deriving stock (Eq, Show, Generic)

objectTypeNames :: [(ObjectType, Text)]
objectTypeNames =
  [ (Database, "database"),
    (DataSource, "data_source"),
    (Page, "page"),
    (Block, "block"),
    (User, "user"),
    (Comment, "comment"),
    (View, "view"),
    (FileUploadObjectType, "file_upload"),
    (PageMarkdownObjectType, "page_markdown"),
    (AsyncTaskObjectType, "async_task"),
    (GroupObjectType, "group")
  ]

instance FromJSON ObjectType where
  parseJSON = withText "ObjectType" $ \t ->
    pure (fromMaybe (UnknownObjectType t) (lookup t (map swap objectTypeNames)))

instance ToJSON ObjectType where
  toJSON = \case
    UnknownObjectType t -> String t
    o -> String (fromMaybe "" (lookup o objectTypeNames))

-- | Parent object that can be a database, data source, page, block, or workspace
data Parent
  = DatabaseParent {databaseId :: UUID}
  | DataSourceParent {dataSourceId :: UUID, parentDatabaseId :: Maybe UUID}
  | PageParent {pageId :: UUID}
  | BlockParent {blockId :: UUID}
  | WorkspaceParent {workspace :: Bool}
  | AgentParent {agentId :: UUID}
  | -- | A parent kind this library does not model yet; holds the raw JSON object.
    UnknownParent Value
  deriving stock (Generic, Show)

instance FromJSON Parent where
  parseJSON = \case
    Object o -> do
      mParentType <- o .:? "type"
      case mParentType of
        Just parentType -> parseByType parentType o
        Nothing -> parseByKey o
    _ -> fail "Expected object for Parent"
    where
      parseByType :: Text -> Object -> Parser Parent
      parseByType = \case
        "database" -> fmap DatabaseParent . (.: "database_id")
        "database_id" -> fmap DatabaseParent . (.: "database_id")
        "data_source" -> \o -> DataSourceParent <$> o .: "data_source_id" <*> o .:? "database_id"
        "data_source_id" -> \o -> DataSourceParent <$> o .: "data_source_id" <*> o .:? "database_id"
        "page" -> fmap PageParent . (.: "page_id")
        "page_id" -> fmap PageParent . (.: "page_id")
        "block" -> fmap BlockParent . (.: "block_id")
        "block_id" -> fmap BlockParent . (.: "block_id")
        "workspace" -> fmap WorkspaceParent . (.: "workspace")
        "agent_id" -> fmap AgentParent . (.: "agent_id")
        _ -> pure . UnknownParent . Object

      parseByKey :: Object -> Parser Parent
      parseByKey o =
        asum
          [ DataSourceParent <$> o .: "data_source_id" <*> o .:? "database_id",
            DatabaseParent <$> o .: "database_id",
            PageParent <$> o .: "page_id",
            BlockParent <$> o .: "block_id",
            AgentParent <$> o .: "agent_id",
            WorkspaceParent <$> o .: "workspace",
            pure (UnknownParent (Object o))
          ]

instance ToJSON Parent where
  toJSON (DatabaseParent dbId) = object ["type" .= ("database_id" :: Text), "database_id" .= dbId]
  toJSON (DataSourceParent dsId mDbId) =
    object $
      ["type" .= ("data_source_id" :: Text), "data_source_id" .= dsId]
        <> maybe [] (\dbId -> ["database_id" .= dbId]) mDbId
  toJSON (PageParent pId) = object ["type" .= ("page_id" :: Text), "page_id" .= pId]
  toJSON (BlockParent bId) = object ["type" .= ("block_id" :: Text), "block_id" .= bId]
  toJSON (WorkspaceParent ws) = object ["type" .= ("workspace" :: Text), "workspace" .= ws]
  toJSON (AgentParent aId) = object ["type" .= ("agent_id" :: Text), "agent_id" .= aId]
  toJSON (UnknownParent v) = v

-- | Unified parent ID type
type ParentID = UUID

-- | Notion color options
data Color
  = Default
  | Gray
  | Brown
  | Orange
  | Yellow
  | Green
  | Blue
  | Purple
  | Pink
  | Red
  | DefaultBackground
  | GrayBackground
  | BrownBackground
  | OrangeBackground
  | YellowBackground
  | GreenBackground
  | BlueBackground
  | PurpleBackground
  | PinkBackground
  | RedBackground
  | -- | A color this library does not know yet; holds the raw string.
    UnknownColor Text
  deriving stock (Eq, Show, Generic)

colorNames :: [(Color, Text)]
colorNames =
  [ (Default, "default"),
    (Gray, "gray"),
    (Brown, "brown"),
    (Orange, "orange"),
    (Yellow, "yellow"),
    (Green, "green"),
    (Blue, "blue"),
    (Purple, "purple"),
    (Pink, "pink"),
    (Red, "red"),
    (DefaultBackground, "default_background"),
    (GrayBackground, "gray_background"),
    (BrownBackground, "brown_background"),
    (OrangeBackground, "orange_background"),
    (YellowBackground, "yellow_background"),
    (GreenBackground, "green_background"),
    (BlueBackground, "blue_background"),
    (PurpleBackground, "purple_background"),
    (PinkBackground, "pink_background"),
    (RedBackground, "red_background")
  ]

instance FromJSON Color where
  parseJSON = withText "Color" $ \t ->
    pure (fromMaybe (UnknownColor t) (lookup t (map swap colorNames)))

instance ToJSON Color where
  toJSON = \case
    UnknownColor t -> String t
    c -> String (fromMaybe "default" (lookup c colorNames))

-- | Icon object for pages/databases
data Icon
  = EmojiIcon {emoji :: Text}
  | FileIcon {file :: File}
  | ExternalIcon {external :: ExternalFile}
  | -- | Native icon specified by name and optional color
    NativeIcon {iconName :: Text, iconColor :: Maybe NoticonColor}
  | -- | Custom emoji icon. Responses carry the emoji's name and URL; requests
    -- need only its ID.
    CustomEmojiIcon {customEmoji :: CustomEmojiRef}
  | -- | File upload icon referenced by upload ID
    FileUploadIcon {fileUploadId :: UUID}
  | -- | An icon kind this library does not model yet; holds the raw icon object.
    UnknownIcon Value
  deriving stock (Eq, Generic, Show)

instance FromJSON Icon where
  parseJSON = \case
    Object o -> do
      iconType :: Text <- o .: "type"
      case iconType of
        "emoji" -> EmojiIcon <$> o .: "emoji"
        "file" -> FileIcon <$> o .: "file"
        "external" -> ExternalIcon <$> o .: "external"
        "icon" -> do
          inner <- o .: "icon"
          NativeIcon <$> inner .: "name" <*> inner .:? "color"
        "custom_emoji" -> do
          mInner <- o .:? "custom_emoji"
          case mInner of
            Just inner -> CustomEmojiIcon <$> parseJSON inner
            -- Shape written by notion-client <= 0.7.0.2; still accepted when reading.
            Nothing -> (\i -> CustomEmojiIcon (CustomEmojiRef i Nothing Nothing)) <$> o .: "id"
        "file_upload" -> do
          uploadObj <- o .: "file_upload"
          FileUploadIcon <$> uploadObj .: "id"
        _ -> pure (UnknownIcon (Object o))
    _ -> fail "Expected object for Icon"

instance ToJSON Icon where
  toJSON (EmojiIcon emoji) = object ["type" .= ("emoji" :: Text), "emoji" .= emoji]
  toJSON (FileIcon file) = object ["type" .= ("file" :: Text), "file" .= file]
  toJSON (ExternalIcon external) = object ["type" .= ("external" :: Text), "external" .= external]
  toJSON (NativeIcon name color) =
    object
      [ "type" .= ("icon" :: Text),
        "icon" .= object (["name" .= name] <> maybe [] (\c -> ["color" .= c]) color)
      ]
  toJSON (CustomEmojiIcon ref) =
    object ["type" .= ("custom_emoji" :: Text), "custom_emoji" .= ref]
  toJSON (FileUploadIcon uid) = object ["type" .= ("file_upload" :: Text), "file_upload" .= object ["id" .= uid]]
  toJSON (UnknownIcon v) = v

-- | Color variant of a Notion native icon.
data NoticonColor
  = NoticonGray
  | NoticonLightgray
  | NoticonBrown
  | NoticonYellow
  | NoticonOrange
  | NoticonGreen
  | NoticonBlue
  | NoticonPurple
  | NoticonPink
  | NoticonRed
  | -- | A color this library does not know yet; holds the raw string.
    UnknownNoticonColor Text
  deriving stock (Eq, Generic, Show)

noticonColorNames :: [(NoticonColor, Text)]
noticonColorNames =
  [ (NoticonGray, "gray"),
    (NoticonLightgray, "lightgray"),
    (NoticonBrown, "brown"),
    (NoticonYellow, "yellow"),
    (NoticonOrange, "orange"),
    (NoticonGreen, "green"),
    (NoticonBlue, "blue"),
    (NoticonPurple, "purple"),
    (NoticonPink, "pink"),
    (NoticonRed, "red")
  ]

instance FromJSON NoticonColor where
  parseJSON = withText "NoticonColor" $ \t ->
    pure (fromMaybe (UnknownNoticonColor t) (lookup t (map swap noticonColorNames)))

instance ToJSON NoticonColor where
  toJSON = \case
    UnknownNoticonColor t -> String t
    c -> String (fromMaybe "" (lookup c noticonColorNames))

-- | Reference to a workspace custom emoji. Responses always include 'name'
-- and 'url'; requests may send only the ID.
data CustomEmojiRef = CustomEmojiRef
  { id :: UUID,
    name :: Maybe Text,
    url :: Maybe Text
  }
  deriving stock (Eq, Generic, Show)

instance FromJSON CustomEmojiRef where
  parseJSON = genericParseJSON aesonOptions

instance ToJSON CustomEmojiRef where
  toJSON = genericToJSON aesonOptions

-- | Cover object for pages/databases
data Cover
  = FileCover {file :: File}
  | ExternalCover {external :: ExternalFile}
  | FileUploadCover {fileUploadId :: UUID}
  deriving stock (Eq, Generic, Show)

instance FromJSON Cover where
  parseJSON = \case
    Object o -> do
      coverType <- o .: "type"
      case coverType of
        "file" -> FileCover <$> o .: "file"
        "external" -> ExternalCover <$> o .: "external"
        "file_upload" -> do
          uploadObj <- o .: "file_upload"
          FileUploadCover <$> uploadObj .: "id"
        _ -> fail $ "Unknown cover type: " <> unpack coverType
    _ -> fail "Expected object for Cover"

instance ToJSON Cover where
  toJSON (FileCover file) = object ["type" .= ("file" :: Text), "file" .= file]
  toJSON (ExternalCover external) = object ["type" .= ("external" :: Text), "external" .= external]
  toJSON (FileUploadCover uid) = object ["type" .= ("file_upload" :: Text), "file_upload" .= object ["id" .= uid]]

-- | Internal file object
data File = File
  { url :: Text,
    expiryTime :: Maybe POSIXTime
  }
  deriving stock (Eq, Generic, Show)

instance FromJSON File where
  parseJSON = \case
    Object o -> do
      url <- o .: "url"
      mExpiry <- o .:? "expiry_time"
      expiryTime <- case mExpiry of
        Nothing -> pure Nothing
        Just str -> Just <$> parseISO8601 str
      pure File {..}
    _ -> fail "Expected object for File"

instance ToJSON File where
  toJSON File {..} =
    object $
      ["url" .= url]
        <> maybe [] (\t -> ["expiry_time" .= posixToISO8601 t]) expiryTime

-- | External file object
newtype ExternalFile = ExternalFile
  { url :: Text
  }
  deriving stock (Eq, Generic, Show)

instance FromJSON ExternalFile where
  parseJSON = genericParseJSON aesonOptions

instance ToJSON ExternalFile where
  toJSON = genericToJSON aesonOptions