Skip to content

Instantly share code, notes, and snippets.

@absolutejam
Created November 3, 2020 16:03
Show Gist options
  • Save absolutejam/01940b84acb373928850ee9619778556 to your computer and use it in GitHub Desktop.
Save absolutejam/01940b84acb373928850ee9619778556 to your computer and use it in GitHub Desktop.
Saturn
module RealmWeaver.Core.Config
open System.IO
open Microsoft.Extensions.Configuration
open RealmWeaver.Core.Utils
let buildConfig environment (configBuilder: IConfigurationBuilder) =
let settingsPath =
match EnvVar.tryGet (sprintf "%s_%s" EnvVar.Root "CONFIG_FILE") with
| Some value -> value |> Path.GetFileNameWithoutExtension
| None -> "settings"
let envSettingsPath = sprintf "%s.%s" settingsPath environment
configBuilder
.SetBasePath(Directory.GetCurrentDirectory ())
.AddJsonFile(settingsPath + ".json", optional=true, reloadOnChange=true)
.AddJsonFile(envSettingsPath + ".json", optional=true, reloadOnChange=true)
.AddYamlFile(settingsPath + ".yaml", optional=false, reloadOnChange=true)
.AddYamlFile(envSettingsPath + ".yaml", optional=true, reloadOnChange=true)
.AddEnvironmentVariables(prefix=EnvVar.Root + "_")
.Build()
module RealmWeaver.Server.Configuration
open System
open System.IO
open Neo4j.Driver
open Serilog.Events
open RealmWeaver.Core
open Microsoft.Extensions.Configuration
open RealmWeaver.Core.Infrastructure
let logger = Logging.makeStartupLogger ()
/// F#-friendly config.
module ParsedConfig =
exception ConfigError of string
type Neo4jConfig =
{
Uri: string
Username: string option
Password: string option
}
type LoggingConfig =
{
SentryDsn: string option
SeqUri: string option
MinimumLogLevel: LogEventLevel
}
type EventStoreConfig =
{
CacheMb: int
InMemory: bool
Username: string option
Password: string option
ConnectionString: Uri
}
type WebServerConfig =
{
Port: uint16
PublicPath: string
}
type AppConfig =
{
Neo4j: Neo4jConfig
Logging: LoggingConfig
WebServer: WebServerConfig
EventStore: EventStoreConfig
}
let makeNeo4jDriver (config: Neo4jConfig) =
let auth =
match config with
| { Username = None; Password = None } ->
AuthTokens.None
| { Username = None; Password = Some _ } ->
failwith "Username specified - Password is required"
| { Username = Some _; Password = None } ->
failwith "Password specified - Username is specified"
| { Username = Some username; Password = Some password } ->
AuthTokens.Basic (username, password)
GraphDatabase.Driver (config.Uri, auth)
let makeStoreConnection (config: EventStoreConfig) =
let storeConfig =
if config.InMemory then
Store.Config.MemoryStore
else
match config with
| { Username = None }
| { Password = None } ->
failwith "Username or Password not provided"
| { Username = Some username; Password = Some password } ->
Store.Config.EventStore (
config.ConnectionString,
username,
password,
config.CacheMb
)
Store.connect storeConfig
/// Contains POCOs that are used by `IConfiguration` to read from configuration
/// sources. All values are C#-friendly primitives (strings, nulls, etc.) and
/// some defaults are provided.
module Raw =
open ParsedConfig
type IConfigSettings<'t> =
abstract ToConfig : unit -> 't
let toConfig (configSettings: IConfigSettings<_>) = configSettings.ToConfig ()
type Neo4jSettings () =
member val Uri: string = "bolt://localhost:7687" with get, set
member val Username: string = null with get, set
member val Password: string = null with get, set
interface IConfigSettings<Neo4jConfig> with
member this.ToConfig () =
{
Uri = this.Uri
Username = this.Username |> Option.ofObj
Password = this.Password |> Option.ofObj
}
let parseLogLevel logLevel =
match LogEventLevel.TryParse logLevel with
| true, value -> value
| false, _ ->
if not (logLevel |> isNull)
then logger.Error ("CONFIG: Could not parse log level {logLevel}. Defaulting to Warning.", logLevel)
LogEventLevel.Warning
type LoggingSettings () =
member val SeqUri: string = null with get, set
member val SentryDsn: string = null with get, set
member val MinimumLogLevel: string = null with get, set
interface IConfigSettings<LoggingConfig> with
member this.ToConfig () =
{
SeqUri = this.SeqUri |> Option.ofObj
SentryDsn = this.SentryDsn |> Option.ofObj
MinimumLogLevel = parseLogLevel this.MinimumLogLevel
}
type EventStoreSettings () =
member val InMemory: bool = false with get, set
member val CacheMb: int = 10 with get, set
member val Username: string = null with get, set
member val Password: string = null with get, set
member val ConnectionString: string = "tcp://localhost:1113" with get, set
interface IConfigSettings<EventStoreConfig> with
member this.ToConfig () =
{
CacheMb = this.CacheMb
InMemory = this.InMemory
Username = this.Username |> Option.ofObj
Password = this.Password |> Option.ofObj
ConnectionString = Uri this.ConnectionString
}
type WebServerSettings () =
member val Port: uint16 = 8085us with get, set
member val PublicPath: string = Path.GetFullPath "../Client/public" with get, set
interface IConfigSettings<WebServerConfig> with
member this.ToConfig () =
{
Port = this.Port
PublicPath = this.PublicPath
}
open ParsedConfig
open Raw
let getExn<'t> key (configRoot: IConfigurationRoot) : 't =
match configRoot.GetSection(key).Get<'t>() |> box with
| null -> failwith (sprintf "Could not retrieve type '%s' from IConfigurationRoot" (typeof<'t>.Name))
| x -> unbox x
let loadExn environment : AppConfig =
let configRoot =
ConfigurationBuilder ()
|> Config.buildConfig environment
{
Neo4j = configRoot |> getExn<Neo4jSettings> "Neo4j" |> toConfig
Logging = configRoot |> getExn<LoggingSettings> "Logging" |> toConfig
WebServer = configRoot |> getExn<WebServerSettings> "WebServer" |> toConfig
EventStore = configRoot |> getExn<EventStoreSettings> "EventStore" |> toConfig
}
module RealmWeaver.Server.Program
open System
open Saturn
open RealmWeaver.Core
open RealmWeaver.Server
open RealmWeaver.Core.Utils
open RealmWeaver.Server.Web.CompositionRoot
open RealmWeaver.Server.Configuration.ParsedConfig
open RealmWeaver.Server.Web.Extensions.SaturnApplicationBuilder
let logger = Logging.makeStartupLogger ()
let environment =
let defaultEnv = "Development"
match EnvVar.tryGet "DOTNET_ENVIRONMENT" with
| Some env -> env
| None ->
logger.Information ("No environment specified - Defaulting to {default}", defaultEnv)
defaultEnv
let buildApp (config: AppConfig) = application {
url (sprintf "http://0.0.0.0:%i/" config.WebServer.Port)
use_router completeApi
memory_cache
use_static config.WebServer.PublicPath
use_gzip
use_serilog (Logging.makeSerilogILogger ())
use_sentry (fun sentry ->
if config.Logging.SentryDsn |> Option.isSome then sentry.Dsn <- config.Logging.SentryDsn.Value
sentry.AttachStacktrace <- true
sentry.ShutdownTimeout <- TimeSpan.FromSeconds 3.
sentry.MaxBreadcrumbs <- 200)
service_config (configureServices config)
}
type ExitCode =
| Ok = 0
| Error = 1
| ConfigFailure = 2
[<EntryPoint>]
let main _args =
try
let appConfig = Configuration.loadExn environment
buildApp appConfig |> run
ExitCode.Ok
with
| ConfigError err ->
logger.Fatal ("Config error: {err}", err)
ExitCode.ConfigFailure
| e ->
logger.Fatal ("Fatal error: " + e.Message)
ExitCode.Error
|> int
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment