Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.HttpClient
open Axial.PlatformService
open Axial.Process
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScriptAxial.FileSystem turns file access into a declared dependency with a typed failure channel. Where
File.ReadAllText throws one of a dozen exception types, FileSystem.readAllText returns
Flow<'env, FileSystemError, string> — the ways it can fail are part of the signature.
open Axial.FileSystem
let loadConfig path : Flow<#IHasFileSystem, FileSystemError, Config> =
flow {
let! text = FileSystem.readAllText path
return parseConfig text
}IFileSystem is supplied the same way as any other explicit service:
type AppEnv =
{ FileSystem: IFileSystem }
interface IHasFileSystem with
member this.FileSystem = this.FileSystem
let! exit = loadConfig "app.json" |> Flow.startTask { FileSystem = FileSystem.live }Typed errors
Every operation fails with FileSystemError, a union that classifies what went wrong:
| Case | Raised when |
|---|---|
FileNotFound path |
The file does not exist |
DirectoryNotFound path |
A directory in the path does not exist |
AlreadyExists path |
The target path is already taken |
Unauthorized (path, message) |
The process lacks permission |
InvalidPath (path, message) |
The path is malformed |
PathTooLong (path, message) |
The platform rejected the path length |
Canceled message |
The operation was interrupted |
Io (path, message) |
A general I/O failure |
Unsupported (path, message) |
The platform or path shape does not support the operation |
Unexpected (path, message) |
Anything else that escaped the operation |
Because failures are typed, recovery is a match rather than an exception filter:
let loadOrDefault path =
loadConfig path
|> Flow.orElseWith (function
| FileSystemError.FileNotFound _ -> Flow.succeed Config.defaults
| error -> Flow.fail error)Files
Whole-file reads and writes come in text, line, and byte forms, each with an encoding-explicit and an asynchronous variant:
FileSystem.readAllText path
FileSystem.readAllTextWithEncoding Encoding.UTF8 path
FileSystem.readAllTextAsync path
FileSystem.readAllLines path
FileSystem.readAllBytes path
FileSystem.writeAllText path contents
FileSystem.writeAllLines path lines
FileSystem.writeAllBytes path bytes
FileSystem.appendAllText path contentsfileExists, exists, deleteFile, copyFile, and moveFile cover the rest of the common surface, and file
metadata has getters and setters for attributes and the creation, last-access, and last-write times in both local and
UTC forms — getFileLastWriteTimeUtc, setFileAttributes, and so on.
Symbolic links are first class: createFileSymbolicLink, createDirectorySymbolicLink, getSymbolicLinkTarget
(which returns None when the path is not a link), and resolveSymbolicLinkTarget, whose boolean argument decides
whether to follow the whole chain or stop at the immediate target.
Streams and scopes
openRead, openText, openWrite, createFile, createText, appendText, and the openFile family return open
handles. An open handle is a resource, so acquire it inside a scope rather than trusting a later Dispose:
let copyThrough source destination =
Flow.acquireReleaseWith
(FileSystem.openRead source)
(fun stream _ ->
stream.Dispose()
Task.CompletedTask)
(fun stream -> readAndTransform stream destination)Directories and paths
createDirectory creates missing parents. deleteDirectory path recursive takes the recursion flag explicitly, so a
recursive delete is visible at the call site. Listing comes in eager (getFiles, getDirectories,
getFileSystemEntries) and lazy (enumerateFiles, enumerateDirectories, enumerateFileSystemEntries) forms, each
taking a search pattern and a SearchOption:
let fsharpSources root =
FileSystem.enumerateFiles root "*.fs" SearchOption.AllDirectories
fsharpSources: string -> Flow<'a,FileSystemError,string seq>root: stringAxial.FileSystem.FileSystemModuleenumerateFiles: string -> string -> SearchOption -> Flow<'env,FileSystemError,string seq>Enumerates files through an explicit file-system service.
System.IO.SearchOptionSpecifies whether to search the current directory, or the current directory and all subdirectories.
AllDirectories: SearchOptionIncludes the current directory and all its subdirectories in a search operation. This option includes reparse points such as mounted drives and symbolic links in the search.
Testing
IFileSystem is a wide interface, and implementing it in full to fake three calls is rarely worth it. Two approaches
work better:
Use FileSystem.live against a temporary directory. This is what Axial's own tests do. The workflow exercises
real I/O, and the test owns cleanup:
let root = Path.Combine(Path.GetTempPath(), "my-tests", Guid.NewGuid().ToString "N")
Directory.CreateDirectory root |> ignore
try
let exit = workflow root |> Flow.run { FileSystem = FileSystem.live }
test <@ exit = Exit.Success expected @>
finally
Directory.Delete(root, true)Fable
FileSystem.live is not compiled for Fable, and Layer.succeed FileSystem.live fails with PlatformNotSupportedException there.
See packages and platforms.
Related
- Service contracts — how a package declares the service it needs.
- Scopes and resources — deterministic cleanup for open handles.
- Error handling — expected failures against defects.

