Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File type to track the content and direction of files #5105

Merged
merged 5 commits into from
Apr 18, 2023

Conversation

newhoggy
Copy link
Contributor

@newhoggy newhoggy commented Apr 15, 2023

This is an alternate design to #5017

@newhoggy newhoggy marked this pull request as ready for review April 15, 2023 16:08
@newhoggy newhoggy force-pushed the newhoggy/file-type-2 branch 4 times, most recently from 89986e4 to 730c07e Compare April 15, 2023 16:37
-- contain and whether it is to be used for reading or writing.
newtype File content (direction :: FileDirection) = File
{ unFile :: FilePath
} deriving newtype (Eq, Ord, Read, Show, IsString, FromJSON, ToJSON)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The File type has an additional content type parameter.

@newhoggy newhoggy force-pushed the newhoggy/file-type-2 branch from 46ee037 to 6b12259 Compare April 16, 2023 01:02
@@ -70,7 +71,7 @@ instance AdjustFilePaths NixServiceOptions where
adjustFilePaths f opts
= opts {
_nix_nodeConfigFile = f <$> _nix_nodeConfigFile opts
, _nix_sigKey = SigningKeyFile . f . unSigningKeyFile $ _nix_sigKey opts
, _nix_sigKey = mapFile f $ _nix_sigKey opts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapFile works on any File regardless of file type.

deriving newtype (IsString, Show)
data OfSigningKey

type SigningKeyFile = File OfSigningKey
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of being a newtype with its own set of instances we will use a type alias to File with the file type content already supplied. This means it's possible to use file operation functions like writeByteStringFile directly because they accept any content.

Copy link
Contributor

@Jimbo4350 Jimbo4350 Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid a proliferation of type level tags we can reuse the types in cardano-api and cardano-cli with something like the following:

data Any
type SigningKeyFile = File (SigningKey Any)
type VerificationKeyFile = File (VerificationKey Any)
type TxBodyFile = File (TxBody Any)
type TxFile = File (Tx Any)

Let me know what you think

@@ -90,7 +89,7 @@ prop_migrate_legacy_to_nonlegacy_signingkeys =
]

eSignKey <- liftIO . runExceptT . readByronSigningKey NonLegacyByronKeyFormat
$ SigningKeyFile nonLegacyKeyFp
$ File nonLegacyKeyFp
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use File and Haskell will infer content = OfSigningKey.

Alternatively we can use type applications to be more specific: File @OfSigningKey.

@@ -181,9 +181,9 @@ readLeaderCredentialsSingleton ProtocolFilepaths {shelleyKESFile = Nothing} =
left KESKeyNotSpecified

opCertKesKeyCheck
:: FilePath
:: File () In
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use () for when we previously used FilePath.

This means we're not tracking the content type of the file yet. We can add this later.

writeByteStringFile fp bs = runExceptT $
handleIOExceptT (FileIOError fp) $ BS.writeFile fp bs
handleIOExceptT (FileIOError (unFile fp)) $ BS.writeFile (unFile fp) bs
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unFile is an escape hatch to get the FilePath within. We use it to implement file operations like writeByteStringFile.

=> MonadIO m
=> File content Out
-> ByteString
-> m (Either (FileError ()) ())
Copy link
Contributor Author

@newhoggy newhoggy Apr 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions can work on all files by leaving content unspecified.

coldSK = SigningKeyFile $ dir </> "delegate" ++ strIndex ++ ".skey"
opCertCtr = OpCertCounterFile $ dir </> "delegate" ++ strIndex ++ ".counter"
kesVK = File @OfVerificationKey $ dir </> "delegate" ++ strIndex ++ ".kes.vkey"
coldSK = File @OfSigningKey $ dir </> "delegate" ++ strIndex ++ ".skey"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coldSK is a signing key file that is both written to and read from in the same command. It is therefore annotated as InOut. This type is inferred, but can be made explicit with type application of @InOut.

(VerificationKeyFilePath kesVK)
coldSK
(VerificationKeyFilePath (onlyIn kesVK))
(onlyIn coldSK)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To use an InOut file for an In operation, we can cast it. This works on all file types regardless of content type.

coldSK
opCertCtr
(File @OfVerificationKey $ dir </> "delegate" ++ strIndex ++ ".vkey")
(onlyOut coldSK)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To use an InOut file for an Out operation, we can cast it. This works on all file types regardless of content type.

