From a447f80496193f5ba320fa304baf040257c82435 Mon Sep 17 00:00:00 2001 From: Pablo Lamela Date: Mon, 13 Jan 2025 13:59:48 +0100 Subject: [PATCH] Move data family up one level as in example --- cardano-api/cardano-api.cabal | 1 - .../Cardano/Api/Governance/Actions/CIP119.hs | 128 ------------------ .../Governance/Metadata/DrepRegistration.hs | 128 ++++++++++++++---- .../Api/Governance/Metadata/GovAction.hs | 43 +++--- .../Api/Governance/Metadata/Validation.hs | 13 +- cardano-api/src/Cardano/Api.hs | 2 +- .../Test/Cardano/Api/GovAnchorValidation.hs | 12 +- 7 files changed, 143 insertions(+), 184 deletions(-) delete mode 100644 cardano-api/internal/Cardano/Api/Governance/Actions/CIP119.hs diff --git a/cardano-api/cardano-api.cabal b/cardano-api/cardano-api.cabal index ebcd1a6bc5..c00d34d2d7 100644 --- a/cardano-api/cardano-api.cabal +++ b/cardano-api/cardano-api.cabal @@ -91,7 +91,6 @@ library internal Cardano.Api.GeneralParsers Cardano.Api.Genesis Cardano.Api.GenesisParameters - Cardano.Api.Governance.Actions.CIP119 Cardano.Api.Governance.Actions.ProposalProcedure Cardano.Api.Governance.Actions.VotingProcedure Cardano.Api.Governance.Metadata.DrepRegistration diff --git a/cardano-api/internal/Cardano/Api/Governance/Actions/CIP119.hs b/cardano-api/internal/Cardano/Api/Governance/Actions/CIP119.hs deleted file mode 100644 index 6bb085952d..0000000000 --- a/cardano-api/internal/Cardano/Api/Governance/Actions/CIP119.hs +++ /dev/null @@ -1,128 +0,0 @@ -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE InstanceSigs #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE TypeFamilies #-} - -module Cardano.Api.Governance.Actions.CIP119 (CIP119 (..)) where - -import Cardano.Api.GeneralParsers (textWithMaxLength) -import Cardano.Api.Governance.Metadata.Validation (GovActionMetadata) - -import Data.Aeson (FromJSON, withObject, (.:), (.:?)) -import qualified Data.Aeson as Aeson -import Data.Aeson.Types (Parser) -import Data.Text (Text) -import GHC.Generics (Generic) - -data CIP119 = CIP119 - --- * DRep metadata validation - --- | Root document: GovActionMetadata CIP119 -data instance GovActionMetadata CIP119 = CIP119Common - { hashAlgorithm :: HashAlgorithm - , body :: Body - } - deriving (Show, Generic) - -instance FromJSON (GovActionMetadata CIP119) where - parseJSON :: Aeson.Value -> Parser (GovActionMetadata CIP119) - parseJSON = withObject "CIP119Common" $ \v -> - CIP119Common - <$> v .: "hashAlgorithm" - <*> v .: "body" - --- Hash Algorithm (Enum) -data HashAlgorithm = Blake2b256 - deriving (Show, Generic) - -instance FromJSON HashAlgorithm where - parseJSON :: Aeson.Value -> Parser HashAlgorithm - parseJSON = Aeson.withText "HashAlgorithm" $ - \case - "blake2b-256" -> return Blake2b256 - _ -> fail "Invalid hashAlgorithm, it must be: blake2b-256" - --- Body of the metadata document -data Body = Body - { paymentAddress :: Maybe Text - , givenName :: Text - , image :: Maybe ImageObject - , objectives :: Maybe Text - , motivations :: Maybe Text - , qualifications :: Maybe Text - , doNotList :: Maybe DoNotList - , references :: Maybe [Reference] - } - deriving (Show, Generic) - -instance FromJSON Body where - parseJSON :: Aeson.Value -> Parser Body - parseJSON = withObject "Body" $ \v -> - Body - <$> v .:? "paymentAddress" - <*> (v .: "givenName" >>= textWithMaxLength "givenName" 80) - <*> v .:? "image" - <*> (v .:? "objectives" >>= traverse (textWithMaxLength "objectives" 1000)) - <*> (v .:? "motivations" >>= traverse (textWithMaxLength "motivations" 1000)) - <*> (v .:? "qualifications" >>= traverse (textWithMaxLength "qualifications" 1000)) - <*> v .:? "doNotList" - <*> v .:? "references" - --- Profile picture -data ImageObject = ImageObject - { contentUrl :: Text -- Base64 encoded image or URL - , sha256 :: Maybe Text -- Only present for URL images - } - deriving (Show, Generic) - -instance FromJSON ImageObject where - parseJSON :: Aeson.Value -> Parser ImageObject - parseJSON = withObject "ImageObject" $ \v -> - ImageObject - <$> v .: "contentUrl" - <*> v .:? "sha256" - --- DoNotList Enum -data DoNotList = DoNotListTrue | DoNotListFalse - deriving (Show, Generic) - -instance FromJSON DoNotList where - parseJSON :: Aeson.Value -> Parser DoNotList - parseJSON = Aeson.withText "DoNotList" $ - \case - "true" -> return DoNotListTrue - "false" -> return DoNotListFalse - _ -> fail "Invalid doNotList value, must be one of: true, false" - --- Reference type -data Reference = Reference - { refType :: ReferenceType - , label :: Text - , uri :: Text - } - deriving (Show, Generic) - -instance FromJSON Reference where - parseJSON :: Aeson.Value -> Parser Reference - parseJSON = withObject "Reference" $ \v -> - Reference - <$> v .: "@type" - <*> v .: "label" - <*> v .: "uri" - --- ReferenceType Enum -data ReferenceType = GovernanceMetadata | Other | Link | Identity - deriving (Show, Generic) - -instance FromJSON ReferenceType where - parseJSON :: Aeson.Value -> Parser ReferenceType - parseJSON = Aeson.withText "ReferenceType" $ - \case - "GovernanceMetadata" -> return GovernanceMetadata - "Other" -> return Other - "Link" -> return Link - "Identity" -> return Identity - _ -> - fail "Invalid reference type, must be one of: GovernanceMetadata, Other, Link, Identity" diff --git a/cardano-api/internal/Cardano/Api/Governance/Metadata/DrepRegistration.hs b/cardano-api/internal/Cardano/Api/Governance/Metadata/DrepRegistration.hs index 5ea9edd625..7b758233b7 100644 --- a/cardano-api/internal/Cardano/Api/Governance/Metadata/DrepRegistration.hs +++ b/cardano-api/internal/Cardano/Api/Governance/Metadata/DrepRegistration.hs @@ -1,44 +1,120 @@ -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE TypeFamilies #-} -module Cardano.Api.Governance.Metadata.DrepRegistration where +module Cardano.Api.Governance.Metadata.DrepRegistration (CIP119 (..)) where +import Cardano.Api.GeneralParsers (textWithMaxLength) +import Cardano.Api.Governance.Metadata.Validation (GovActionMetadata (..), HashAlgorithm, Body, Authors) + +import Data.Aeson (FromJSON, withObject, (.:), (.:?)) +import qualified Data.Aeson as Aeson +import Data.Aeson.Types (Parser) import Data.Text (Text) +import GHC.Generics (Generic) ---------------------------------- --- EXISTS IN A SEPARATE MODULE -- ---------------------------------- +data CIP119 = DrepRegistrationMetadata +instance FromJSON (GovActionMetadata CIP119) where + parseJSON :: Aeson.Value -> Parser (GovActionMetadata CIP119) + parseJSON = withObject "CIP119Common" $ \v -> + GovActionMetadata + <$> v .: "hashAlgorithm" + <*> pure Absent + <*> v .: "body" --- GovActionMetadata' Needs to be extended --- to all fields of CIP-100 -data GovActionMetadata' cip - = GovActionMetadata' - { body :: Body' cip - } +-- Hash Algorithm (Enum) +data instance HashAlgorithm CIP119 = Blake2b256 + deriving (Show, Generic) -data family Body' cip +instance FromJSON (HashAlgorithm CIP119) where + parseJSON :: Aeson.Value -> Parser (HashAlgorithm CIP119) + parseJSON = Aeson.withText "HashAlgorithm" $ + \case + "blake2b-256" -> return Blake2b256 + _ -> fail "Invalid hashAlgorithm, it must be: blake2b-256" ---------------------------------- ---------------------------------- +-- Body of the metadata document +data instance Body CIP119 = Body + { paymentAddress :: Maybe Text + , givenName :: Text + , image :: Maybe ImageObject + , objectives :: Maybe Text + , motivations :: Maybe Text + , qualifications :: Maybe Text + , doNotList :: Maybe DoNotList + , references :: Maybe [Reference] + } + deriving (Show, Generic) --- Everything below exists in this module -data CIP108 = DrepRegistrationMetadata +instance FromJSON (Body CIP119) where + parseJSON :: Aeson.Value -> Parser (Body CIP119) + parseJSON = withObject "Body" $ \v -> + Body + <$> v .:? "paymentAddress" + <*> (v .: "givenName" >>= textWithMaxLength "givenName" 80) + <*> v .:? "image" + <*> (v .:? "objectives" >>= traverse (textWithMaxLength "objectives" 1000)) + <*> (v .:? "motivations" >>= traverse (textWithMaxLength "motivations" 1000)) + <*> (v .:? "qualifications" >>= traverse (textWithMaxLength "qualifications" 1000)) + <*> v .:? "doNotList" + <*> v .:? "references" -data instance Body' CIP108 = Body - { title :: Text - , abstract :: Text - , motivation :: Text - , rationale :: Text - , references :: Maybe [Reference] +-- Profile picture +data ImageObject = ImageObject + { contentUrl :: Text -- Base64 encoded image or URL + , sha256 :: Maybe Text -- Only present for URL images } + deriving (Show, Generic) +instance FromJSON ImageObject where + parseJSON :: Aeson.Value -> Parser ImageObject + parseJSON = withObject "ImageObject" $ \v -> + ImageObject + <$> v .: "contentUrl" + <*> v .:? "sha256" +-- DoNotList Enum +data DoNotList = DoNotListTrue | DoNotListFalse + deriving (Show, Generic) +instance FromJSON DoNotList where + parseJSON :: Aeson.Value -> Parser DoNotList + parseJSON = Aeson.withText "DoNotList" $ + \case + "true" -> return DoNotListTrue + "false" -> return DoNotListFalse + _ -> fail "Invalid doNotList value, must be one of: true, false" + +-- Reference type data Reference = Reference { refType :: ReferenceType , label :: Text , uri :: Text } - deriving Show + deriving (Show, Generic) + +instance FromJSON Reference where + parseJSON :: Aeson.Value -> Parser Reference + parseJSON = withObject "Reference" $ \v -> + Reference + <$> v .: "@type" + <*> v .: "label" + <*> v .: "uri" + +-- ReferenceType Enum +data ReferenceType = GovernanceMetadata | Other | Link | Identity + deriving (Show, Generic) + +instance FromJSON ReferenceType where + parseJSON :: Aeson.Value -> Parser ReferenceType + parseJSON = Aeson.withText "ReferenceType" $ + \case + "GovernanceMetadata" -> return GovernanceMetadata + "Other" -> return Other + "Link" -> return Link + "Identity" -> return Identity + _ -> + fail "Invalid reference type, must be one of: GovernanceMetadata, Other, Link, Identity" -data ReferenceType = GovernanceMetadata | Other - deriving Show +-- We don't need to validate Authors because it is optional in CIP-119 +data instance Authors CIP119 = Absent diff --git a/cardano-api/internal/Cardano/Api/Governance/Metadata/GovAction.hs b/cardano-api/internal/Cardano/Api/Governance/Metadata/GovAction.hs index cc857b5142..c4bf3b8078 100644 --- a/cardano-api/internal/Cardano/Api/Governance/Metadata/GovAction.hs +++ b/cardano-api/internal/Cardano/Api/Governance/Metadata/GovAction.hs @@ -7,44 +7,46 @@ module Cardano.Api.Governance.Metadata.GovAction (CIP108 (..)) where import Cardano.Api.GeneralParsers (textWithMaxLength) -import Cardano.Api.Governance.Metadata.Validation (GovActionMetadata) +import Cardano.Api.Governance.Metadata.Validation (GovActionMetadata (..), HashAlgorithm, Authors, Body) -import Data.Aeson (FromJSON, withObject, withText, (.:), (.:?)) +import Data.Aeson (FromJSON, withObject, withText, (.:), (.:?), withArray) import qualified Data.Aeson as Aeson -import Data.Aeson.Types (Parser, Value) +import Data.Aeson.Types (Parser, Value (..)) import Data.Text (Text) import GHC.Generics (Generic) -data CIP108 = CIP108 - --- Root object: GovActionMetadata CIP108 -data instance GovActionMetadata CIP108 = CIP108Common - { hashAlgorithm :: HashAlgorithm - , authors :: [Author] - , body :: Body - } - deriving (Show, Generic) +data CIP108 = BaseGovActionMetadata instance FromJSON (GovActionMetadata CIP108) where parseJSON :: Value -> Parser (GovActionMetadata CIP108) parseJSON = withObject "CIP108Common" $ \v -> - CIP108Common + GovActionMetadata <$> v .: "hashAlgorithm" <*> v .: "authors" <*> v .: "body" -- Enum for HashAlgorithm -data HashAlgorithm = Blake2b256 + +data instance HashAlgorithm CIP108 = Blake2b256 deriving (Show, Generic) -instance FromJSON HashAlgorithm where - parseJSON :: Value -> Parser HashAlgorithm +instance FromJSON (HashAlgorithm CIP108) where + parseJSON :: Value -> Parser (HashAlgorithm CIP108) parseJSON = withText "HashAlgorithm" $ \case "blake2b-256" -> return Blake2b256 _ -> fail "Invalid hashAlgorithm value, must be: blake2b-256" -- Author object + +newtype instance Authors CIP108 = Authors [Author] + deriving (Show, Generic) + +instance FromJSON (Authors CIP108) where + parseJSON :: Value -> Parser (Authors CIP108) + parseJSON = withArray "Authors" $ \arr -> + Authors <$> Aeson.parseJSON (Array arr) + data Author = Author { name :: Maybe Text , witness :: Witness @@ -87,7 +89,8 @@ instance FromJSON WitnessAlgorithm where _ -> fail "Invalid witnessAlgorithm value, must be: ed25519 or CIP-0008" -- Body of the metadata document -data Body = Body + +data instance Body CIP108 = Body { title :: Text , abstract :: Text , motivation :: Text @@ -96,8 +99,8 @@ data Body = Body } deriving (Show, Generic) -instance FromJSON Body where - parseJSON :: Value -> Parser Body +instance FromJSON (Body CIP108) where + parseJSON :: Value -> Parser (Body CIP108) parseJSON = withObject "Body" $ \v -> Body <$> (v .: "title" >>= textWithMaxLength "title" 80) @@ -139,7 +142,7 @@ instance FromJSON ReferenceType where -- ReferenceHash object data ReferenceHash = ReferenceHash { referenceHashDigest :: Text - , referenceHashAlgorithm :: HashAlgorithm + , referenceHashAlgorithm :: HashAlgorithm CIP108 } deriving (Show, Generic) diff --git a/cardano-api/internal/Cardano/Api/Governance/Metadata/Validation.hs b/cardano-api/internal/Cardano/Api/Governance/Metadata/Validation.hs index 3c2def4251..66ab23763c 100644 --- a/cardano-api/internal/Cardano/Api/Governance/Metadata/Validation.hs +++ b/cardano-api/internal/Cardano/Api/Governance/Metadata/Validation.hs @@ -2,13 +2,22 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} -module Cardano.Api.Governance.Metadata.Validation (GovActionMetadata, validateGovActionAnchorData) where +module Cardano.Api.Governance.Metadata.Validation (GovActionMetadata(..), Authors, Body, HashAlgorithm, validateGovActionAnchorData) where import Data.Aeson (FromJSON, eitherDecodeStrict) import Data.ByteString (ByteString) import Data.Either.Combinators (mapRight) -data family GovActionMetadata cip +data GovActionMetadata cip + = GovActionMetadata + { hashAlgorithm :: HashAlgorithm cip + , authors :: Authors cip + , body :: Body cip + } + +data family Authors cip +data family Body cip +data family HashAlgorithm cip validateGovActionAnchorData :: forall cip. FromJSON (GovActionMetadata cip) => cip -> ByteString -> Either String () diff --git a/cardano-api/src/Cardano/Api.hs b/cardano-api/src/Cardano/Api.hs index 44cf3a2da0..7205445759 100644 --- a/cardano-api/src/Cardano/Api.hs +++ b/cardano-api/src/Cardano/Api.hs @@ -1092,7 +1092,7 @@ import Cardano.Api.Feature import Cardano.Api.Fees import Cardano.Api.Genesis import Cardano.Api.GenesisParameters -import Cardano.Api.Governance.Actions.CIP119 (CIP119 (..)) +import Cardano.Api.Governance.Metadata.DrepRegistration (CIP119 (..)) import Cardano.Api.Governance.Actions.ProposalProcedure import Cardano.Api.Governance.Metadata.GovAction (CIP108 (..)) import Cardano.Api.Governance.Metadata.Validation diff --git a/cardano-api/test/cardano-api-test/Test/Cardano/Api/GovAnchorValidation.hs b/cardano-api/test/cardano-api-test/Test/Cardano/Api/GovAnchorValidation.hs index b73f07be3c..27c78df29f 100644 --- a/cardano-api/test/cardano-api-test/Test/Cardano/Api/GovAnchorValidation.hs +++ b/cardano-api/test/cardano-api-test/Test/Cardano/Api/GovAnchorValidation.hs @@ -56,7 +56,7 @@ prop_positive_drep_registration_json = propertyOnce $ do :: File DRepMetadata In ) value <- H.evalEither eitherValue - validateGovActionAnchorData CIP119 value === Right () + validateGovActionAnchorData DrepRegistrationMetadata value === Right () prop_missing_given_name_drep_registration_json :: Property prop_missing_given_name_drep_registration_json = propertyOnce $ do @@ -66,7 +66,7 @@ prop_missing_given_name_drep_registration_json = propertyOnce $ do :: File DRepMetadata In ) value <- H.evalEither eitherValue - validateGovActionAnchorData CIP119 value === Left "Error in $.body: key \"givenName\" not found" + validateGovActionAnchorData DrepRegistrationMetadata value === Left "Error in $.body: key \"givenName\" not found" prop_given_name_too_long_drep_registration_json :: Property prop_given_name_too_long_drep_registration_json = propertyOnce $ do @@ -76,7 +76,7 @@ prop_given_name_too_long_drep_registration_json = propertyOnce $ do :: File DRepMetadata In ) value <- H.evalEither eitherValue - validateGovActionAnchorData CIP119 value + validateGovActionAnchorData DrepRegistrationMetadata value === Left "Error in $.body: key \"givenName\" exceeds maximum length of 80 characters. Got length: 90" prop_positive_no_confidence_json :: Property @@ -87,7 +87,7 @@ prop_positive_no_confidence_json = propertyOnce $ do :: File DRepMetadata In ) value <- H.evalEither eitherValue - validateGovActionAnchorData CIP108 value === Right () + validateGovActionAnchorData BaseGovActionMetadata value === Right () prop_positive_treasury_withdrawal_json :: Property prop_positive_treasury_withdrawal_json = propertyOnce $ do @@ -97,7 +97,7 @@ prop_positive_treasury_withdrawal_json = propertyOnce $ do :: File DRepMetadata In ) value <- H.evalEither eitherValue - validateGovActionAnchorData CIP108 value === Right () + validateGovActionAnchorData BaseGovActionMetadata value === Right () prop_title_name_too_long_treasury_withdrawal_json :: Property prop_title_name_too_long_treasury_withdrawal_json = propertyOnce $ do @@ -107,5 +107,5 @@ prop_title_name_too_long_treasury_withdrawal_json = propertyOnce $ do :: File DRepMetadata In ) value <- H.evalEither eitherValue - validateGovActionAnchorData CIP108 value + validateGovActionAnchorData BaseGovActionMetadata value === Left "Error in $.body: key \"title\" exceeds maximum length of 80 characters. Got length: 112"