-
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
24 changed files
with
1,006 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
#ignore preconditional tests with long arrays | ||
tests/Oxpecker.Tests/Preconditional.Tests.fs | ||
tests/Oxpecker.Tests/Streaming.Tests.fs | ||
examples/ContactApp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
|
||
<ItemGroup> | ||
<Compile Include="Program.fs"/> | ||
<Content Include="README.md" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
## Basic examples | ||
|
||
Here you can find a dump of different route patterns and handlers all in one file. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
## CRUD example | ||
|
||
Here you can find a JSON api for basic CRUD operations. [Functional DI](https://www.bartoszsypytkowski.com/dealing-with-complex-dependency-injection-in-f/) is used to glue components together. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net8.0</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Content Include="wwwroot\site.css"> | ||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
</Content> | ||
<Content Include="wwwroot\spinning-circles.svg"> | ||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
</Content> | ||
<Compile Include="Models.fs" /> | ||
<Compile Include="Tools.fs" /> | ||
<Compile Include="templates\shared\layout.fs" /> | ||
<Compile Include="templates\shared\contactFields.fs" /> | ||
<Compile Include="templates\index.fs" /> | ||
<Compile Include="templates\show.fs" /> | ||
<Compile Include="templates\edit.fs" /> | ||
<Compile Include="templates\new.fs" /> | ||
<Compile Include="ContactService.fs" /> | ||
<Compile Include="Handlers.fs" /> | ||
<Compile Include="Program.fs" /> | ||
<Content Include="README.md" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\src\Oxpecker.Htmx\Oxpecker.Htmx.fsproj" /> | ||
<ProjectReference Include="..\..\src\Oxpecker\Oxpecker.fsproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
module ContactApp.ContactService | ||
|
||
open System | ||
open System.Threading | ||
open ContactApp.Models | ||
|
||
let internal contactDb = ResizeArray([ | ||
{ Id = 1; First = "John"; Last = "Smith"; Email = "john@example.com"; Phone = "123-456-7890" } | ||
{ Id = 2; First = "Dana"; Last = "Crandith"; Email = "dcran@example.com"; Phone = "123-456-7890" } | ||
{ Id = 3; First = "Edith"; Last = "Neutvaar"; Email = "en@example.com"; Phone = "123-456-7890" } | ||
{ Id = 4; First = "John2"; Last = "Smith"; Email = "john2@example.com"; Phone = "123-456-7890" } | ||
{ Id = 5; First = "Dana2"; Last = "Crandith"; Email = "dcran2@example.com"; Phone = "123-456-7890" } | ||
{ Id = 6; First = "Edith2"; Last = "Neutvaar"; Email = "en2@example.com"; Phone = "123-456-7890" } | ||
{ Id = 7; First = "John3"; Last = "Smith"; Email = "john3@example.com"; Phone = "123-456-7890" } | ||
{ Id = 8; First = "Dana3"; Last = "Crandith"; Email = "dcran3@example.com"; Phone = "123-456-7890" } | ||
{ Id = 9; First = "Edith3"; Last = "Neutvaar"; Email = "en3@example.com"; Phone = "123-456-7890" } | ||
{ Id = 10; First = "John4"; Last = "Smith"; Email = "john4@example.com"; Phone = "123-456-7890" } | ||
{ Id = 11; First = "Dana4"; Last = "Crandith"; Email = "dcran4@example.com"; Phone = "123-456-7890" } | ||
{ Id = 12; First = "Edith4"; Last = "Neutvaar"; Email = "en4@example.com"; Phone = "123-456-7890" } | ||
]) | ||
|
||
let count() = | ||
Thread.Sleep 2000 | ||
contactDb.Count | ||
|
||
let searchContact (search: string) = | ||
contactDb | ||
|> Seq.filter(fun c -> c.First.Contains(search, StringComparison.OrdinalIgnoreCase) | ||
|| c.Last.Contains(search, StringComparison.OrdinalIgnoreCase)) | ||
|
||
let all page = | ||
contactDb |> Seq.skip ((page-1)*5) |> Seq.truncate 5 | ||
|
||
let add (contact: Contact) = | ||
let newId = contactDb |> Seq.maxBy(fun c -> c.Id) |> fun c -> c.Id + 1 | ||
let newContact = { contact with Id = newId } | ||
contactDb.Add(newContact) | ||
newContact | ||
|
||
let find id = | ||
contactDb.Find(fun c -> c.Id = id) | ||
|
||
let update (contact: Contact) = | ||
let index = contactDb.FindIndex(fun c -> c.Id = contact.Id) | ||
contactDb[index] <- contact | ||
|
||
let delete id = | ||
contactDb.RemoveAll(fun c -> c.Id = id) | ||
|
||
let validateEmail (contact: Contact) = | ||
let existingContact = contactDb |> Seq.tryFind(fun c -> c.Email = contact.Email) | ||
match existingContact with | ||
| Some c when c.Id <> contact.Id -> false | ||
| _ -> true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
module ContactApp.Handlers | ||
open System.Threading.Tasks | ||
open ContactApp.templates | ||
open ContactApp.Models | ||
open ContactApp.Tools | ||
open Microsoft.AspNetCore.Http | ||
open Oxpecker | ||
|
||
let mutable archiver = Archiver(ResizeArray()) | ||
|
||
let getContacts: EndpointHandler = | ||
fun ctx -> | ||
let page = ctx.TryGetQueryValue "page" |> Option.map int |> Option.defaultValue 1 | ||
match ctx.TryGetQueryValue "q" with | ||
| Some search -> | ||
let result = | ||
ContactService.searchContact search | ||
|> Seq.toArray | ||
match ctx.TryGetHeaderValue "HX-Trigger" with | ||
| Some "search" -> | ||
ctx.WriteHtmlView (index.rows page result) | ||
| _ -> | ||
ctx |> writeHtml (index.html search page result archiver) | ||
| None -> | ||
let result = | ||
ContactService.all page | ||
|> Seq.toArray | ||
ctx |> writeHtml (index.html "" page result archiver) | ||
|
||
let getContactsCount: EndpointHandler = | ||
fun ctx -> | ||
let count = ContactService.count() | ||
ctx.WriteText $"({count} total Contacts)" | ||
|
||
let getNewContact: EndpointHandler = | ||
let newContact = { | ||
id = 0 | ||
first = "" | ||
last = "" | ||
email = "" | ||
phone = "" | ||
errors = dict [] | ||
} | ||
writeHtml (new'.html newContact) | ||
|
||
let insertContact: EndpointHandler = | ||
fun ctx -> | ||
task { | ||
let! contact = ctx.BindForm<ContactDTO>() | ||
let validatedContact = contact.Validate() | ||
if validatedContact.errors.Count > 0 then | ||
return! ctx |> writeHtml (new'.html validatedContact) | ||
else | ||
validatedContact.ToDomain() | ||
|> ContactService.add | ||
|> ignore | ||
flash "Created new Contact!" ctx | ||
return ctx.Response.Redirect("/contacts") | ||
} | ||
|
||
let updateContact id: EndpointHandler = | ||
fun ctx -> | ||
task { | ||
let! contact = ctx.BindForm<ContactDTO>() | ||
let validatedContact = contact.Validate() | ||
if validatedContact.errors.Count > 0 then | ||
return! ctx |> writeHtml (edit.html { validatedContact with id = id }) | ||
else | ||
let domainContact = validatedContact.ToDomain() | ||
ContactService.update({domainContact with Id = id}) | ||
flash "Updated Contact!" ctx | ||
return ctx.Response.Redirect($"/contacts/{id}") | ||
} | ||
|
||
let viewContact id: EndpointHandler = | ||
let contact = ContactService.find id | ||
writeHtml <| show.html contact | ||
|
||
let getEditContact id: EndpointHandler = | ||
let contact = ContactService.find id |> ContactDTO.FromDomain | ||
writeHtml <| edit.html contact | ||
|
||
let deleteContact id: EndpointHandler = | ||
fun ctx -> | ||
task { | ||
ContactService.delete id |> ignore | ||
match ctx.TryGetHeaderValue "HX-Trigger" with | ||
| Some "delete-btn" -> | ||
flash "Deleted Contact!" ctx | ||
ctx.Response.Redirect("/contacts") | ||
ctx.SetStatusCode(303) | ||
| _ -> | ||
() | ||
} | ||
|
||
let deleteContacts (ctx: HttpContext) = | ||
match ctx.TryGetFormValues "selected_contact_ids" with | ||
| Some ids -> | ||
for id in ids do | ||
id |> int |> ContactService.delete |> ignore | ||
flash "Deleted Contacts!" ctx | ||
| None -> | ||
() | ||
let page = 1 | ||
let result = | ||
ContactService.all page | ||
|> Seq.toArray | ||
ctx |> writeHtml (index.html "" page result archiver) | ||
|
||
let validateEmail id: EndpointHandler = | ||
fun ctx -> | ||
match ctx.TryGetQueryValue("email") with | ||
| Some email -> | ||
let contact = | ||
if id = 0 then | ||
{ Id = 0; First = ""; Last = ""; Phone = ""; Email = email } | ||
else | ||
let contact = ContactService.find id | ||
{ contact with Email = email } | ||
if ContactService.validateEmail { contact with Email = email } then | ||
Task.CompletedTask | ||
else | ||
ctx.WriteText "Invalid email" | ||
| None -> | ||
Task.CompletedTask | ||
|
||
let startArchive: EndpointHandler = | ||
fun ctx -> | ||
archiver <- Archiver(ContactService.contactDb) | ||
archiver.Run() |> ignore | ||
ctx.WriteHtmlView (index.archiveUi archiver) | ||
|
||
let getArchiveStatus: EndpointHandler = | ||
fun ctx -> | ||
ctx.WriteHtmlView (index.archiveUi archiver) | ||
|
||
let deleteArchive: EndpointHandler = | ||
fun ctx -> | ||
archiver.Reset() | ||
ctx.WriteHtmlView (index.archiveUi archiver) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
module ContactApp.Models | ||
open System | ||
open System.Collections.Generic | ||
|
||
|
||
type Contact = { | ||
Id: int | ||
First: string | ||
Last: string | ||
Phone: string | ||
Email: string | ||
} | ||
|
||
[<CLIMutable>] | ||
type ContactDTO = { | ||
id: int | ||
first: string | ||
last: string | ||
phone: string | ||
email: string | ||
errors: IDictionary<string, string> | ||
} with | ||
member this.GetError key = | ||
match this.errors.TryGetValue key with | ||
| true, value -> value | ||
| _ -> "" | ||
member this.Validate() = | ||
let errors = Dictionary<string, string>() | ||
if String.IsNullOrEmpty(this.first) then errors.Add("first", "First name is required") | ||
if String.IsNullOrEmpty(this.last) then errors.Add("last", "Last name is required") | ||
if String.IsNullOrEmpty(this.phone) then errors.Add("phone", "Phone is required") | ||
if String.IsNullOrEmpty(this.email) then errors.Add("email", "Email is required") | ||
{ this with errors = errors } | ||
member this.ToDomain() = | ||
{ Id = this.id; First = this.first; Last = this.last; Phone = this.phone; Email = this.email } | ||
static member FromDomain(contact: Contact) = | ||
{ id = contact.Id; first = contact.First; last = contact.Last; phone = contact.Phone; email = contact.Email; errors = dict [] } |
Oops, something went wrong.