@@ -1279,7 +1279,7 @@ runQueryLeadershipSchedule (AnyConsensusModeParams cModeParams) network

case mJsonOutputFile of
Nothing -> liftIO $ printLeadershipScheduleAsText schedule eInfo (SystemStart $ sgSystemStart shelleyGenesis)
Just (OutputFile jsonOutputFile) ->
Just (File jsonOutputFile) ->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are parts of the code where we still unwrap the FilePath and use that directly. All these cases can be converted to use the type-safe functions like writeLazyByteString instead. It isn't done here to keep this PR as small as possible.

pByronKeyFile =
(ASigningKeyFile <$> pByronSigningKeyFile)
<|> (AVerificationKeyFile <$> pByronVerificationKeyFile)

pByronSigningKeyFile :: Parser SigningKeyFile
pByronSigningKeyFile :: Parser (SigningKeyFile In)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can now be explicit in the types for parsers to say this is meant for input only. If we use it for Out or for InOut or for the wrong content type, we get a type error.

@newhoggy newhoggy force-pushed the newhoggy/file-type-2 branch 2 times, most recently from 70f40bc to 1937111 Compare April 16, 2023 04:13
@newhoggy newhoggy force-pushed the newhoggy/file-type-2 branch from 1937111 to 75ec1bc Compare April 16, 2023 13:10

-- | A file path with additional type information to indicate what the file is meant to
-- contain and whether it is to be used for reading or writing.
newtype File content (direction :: FileDirection) = File
Copy link
Contributor

@carbolymer carbolymer Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

direction could also be represented as a type-level list [FileDirection] . This way functions requiring file for writing for example could have a following signature:

writeByteStringOutput :: ()
  => MonadIO m
  => Out `CouldBeAnyOf` directions
  => Maybe (File content directions)
  -> ByteString
  -> m (Either (FileError ()) ())

this way conversion of InOut to In or Out would be not necessary and onlyOut functions wouldn't be needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea, but it can be a separate PR.

@carbolymer
Copy link
Contributor

I think I'm missing the value of having content as a type parameter for File . It's not used as a type check for decoding data from files. What are the benefits of having it?

Copy link
Contributor

@Jimbo4350 Jimbo4350 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but just one consideration: #5105 (comment)

deriving newtype (IsString, Show)
data OfSigningKey

type SigningKeyFile = File OfSigningKey
Copy link
Contributor

@Jimbo4350 Jimbo4350 Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid a proliferation of type level tags we can reuse the types in cardano-api and cardano-cli with something like the following:

data Any
type SigningKeyFile = File (SigningKey Any)
type VerificationKeyFile = File (VerificationKey Any)
type TxBodyFile = File (TxBody Any)
type TxFile = File (Tx Any)

Let me know what you think

@newhoggy newhoggy force-pushed the newhoggy/file-type-2 branch 4 times, most recently from e1e75c9 to 6a46d9a Compare April 18, 2023 03:32
@newhoggy
Copy link
Contributor Author

I've used this instead:

type SigningKeyFile = File (SigningKey ())
type VerificationKeyFile = File (VerificationKey ())
type TxBodyFile = File (TxBody ())
type TxFile = File (Tx ())

@newhoggy
Copy link
Contributor Author

newhoggy commented Apr 18, 2023

I think I'm missing the value of having content as a type parameter for File . It's not used as a type check for decoding data from files. What are the benefits of having it?

The same is true of the newtype system it replaces.

The benefits are the same also. If a function takes two different file types you can't accidentally put them in the wrong order.

Having a content type argument means we don't need to have an extra newtype for each file type we want to support and not having them means we don't have to worry about adding new type classes and instances.

There's lots of examples of File () Out still and we can replace more of the () as we go.

@newhoggy newhoggy enabled auto-merge April 18, 2023 07:06
Copy link
Contributor

@deepfire deepfire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @newhoggy !

@newhoggy newhoggy force-pushed the newhoggy/file-type-2 branch from 6a46d9a to 20a31ec Compare April 18, 2023 22:07
@newhoggy newhoggy added this pull request to the merge queue Apr 18, 2023
Merged via the queue into master with commit 26685ca Apr 18, 2023
@iohk-bors iohk-bors bot deleted the newhoggy/file-type-2 branch April 18, 2023 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants