Remove migration and shared projects (#24)
This commit is contained in:
parent
909a0982e0
commit
45861e06f0
|
@ -17,8 +17,6 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Domain", "JobsJobsJobs\Doma
|
||||||
EndProject
|
EndProject
|
||||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Api", "JobsJobsJobs\Api\Api.fsproj", "{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}"
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Api", "JobsJobsJobs\Api\Api.fsproj", "{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "DataMigrate", "JobsJobsJobs\DataMigrate\DataMigrate.fsproj", "{C5774E4F-2930-4B64-8407-77BF7EB79F39}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -33,10 +31,6 @@ Global
|
||||||
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{C5774E4F-2930-4B64-8407-77BF7EB79F39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{C5774E4F-2930-4B64-8407-77BF7EB79F39}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{C5774E4F-2930-4B64-8407-77BF7EB79F39}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{C5774E4F-2930-4B64-8407-77BF7EB79F39}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
@ -47,6 +41,5 @@ Global
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{C81278DA-DA97-4E55-AB39-4B88565B615D} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
{C81278DA-DA97-4E55-AB39-4B88565B615D} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
||||||
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
{8F5A3D1E-562B-4F27-9787-6CB14B35E69E} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
||||||
{C5774E4F-2930-4B64-8407-77BF7EB79F39} = {FA833B24-B8F6-4CE6-A044-99257EAC02FF}
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
namespace JobsJobsJobs.DataMigrate
|
|
||||||
|
|
||||||
/// Converters used to translate between database and domain types
|
|
||||||
module Converters =
|
|
||||||
open JobsJobsJobs.Shared
|
|
||||||
open Microsoft.EntityFrameworkCore.Storage.ValueConversion
|
|
||||||
|
|
||||||
/// Citizen ID converter
|
|
||||||
let CitizenIdConverter = ValueConverter<CitizenId, string> ((fun v -> string v), fun v -> CitizenId.Parse v)
|
|
||||||
|
|
||||||
/// Continent ID converter
|
|
||||||
let ContinentIdConverter = ValueConverter<ContinentId, string> ((fun v -> string v), fun v -> ContinentId.Parse v)
|
|
||||||
|
|
||||||
/// Markdown converter
|
|
||||||
let MarkdownStringConverter = ValueConverter<MarkdownString, string> ((fun v -> v.Text), fun v -> MarkdownString v)
|
|
||||||
|
|
||||||
/// Markdown converter for possibly-null values
|
|
||||||
let OptionalMarkdownStringConverter : ValueConverter<MarkdownString, string> =
|
|
||||||
ValueConverter<MarkdownString, string>
|
|
||||||
((fun v -> match v with null -> null | _ -> v.Text), fun v -> match v with null -> null | _ -> MarkdownString v)
|
|
||||||
|
|
||||||
/// Skill ID converter
|
|
||||||
let SkillIdConverter = ValueConverter<SkillId, string> ((fun v -> string v), fun v -> SkillId.Parse v)
|
|
||||||
|
|
||||||
/// Success ID converter
|
|
||||||
let SuccessIdConverter = ValueConverter<SuccessId, string> ((fun v -> string v), fun v -> SuccessId.Parse v)
|
|
||||||
|
|
||||||
|
|
||||||
open JobsJobsJobs.Shared
|
|
||||||
open Microsoft.EntityFrameworkCore
|
|
||||||
open System.Collections.Generic
|
|
||||||
|
|
||||||
/// Data context for Jobs, Jobs, Jobs
|
|
||||||
type JobsDbContext (options : DbContextOptions<JobsDbContext>) =
|
|
||||||
inherit DbContext (options)
|
|
||||||
|
|
||||||
/// Citizens (users known to us)
|
|
||||||
member val Citizens : DbSet<Citizen> = Unchecked.defaultof<DbSet<Citizen>> with get, set
|
|
||||||
|
|
||||||
/// Continents (large land masses - 7 of them!)
|
|
||||||
member val Continents : DbSet<Continent> = Unchecked.defaultof<DbSet<Continent>> with get, set
|
|
||||||
|
|
||||||
/// Employment profiles
|
|
||||||
member val Profiles : DbSet<Profile> = Unchecked.defaultof<DbSet<Profile>> with get, set
|
|
||||||
|
|
||||||
/// Skills held by citizens of Gitmo Nation
|
|
||||||
member val Skills : DbSet<Skill> = Unchecked.defaultof<DbSet<Skill>> with get, set
|
|
||||||
|
|
||||||
/// Success stories from the site
|
|
||||||
member val Successes : DbSet<Success> = Unchecked.defaultof<DbSet<Success>> with get, set
|
|
||||||
|
|
||||||
override _.OnModelCreating (mb : ModelBuilder) =
|
|
||||||
base.OnModelCreating(mb)
|
|
||||||
|
|
||||||
mb.Entity<Citizen>(fun m ->
|
|
||||||
m.ToTable("citizen", "jjj").HasKey(fun e -> e.Id :> obj) |> ignore
|
|
||||||
m.Property(fun e -> e.Id).HasColumnName("id").IsRequired().HasMaxLength(12)
|
|
||||||
.HasConversion(Converters.CitizenIdConverter) |> ignore
|
|
||||||
m.Property(fun e -> e.NaUser).HasColumnName("na_user").IsRequired().HasMaxLength(50) |> ignore
|
|
||||||
m.Property(fun e -> e.DisplayName).HasColumnName("display_name").HasMaxLength(255) |> ignore
|
|
||||||
m.Property(fun e -> e.ProfileUrl).HasColumnName("profile_url").IsRequired().HasMaxLength(1_024) |> ignore
|
|
||||||
m.Property(fun e -> e.JoinedOn).HasColumnName("joined_on").IsRequired() |> ignore
|
|
||||||
m.Property(fun e -> e.LastSeenOn).HasColumnName("last_seen_on").IsRequired() |> ignore
|
|
||||||
m.Property(fun e -> e.RealName).HasColumnName("real_name").HasMaxLength(255) |> ignore
|
|
||||||
m.HasIndex(fun e -> e.NaUser :> obj).IsUnique() |> ignore
|
|
||||||
m.Ignore(fun e -> e.CitizenName :> obj) |> ignore)
|
|
||||||
|> ignore
|
|
||||||
|
|
||||||
mb.Entity<Continent>(fun m ->
|
|
||||||
m.ToTable("continent", "jjj").HasKey(fun e -> e.Id :> obj) |> ignore
|
|
||||||
m.Property(fun e -> e.Id).HasColumnName("id").IsRequired().HasMaxLength(12)
|
|
||||||
.HasConversion(Converters.ContinentIdConverter) |> ignore
|
|
||||||
m.Property(fun e -> e.Name).HasColumnName("name").IsRequired().HasMaxLength(255) |> ignore)
|
|
||||||
|> ignore
|
|
||||||
|
|
||||||
mb.Entity<Profile>(fun m ->
|
|
||||||
m.ToTable("profile", "jjj").HasKey(fun e -> e.Id :> obj) |> ignore
|
|
||||||
m.Property(fun e -> e.Id).HasColumnName("citizen_id").IsRequired().HasMaxLength(12)
|
|
||||||
.HasConversion(Converters.CitizenIdConverter) |> ignore
|
|
||||||
m.Property(fun e -> e.SeekingEmployment).HasColumnName("seeking_employment").IsRequired() |> ignore
|
|
||||||
m.Property(fun e -> e.IsPublic).HasColumnName("is_public").IsRequired() |> ignore
|
|
||||||
m.Property(fun e -> e.ContinentId).HasColumnName("continent_id").IsRequired().HasMaxLength(12)
|
|
||||||
.HasConversion(Converters.ContinentIdConverter) |> ignore
|
|
||||||
m.Property(fun e -> e.Region).HasColumnName("region").IsRequired().HasMaxLength(255) |> ignore
|
|
||||||
m.Property(fun e -> e.RemoteWork).HasColumnName("remote_work").IsRequired() |> ignore
|
|
||||||
m.Property(fun e -> e.FullTime).HasColumnName("full_time").IsRequired() |> ignore
|
|
||||||
m.Property(fun e -> e.Biography).HasColumnName("biography").IsRequired()
|
|
||||||
.HasConversion(Converters.MarkdownStringConverter) |> ignore
|
|
||||||
m.Property(fun e -> e.LastUpdatedOn).HasColumnName("last_updated_on").IsRequired() |> ignore
|
|
||||||
m.Property(fun e -> e.Experience).HasColumnName("experience")
|
|
||||||
.HasConversion(Converters.OptionalMarkdownStringConverter) |> ignore
|
|
||||||
m.HasOne(fun e -> e.Continent :> obj)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(fun e -> e.ContinentId :> obj) |> ignore
|
|
||||||
m.HasMany(fun e -> e.Skills :> IEnumerable<Skill>)
|
|
||||||
.WithOne()
|
|
||||||
.HasForeignKey(fun e -> e.CitizenId :> obj) |> ignore)
|
|
||||||
|> ignore
|
|
||||||
|
|
||||||
mb.Entity<Skill>(fun m ->
|
|
||||||
m.ToTable("skill", "jjj").HasKey(fun e -> e.Id :> obj) |> ignore
|
|
||||||
m.Property(fun e -> e.Id).HasColumnName("id").IsRequired().HasMaxLength(12)
|
|
||||||
.HasConversion(Converters.SkillIdConverter) |> ignore
|
|
||||||
m.Property(fun e -> e.CitizenId).HasColumnName("citizen_id").IsRequired().HasMaxLength(12)
|
|
||||||
.HasConversion(Converters.CitizenIdConverter) |> ignore
|
|
||||||
m.Property(fun e -> e.Description).HasColumnName("skill").IsRequired().HasMaxLength(100) |> ignore
|
|
||||||
m.Property(fun e -> e.Notes).HasColumnName("notes").HasMaxLength(100) |> ignore)
|
|
||||||
|> ignore
|
|
||||||
|
|
||||||
mb.Entity<Success>(fun m ->
|
|
||||||
m.ToTable("success", "jjj").HasKey(fun e -> e.Id :> obj) |> ignore
|
|
||||||
m.Property(fun e -> e.Id).HasColumnName("id").IsRequired().HasMaxLength(12)
|
|
||||||
.HasConversion(Converters.SuccessIdConverter) |> ignore
|
|
||||||
m.Property(fun e -> e.CitizenId).HasColumnName("citizen_id").IsRequired().HasMaxLength(12)
|
|
||||||
.HasConversion(Converters.CitizenIdConverter) |> ignore
|
|
||||||
m.Property(fun e -> e.RecordedOn).HasColumnName("recorded_on").IsRequired() |> ignore
|
|
||||||
m.Property(fun e -> e.FromHere).HasColumnName("from_here").IsRequired() |> ignore
|
|
||||||
// m.Property(fun e -> e.Source).HasColumnName("source").IsRequired().HasMaxLength(7);
|
|
||||||
m.Property(fun e -> e.Story).HasColumnName("story")
|
|
||||||
.HasConversion(Converters.OptionalMarkdownStringConverter) |> ignore)
|
|
||||||
|> ignore
|
|
|
@ -1,28 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
|
||||||
<WarnOn>3390;$(WarnOn)</WarnOn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="DataContext.fs" />
|
|
||||||
<Compile Include="Program.fs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Shared\JobsJobsJobs.Shared.csproj" />
|
|
||||||
<ProjectReference Include="..\Api\Api.fsproj" />
|
|
||||||
<ProjectReference Include="..\Domain\Domain.fsproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Npgsql" Version="5.0.7" />
|
|
||||||
<PackageReference Include="Npgsql" Version="5.0.7" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.7" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="5.0.7" />
|
|
||||||
<PackageReference Include="Npgsql.NodaTime" Version="5.0.7" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
|
@ -1,142 +0,0 @@
|
||||||
// Learn more about F# at http://docs.microsoft.com/dotnet/fsharp
|
|
||||||
|
|
||||||
open FSharp.Control.Tasks
|
|
||||||
open System
|
|
||||||
open System.Linq
|
|
||||||
open JobsJobsJobs.Api.Data
|
|
||||||
open JobsJobsJobs.Domain
|
|
||||||
open JobsJobsJobs.DataMigrate
|
|
||||||
open Microsoft.EntityFrameworkCore
|
|
||||||
open Microsoft.Extensions.Hosting
|
|
||||||
open Microsoft.Extensions.DependencyInjection
|
|
||||||
open Microsoft.Extensions.Logging
|
|
||||||
open RethinkDb.Driver.Net
|
|
||||||
|
|
||||||
/// Create the host (reads configuration and initializes both databases)
|
|
||||||
let createHostBuilder argv =
|
|
||||||
Host.CreateDefaultBuilder(argv)
|
|
||||||
.ConfigureServices(
|
|
||||||
Action<HostBuilderContext, IServiceCollection> (
|
|
||||||
fun hostCtx svcs ->
|
|
||||||
svcs.AddSingleton hostCtx.Configuration |> ignore
|
|
||||||
// PostgreSQL via EF Core
|
|
||||||
svcs.AddDbContext<JobsDbContext>(fun options ->
|
|
||||||
options.UseNpgsql(hostCtx.Configuration.["ConnectionStrings:JobsDb"],
|
|
||||||
fun o -> o.UseNodaTime() |> ignore)
|
|
||||||
|> ignore)
|
|
||||||
|> ignore
|
|
||||||
// RethinkDB
|
|
||||||
let cfg = hostCtx.Configuration.GetSection "Rethink"
|
|
||||||
let log = svcs.BuildServiceProvider().GetRequiredService<ILoggerFactory>().CreateLogger "Data"
|
|
||||||
let conn = Startup.createConnection cfg log
|
|
||||||
svcs.AddSingleton conn |> ignore
|
|
||||||
Startup.establishEnvironment cfg log conn |> awaitIgnore
|
|
||||||
))
|
|
||||||
.Build()
|
|
||||||
|
|
||||||
[<EntryPoint>]
|
|
||||||
let main argv =
|
|
||||||
let host = createHostBuilder argv
|
|
||||||
let r = RethinkDb.Driver.RethinkDB.R
|
|
||||||
|
|
||||||
printfn "0) Connecting to databases..."
|
|
||||||
|
|
||||||
use db = host.Services.GetRequiredService<JobsDbContext> ()
|
|
||||||
let conn = host.Services.GetRequiredService<IConnection> ()
|
|
||||||
|
|
||||||
task {
|
|
||||||
|
|
||||||
printfn "1) Migrating continents..."
|
|
||||||
|
|
||||||
let mutable continentXref = Map.empty<string, Types.ContinentId>
|
|
||||||
let! continents = db.Continents.AsNoTracking().ToListAsync ()
|
|
||||||
let reContinents =
|
|
||||||
continents
|
|
||||||
|> Seq.map (fun c ->
|
|
||||||
let reContinentId = ContinentId.create ()
|
|
||||||
continentXref <- continentXref.Add (string c.Id, reContinentId)
|
|
||||||
let it : Types.Continent = {
|
|
||||||
id = reContinentId
|
|
||||||
name = c.Name
|
|
||||||
}
|
|
||||||
it)
|
|
||||||
|> List.ofSeq
|
|
||||||
let! _ = r.Table(Table.Continent).Insert(reContinents).RunWriteAsync conn
|
|
||||||
|
|
||||||
printfn "2) Migrating citizens..."
|
|
||||||
|
|
||||||
let mutable citizenXref = Map.empty<string, Types.CitizenId>
|
|
||||||
let! citizens = db.Citizens.AsNoTracking().ToListAsync ()
|
|
||||||
let reCitizens =
|
|
||||||
citizens
|
|
||||||
|> Seq.map (fun c ->
|
|
||||||
let reCitizenId = CitizenId.create ()
|
|
||||||
citizenXref <- citizenXref.Add (string c.Id, reCitizenId)
|
|
||||||
let it : Types.Citizen = {
|
|
||||||
id = reCitizenId
|
|
||||||
naUser = c.NaUser
|
|
||||||
displayName = Option.ofObj c.DisplayName
|
|
||||||
realName = Option.ofObj c.RealName
|
|
||||||
profileUrl = c.ProfileUrl
|
|
||||||
joinedOn = c.JoinedOn
|
|
||||||
lastSeenOn = c.LastSeenOn
|
|
||||||
}
|
|
||||||
it)
|
|
||||||
let! _ = r.Table(Table.Citizen).Insert(reCitizens).RunWriteAsync conn
|
|
||||||
|
|
||||||
printfn "3) Migrating profiles and skills..."
|
|
||||||
|
|
||||||
let! profiles = db.Profiles.AsNoTracking().ToListAsync ()
|
|
||||||
let reProfiles =
|
|
||||||
profiles
|
|
||||||
|> Seq.map (fun p ->
|
|
||||||
let skills = db.Skills.AsNoTracking().Where(fun s -> s.CitizenId = p.Id).ToList ()
|
|
||||||
let reSkills =
|
|
||||||
skills
|
|
||||||
|> Seq.map (fun skill ->
|
|
||||||
let it : Types.Skill = {
|
|
||||||
id = SkillId.create()
|
|
||||||
description = skill.Description
|
|
||||||
notes = Option.ofObj skill.Notes
|
|
||||||
}
|
|
||||||
it)
|
|
||||||
|> List.ofSeq
|
|
||||||
let it : Types.Profile = {
|
|
||||||
id = citizenXref.[string p.Id]
|
|
||||||
seekingEmployment = p.SeekingEmployment
|
|
||||||
isPublic = p.IsPublic
|
|
||||||
continentId = continentXref.[string p.ContinentId]
|
|
||||||
region = p.Region
|
|
||||||
remoteWork = p.RemoteWork
|
|
||||||
fullTime = p.FullTime
|
|
||||||
biography = Types.Text p.Biography.Text
|
|
||||||
lastUpdatedOn = p.LastUpdatedOn
|
|
||||||
experience = match p.Experience with null -> None | x -> (Types.Text >> Some) x.Text
|
|
||||||
skills = reSkills
|
|
||||||
}
|
|
||||||
it)
|
|
||||||
let! _ = r.Table(Table.Profile).Insert(reProfiles).RunWriteAsync conn
|
|
||||||
|
|
||||||
printfn "4) Migrating success stories..."
|
|
||||||
|
|
||||||
let! successes = db.Successes.AsNoTracking().ToListAsync ()
|
|
||||||
let reSuccesses =
|
|
||||||
successes
|
|
||||||
|> Seq.map (fun s ->
|
|
||||||
let it : Types.Success = {
|
|
||||||
id = SuccessId.create ()
|
|
||||||
citizenId = citizenXref.[string s.CitizenId]
|
|
||||||
recordedOn = s.RecordedOn
|
|
||||||
fromHere = s.FromHere
|
|
||||||
source = "profile"
|
|
||||||
story = match s.Story with null -> None | x -> (Types.Text >> Some) x.Text
|
|
||||||
}
|
|
||||||
it)
|
|
||||||
let! _ = r.Table(Table.Success).Insert(reSuccesses).RunWriteAsync conn
|
|
||||||
()
|
|
||||||
}
|
|
||||||
|> awaitIgnore
|
|
||||||
|
|
||||||
printfn "Migration complete"
|
|
||||||
|
|
||||||
0
|
|
|
@ -1,7 +0,0 @@
|
||||||
namespace JobsJobsJobs.Shared.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A transport mechanism to send counts across the wire via JSON
|
|
||||||
/// </summary>
|
|
||||||
public record Count(int Value);
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
namespace JobsJobsJobs.Shared.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A successful log on; returns JWT, citizen ID, and display name
|
|
||||||
/// </summary>
|
|
||||||
public record LogOnSuccess(string Jwt, string Id, string Name)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID return value as a citizen ID
|
|
||||||
/// </summary>
|
|
||||||
public CitizenId CitizenId => CitizenId.Parse(Id);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The data required to update a profile
|
|
||||||
/// </summary>
|
|
||||||
public class ProfileForm
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the citizen to whom this profile belongs is actively seeking employment
|
|
||||||
/// </summary>
|
|
||||||
public bool IsSeekingEmployment { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this profile should appear in the public search
|
|
||||||
/// </summary>
|
|
||||||
public bool IsPublic { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user's real name
|
|
||||||
/// </summary>
|
|
||||||
[StringLength(255)]
|
|
||||||
public string RealName { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the continent on which the citizen is located
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
[StringLength(12, MinimumLength = 1)]
|
|
||||||
[Display(Name = "Continent")]
|
|
||||||
public string ContinentId { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The area within that continent where the citizen is located
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
[StringLength(255)]
|
|
||||||
public string Region { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If the citizen is available for remote work
|
|
||||||
/// </summary>
|
|
||||||
public bool RemoteWork { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If the citizen is seeking full-time employment
|
|
||||||
/// </summary>
|
|
||||||
public bool FullTime { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user's professional biography
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public string Biography { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user's past experience
|
|
||||||
/// </summary>
|
|
||||||
public string Experience { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The skills for the user
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<SkillForm> Skills { get; set; } = new List<SkillForm>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create an instance of this form from the given profile
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="profile">The profile off which this form will be based</param>
|
|
||||||
/// <returns>The profile form, popluated with values from the given profile</returns>
|
|
||||||
public static ProfileForm FromProfile(Profile profile) =>
|
|
||||||
new ProfileForm
|
|
||||||
{
|
|
||||||
IsSeekingEmployment = profile.SeekingEmployment,
|
|
||||||
IsPublic = profile.IsPublic,
|
|
||||||
ContinentId = profile.ContinentId.ToString(),
|
|
||||||
Region = profile.Region,
|
|
||||||
RemoteWork = profile.RemoteWork,
|
|
||||||
FullTime = profile.FullTime,
|
|
||||||
Biography = profile.Biography.Text,
|
|
||||||
Experience = profile.Experience?.Text ?? "",
|
|
||||||
Skills = profile.Skills.Select(s => new SkillForm
|
|
||||||
{
|
|
||||||
Id = s.Id.ToString(),
|
|
||||||
Description = s.Description,
|
|
||||||
Notes = s.Notes
|
|
||||||
}).ToList()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
namespace JobsJobsJobs.Shared.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The various ways profiles can be searched
|
|
||||||
/// </summary>
|
|
||||||
public class ProfileSearch
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieve citizens from this continent
|
|
||||||
/// </summary>
|
|
||||||
public string? ContinentId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Text for a search within a citizen's skills
|
|
||||||
/// </summary>
|
|
||||||
public string? Skill { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Text for a search with a citizen's professional biography and experience fields
|
|
||||||
/// </summary>
|
|
||||||
public string? BioExperience { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to retrieve citizens who do or do not want remote work
|
|
||||||
/// </summary>
|
|
||||||
public string RemoteWork { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is the search empty?
|
|
||||||
/// </summary>
|
|
||||||
public bool IsEmptySearch =>
|
|
||||||
string.IsNullOrEmpty(ContinentId)
|
|
||||||
&& string.IsNullOrEmpty(Skill)
|
|
||||||
&& string.IsNullOrEmpty(BioExperience)
|
|
||||||
&& string.IsNullOrEmpty(RemoteWork);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
using NodaTime;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A user matching the profile search
|
|
||||||
/// </summary>
|
|
||||||
public record ProfileSearchResult(
|
|
||||||
CitizenId CitizenId,
|
|
||||||
string DisplayName,
|
|
||||||
bool SeekingEmployment,
|
|
||||||
bool RemoteWork,
|
|
||||||
bool FullTime,
|
|
||||||
Instant LastUpdated);
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
namespace JobsJobsJobs.Shared.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The parameters for a public job search
|
|
||||||
/// </summary>
|
|
||||||
public class PublicSearch
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieve citizens from this continent
|
|
||||||
/// </summary>
|
|
||||||
public string? ContinentId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieve citizens from this region
|
|
||||||
/// </summary>
|
|
||||||
public string? Region { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Text for a search within a citizen's skills
|
|
||||||
/// </summary>
|
|
||||||
public string? Skill { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to retrieve citizens who do or do not want remote work
|
|
||||||
/// </summary>
|
|
||||||
public string RemoteWork { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is the search empty?
|
|
||||||
/// </summary>
|
|
||||||
public bool IsEmptySearch =>
|
|
||||||
string.IsNullOrEmpty(ContinentId)
|
|
||||||
&& string.IsNullOrEmpty(Region)
|
|
||||||
&& string.IsNullOrEmpty(Skill)
|
|
||||||
&& string.IsNullOrEmpty(RemoteWork);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A public profile search result
|
|
||||||
/// </summary>
|
|
||||||
public record PublicSearchResult(
|
|
||||||
string Continent,
|
|
||||||
string Region,
|
|
||||||
bool RemoteWork,
|
|
||||||
IEnumerable<string> Skills);
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The fields required for a skill
|
|
||||||
/// </summary>
|
|
||||||
public class SkillForm
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of this skill
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public string Id { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The description of the skill
|
|
||||||
/// </summary>
|
|
||||||
[StringLength(100)]
|
|
||||||
public string Description { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Notes regarding the skill
|
|
||||||
/// </summary>
|
|
||||||
[StringLength(100)]
|
|
||||||
public string? Notes { get; set; } = null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
using NodaTime;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// An entry in the list of success stories
|
|
||||||
/// </summary>
|
|
||||||
public record StoryEntry(
|
|
||||||
SuccessId Id,
|
|
||||||
CitizenId CitizenId,
|
|
||||||
string CitizenName,
|
|
||||||
Instant RecordedOn,
|
|
||||||
bool FromHere,
|
|
||||||
bool HasStory);
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
namespace JobsJobsJobs.Shared.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The data required to provide a success story
|
|
||||||
/// </summary>
|
|
||||||
public class StoryForm
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of this story
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = "new";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the employment was obtained from Jobs, Jobs, Jobs
|
|
||||||
/// </summary>
|
|
||||||
public bool FromHere { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The success story
|
|
||||||
/// </summary>
|
|
||||||
public string Story { get; set; } = "";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
using NodaTime;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A user of Jobs, Jobs, Jobs
|
|
||||||
/// </summary>
|
|
||||||
public record Citizen(
|
|
||||||
CitizenId Id,
|
|
||||||
string NaUser,
|
|
||||||
string? DisplayName,
|
|
||||||
string? RealName,
|
|
||||||
string ProfileUrl,
|
|
||||||
Instant JoinedOn,
|
|
||||||
Instant LastSeenOn)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The user's name by which they should be known
|
|
||||||
/// </summary>
|
|
||||||
public string CitizenName => RealName ?? DisplayName ?? NaUser;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
namespace JobsJobsJobs.Shared
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A continent
|
|
||||||
/// </summary>
|
|
||||||
public record Continent(ContinentId Id, string Name);
|
|
||||||
}
|
|
|
@ -1,148 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A short ID
|
|
||||||
/// </summary>
|
|
||||||
public record ShortId(string Id)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Validate the format of the short ID
|
|
||||||
/// </summary>
|
|
||||||
private static readonly Regex ValidShortId =
|
|
||||||
new Regex("^[a-z0-9_-]{12}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new short ID
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A new short ID</returns>
|
|
||||||
public static async Task<ShortId> Create() => new ShortId(await Nanoid.Nanoid.GenerateAsync(size: 12));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Try to parse a string of text into a short ID
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="text">The text of the prospective short ID</param>
|
|
||||||
/// <returns>The short ID</returns>
|
|
||||||
/// <exception cref="FormatException">If the format is not valid</exception>
|
|
||||||
public static ShortId Parse(string text)
|
|
||||||
{
|
|
||||||
if (text.Length == 12 && ValidShortId.IsMatch(text)) return new ShortId(text);
|
|
||||||
throw new FormatException($"The string {text} is not a valid short ID");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of a user (a citizen of Gitmo Nation)
|
|
||||||
/// </summary>
|
|
||||||
public record CitizenId(ShortId Id)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new citizen ID
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A new citizen ID</returns>
|
|
||||||
public static async Task<CitizenId> Create() => new CitizenId(await ShortId.Create());
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempt to create a citizen ID from a string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The prospective ID</param>
|
|
||||||
/// <returns>The citizen ID</returns>
|
|
||||||
/// <exception cref="System.FormatException">If the string is not a valid citizen ID</exception>
|
|
||||||
public static CitizenId Parse(string id) => new(ShortId.Parse(id));
|
|
||||||
|
|
||||||
public override string ToString() => Id.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of a continent
|
|
||||||
/// </summary>
|
|
||||||
public record ContinentId(ShortId Id)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new continent ID
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A new continent ID</returns>
|
|
||||||
public static async Task<ContinentId> Create() => new ContinentId(await ShortId.Create());
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempt to create a continent ID from a string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The prospective ID</param>
|
|
||||||
/// <returns>The continent ID</returns>
|
|
||||||
/// <exception cref="System.FormatException">If the string is not a valid continent ID</exception>
|
|
||||||
public static ContinentId Parse(string id) => new(ShortId.Parse(id));
|
|
||||||
|
|
||||||
public override string ToString() => Id.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of a job listing
|
|
||||||
/// </summary>
|
|
||||||
public record ListingId(ShortId Id)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new job listing ID
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A new job listing ID</returns>
|
|
||||||
public static async Task<ListingId> Create() => new ListingId(await ShortId.Create());
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempt to create a job listing ID from a string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The prospective ID</param>
|
|
||||||
/// <returns>The job listing ID</returns>
|
|
||||||
/// <exception cref="System.FormatException">If the string is not a valid job listing ID</exception>
|
|
||||||
public static ListingId Parse(string id) => new(ShortId.Parse(id));
|
|
||||||
|
|
||||||
public override string ToString() => Id.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of a skill
|
|
||||||
/// </summary>
|
|
||||||
public record SkillId(ShortId Id)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new skill ID
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A new skill ID</returns>
|
|
||||||
public static async Task<SkillId> Create() => new SkillId(await ShortId.Create());
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempt to create a skill ID from a string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The prospective ID</param>
|
|
||||||
/// <returns>The skill ID</returns>
|
|
||||||
/// <exception cref="System.FormatException">If the string is not a valid skill ID</exception>
|
|
||||||
public static SkillId Parse(string id) => new(ShortId.Parse(id));
|
|
||||||
|
|
||||||
public override string ToString() => Id.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of a success report
|
|
||||||
/// </summary>
|
|
||||||
public record SuccessId(ShortId Id)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new success report ID
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A new success report ID</returns>
|
|
||||||
public static async Task<SuccessId> Create() => new SuccessId(await ShortId.Create());
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempt to create a success report ID from a string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The prospective ID</param>
|
|
||||||
/// <returns>The success report ID</returns>
|
|
||||||
/// <exception cref="System.FormatException">If the string is not a valid success report ID</exception>
|
|
||||||
public static SuccessId Parse(string id) => new(ShortId.Parse(id));
|
|
||||||
|
|
||||||
public override string ToString() => Id.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
using NodaTime;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A job listing
|
|
||||||
/// </summary>
|
|
||||||
public record Listing(
|
|
||||||
ListingId Id,
|
|
||||||
CitizenId CitizenId,
|
|
||||||
Instant CreatedOn,
|
|
||||||
string Title,
|
|
||||||
ContinentId ContinentId,
|
|
||||||
string Region,
|
|
||||||
bool RemoteWork,
|
|
||||||
bool IsExpired,
|
|
||||||
Instant UpdatedOn,
|
|
||||||
MarkdownString Text,
|
|
||||||
LocalDate? NeededBy,
|
|
||||||
bool? WasFilledHere)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Navigation property for the citizen who created the job listing
|
|
||||||
/// </summary>
|
|
||||||
public Citizen? Citizen { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Navigation property for the continent
|
|
||||||
/// </summary>
|
|
||||||
public Continent? Continent { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
using Markdig;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A string of Markdown text
|
|
||||||
/// </summary>
|
|
||||||
public record MarkdownString(string Text)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The Markdown conversion pipeline (enables all advanced features)
|
|
||||||
/// </summary>
|
|
||||||
private readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert this Markdown string to HTML
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>This Markdown string as HTML</returns>
|
|
||||||
public string ToHtml() => Markdown.ToHtml(Text, Pipeline);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
using NodaTime;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A job seeker profile
|
|
||||||
/// </summary>
|
|
||||||
public record Profile(
|
|
||||||
CitizenId Id,
|
|
||||||
bool SeekingEmployment,
|
|
||||||
bool IsPublic,
|
|
||||||
ContinentId ContinentId,
|
|
||||||
string Region,
|
|
||||||
bool RemoteWork,
|
|
||||||
bool FullTime,
|
|
||||||
MarkdownString Biography,
|
|
||||||
Instant LastUpdatedOn,
|
|
||||||
MarkdownString? Experience)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Navigation property for continent
|
|
||||||
/// </summary>
|
|
||||||
public Continent? Continent { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convenience property for skills associated with a profile
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<Skill> Skills { get; set; } = new List<Skill>();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
namespace JobsJobsJobs.Shared
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A skill the job seeker possesses
|
|
||||||
/// </summary>
|
|
||||||
public record Skill(SkillId Id, CitizenId CitizenId, string Description, string? Notes);
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
using NodaTime;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A record of success finding employment
|
|
||||||
/// </summary>
|
|
||||||
public record Success(
|
|
||||||
SuccessId Id,
|
|
||||||
CitizenId CitizenId,
|
|
||||||
Instant RecordedOn,
|
|
||||||
bool FromHere,
|
|
||||||
// string Source,
|
|
||||||
MarkdownString? Story);
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Markdig" Version="0.22.1" />
|
|
||||||
<PackageReference Include="Nanoid" Version="2.1.0" />
|
|
||||||
<PackageReference Include="NodaTime" Version="3.0.3" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<SupportedPlatform Include="browser" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
|
@ -1,85 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A result with two different possibilities
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TOk">The type of the Ok result</typeparam>
|
|
||||||
public struct Result<TOk>
|
|
||||||
{
|
|
||||||
private readonly TOk? _okValue;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is this an Ok result?
|
|
||||||
/// </summary>
|
|
||||||
public bool IsOk { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is this an Error result?
|
|
||||||
/// </summary>
|
|
||||||
public bool IsError
|
|
||||||
{
|
|
||||||
get => !IsOk;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The Ok value
|
|
||||||
/// </summary>
|
|
||||||
public TOk Ok
|
|
||||||
{
|
|
||||||
get => _okValue!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The error value
|
|
||||||
/// </summary>
|
|
||||||
public string Error { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor (inaccessible - use static creation methods)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="isOk">Whether this is an Ok result</param>
|
|
||||||
/// <param name="okValue">The value of the Ok result</param>
|
|
||||||
/// <param name="error">The error message of the Error result</param>
|
|
||||||
private Result(bool isOk, TOk? okValue = default, string error = "")
|
|
||||||
{
|
|
||||||
IsOk = isOk;
|
|
||||||
_okValue = okValue;
|
|
||||||
Error = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create an Ok result
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="okValue">The value of the Ok result</param>
|
|
||||||
/// <returns>The Ok result</returns>
|
|
||||||
public static Result<TOk> AsOk(TOk okValue) => new Result<TOk>(true, okValue);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create an Error result
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="error">The error message</param>
|
|
||||||
/// <returns>The Error result</returns>
|
|
||||||
public static Result<TOk> AsError(string error) => new Result<TOk>(false) { Error = error };
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transform a result if it is OK, passing the error along if it is an error
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="f">The transforming function</param>
|
|
||||||
/// <param name="result">The existing result</param>
|
|
||||||
/// <returns>The resultant result</returns>
|
|
||||||
public static Result<TOk> Bind(Func<TOk, Result<TOk>> f, Result<TOk> result) =>
|
|
||||||
result.IsOk ? f(result.Ok) : result;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transform a result to a different type if it is OK, passing the error along if it is an error
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TOther">The type to which the result is transformed</typeparam>
|
|
||||||
/// <param name="f">The transforming function</param>
|
|
||||||
/// <param name="result">The existing result</param>
|
|
||||||
/// <returns>The resultant result</returns>
|
|
||||||
public static Result<TOther> Map<TOther>(Func<TOk, Result<TOther>> f, Result<TOk> result) =>
|
|
||||||
result.IsOk ? f(result.Ok) : Result<TOther>.AsError(result.Error);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user