Skip to content

Commit

Permalink
Merge branch develop into 'main'
Browse files Browse the repository at this point in the history
# Conflicts:
#	tests/Oxpecker.Tests/Oxpecker.Tests.fsproj
#	tests/Oxpecker.ViewEngine.Tests/Oxpecker.ViewEngine.Tests.fsproj
  • Loading branch information
Lanayx committed Nov 27, 2024
2 parents 43c1180 + 61d0910 commit 1cdd77b
Show file tree
Hide file tree
Showing 22 changed files with 354 additions and 85 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

![Oxpecker](https://github.com/Lanayx/Oxpecker/raw/develop/images/oxpecker.png)

**Oxpecker** is a library started as functional wrapper around **ASP.NET Core Endpoint routing**, but now providing fullstack capabilities with **Htmx** and **Solid.js** integrations. Repository is a monorepo with all related projects included and documentation located in *README.md* files per project. As of November 2024, Oxpecker is the fastest .NET web framework in several categories [in the TechEmpower benchmark](https://www.techempower.com/benchmarks/#section=test&runid=6ef367d2-de5c-464a-b3fa-2c3cf4ba1f8f&hw=ph&test=db&p=zik0zi-zik0zj-zik0zj-zik0zj-zik0zj-1kv)
**Oxpecker** is a library started as functional wrapper around **ASP.NET Core Endpoint routing**, but now providing fullstack capabilities with **Htmx** and **Solid.js** integrations. Repository is a monorepo with all related projects included and documentation located in *README.md* files per project. As of November 2024, Oxpecker is the fastest .NET 8 web framework in several categories [in the TechEmpower benchmark](https://www.techempower.com/benchmarks/#section=test&runid=6ef367d2-de5c-464a-b3fa-2c3cf4ba1f8f&hw=ph&test=db&p=zik0zi-zik0zj-zik0zj-zik0zj-zik0zj-1kv)

The server part of the Oxpecker library is a revised version of [Giraffe](https://github.com/giraffe-fsharp/Giraffe), it mostly sticks to Giraffe's successful API (hence the name). Improvements involve changing some core types, performance of template handlers, simplifying handlers and dropping a lot of outdated functionality.

Expand Down
2 changes: 1 addition & 1 deletion examples/Basic/Basic.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Oxpecker" Version="1.0.0" />
<PackageReference Include="Oxpecker" Version="1.1.1" />
<PackageReference Include="Oxpecker.OpenApi" Version="1.0.0" />
<PackageReference Include="Oxpecker.Htmx" Version="1.0.0" />
<PackageReference Include="FSharp.Control.TaskSeq" Version="0.4.0" />
Expand Down
2 changes: 1 addition & 1 deletion examples/CRUD/Backend/Backend.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Oxpecker" Version="1.0.0" />
<PackageReference Include="Oxpecker" Version="1.1.1" />
<PackageReference Include="FSharp.UMX" Version="1.1.0" />
</ItemGroup>

Expand Down
2 changes: 0 additions & 2 deletions examples/CRUD/Backend/Handlers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ type OperationEnv(env: Env) =
interface IGetProducts with
member this.GetProducts() = ProductRepository.getProducts env



let getOrders env (ctx: HttpContext) =
task {
let operationEnv = OperationEnv(env)
Expand Down
3 changes: 2 additions & 1 deletion examples/ContactApp/ContactApp.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<Compile Include="Models.fs" />
<Compile Include="Tools.fs" />
<Compile Include="templates\shared\layout.fs" />
<Compile Include="templates\shared\errors.fs" />
<Compile Include="templates\shared\contactFields.fs" />
<Compile Include="templates\index.fs" />
<Compile Include="templates\show.fs" />
Expand All @@ -29,7 +30,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Oxpecker" Version="1.0.0" />
<PackageReference Include="Oxpecker" Version="1.1.1" />
<PackageReference Include="Oxpecker.ViewEngine" Version="1.0.0" />
<PackageReference Include="Oxpecker.Htmx" Version="1.0.0" />
</ItemGroup>
Expand Down
46 changes: 25 additions & 21 deletions examples/ContactApp/Handlers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -34,52 +34,56 @@ let getContactsCount: EndpointHandler =
ctx.WriteText $"({count} total Contacts)"

let getNewContact: EndpointHandler =
let newContact = {
id = 0
first = ""
last = ""
email = ""
phone = ""
errors = dict []
}
writeHtml (new'.html newContact)
writeHtml (new'.html ModelState.Empty)

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
match! ctx.BindAndValidateForm<ContactDTO>() with
| ModelValidationResult.Valid validatedContact ->
validatedContact.ToDomain()
|> ContactService.add
|> ignore
flash "Created new Contact!" ctx
return ctx.Response.Redirect("/contacts")
| ModelValidationResult.Invalid invalidModel ->
return!
invalidModel
|> ModelState.Invalid
|> new'.html
|> writeHtml
<| ctx
}

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
match! ctx.BindAndValidateForm<ContactDTO>() with
| ModelValidationResult.Valid validatedContact ->
let domainContact = validatedContact.ToDomain()
ContactService.update({domainContact with Id = id})
flash "Updated Contact!" ctx
return ctx.Response.Redirect($"/contacts/{id}")
| ModelValidationResult.Invalid (contactDto, errors) ->
return!
({ contactDto with id = id }, errors)
|> ModelState.Invalid
|> edit.html
|> writeHtml
<| ctx
}

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
id
|> ContactService.find
|> ContactDTO.FromDomain
|> ModelState.Valid
|> edit.html
|> writeHtml

let deleteContact id: EndpointHandler =
fun ctx ->
Expand Down
22 changes: 7 additions & 15 deletions examples/ContactApp/Models.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
module ContactApp.Models
open System
open System.Collections.Generic
open System.ComponentModel.DataAnnotations


type Contact = {
Expand All @@ -14,24 +13,17 @@ type Contact = {
[<CLIMutable>]
type ContactDTO = {
id: int
[<Required>]
first: string
[<Required>]
last: string
[<Required>]
phone: string
[<Required>]
[<EmailAddress>]
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 [] }
{ id = contact.Id; first = contact.First; last = contact.Last; phone = contact.Phone; email = contact.Email }
2 changes: 2 additions & 0 deletions examples/ContactApp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

Here you can find an F# version of the contact app presented in [Hypermedia Systems](https://hypermedia.systems/) book.

It is also an example of `Oxpecker.ModelValidation` usage.

![image](https://github.com/Lanayx/Oxpecker/assets/3329606/888dc44f-3fa5-43e1-9da7-5df1d255b584)
8 changes: 5 additions & 3 deletions examples/ContactApp/templates/edit.fs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
module ContactApp.templates.edit

open Oxpecker.ModelValidation
open Oxpecker.ViewEngine
open Oxpecker.Htmx
open ContactApp.Models
open ContactApp.templates.shared

let html (contact: ContactDTO) =
let html (contact: ModelState<ContactDTO>) =
let contactId = contact.Value(_.id >> string)
Fragment() {
form(action= $"/contacts/{contact.id}/edit", method="post") {
form(action= $"/contacts/{contactId}/edit", method="post") {
fieldset() {
legend() { "Contact Values" }
contactFields.html contact
Expand All @@ -16,7 +18,7 @@ let html (contact: ContactDTO) =
}

button(id="delete-btn",
hxDelete= $"/contacts/{contact.id}",
hxDelete= $"/contacts/{contactId}",
hxPushUrl="true",
hxConfirm="Are you sure you want to delete this contact?",
hxTarget="body") { "Delete Contact" }
Expand Down
3 changes: 2 additions & 1 deletion examples/ContactApp/templates/new.fs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
module ContactApp.templates.new'
open ContactApp.Models
open Oxpecker.ModelValidation
open Oxpecker.ViewEngine
open ContactApp.templates.shared

let html (contact: ContactDTO) =
let html (contact: ModelState<ContactDTO>) =
Fragment() {
form(action="/contacts/new", method="post") {
fieldset() {
Expand Down
24 changes: 14 additions & 10 deletions examples/ContactApp/templates/shared/contactFields.fs
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
namespace ContactApp.templates.shared

open ContactApp.templates.shared.errors
open Oxpecker.Htmx
open Oxpecker.ModelValidation

module contactFields =

open ContactApp.Models
open Oxpecker.ViewEngine

let html (contact: ContactDTO) =
let html (contact: ModelState<ContactDTO>) =
let x = Unchecked.defaultof<ContactDTO>
let showErrors = showErrors contact
div() {
p() {
label(for'="email") { "Email" }
input(name="email", id="email", type'="email", placeholder="Email", value=contact.email,
input(name=nameof x.email, id="email", type'="email", placeholder="Email", value=contact.Value(_.email),
hxTrigger="change, keyup delay:200ms changed",
hxGet= $"/contacts/{contact.id}/email", hxTarget="next .error")
span(class'="error") { contact.GetError("email") }
hxGet= $"/contacts/{contact.Value(_.id >> string)}/email", hxTarget="next .error")
showErrors <| nameof x.email
}
p() {
label(for'="first") { "First Name" }
input(name="first", id="firs", type'="text", placeholder="First Name", value=contact.first)
span(class'="error") { contact.GetError("first") }
input(name=nameof x.first, id="first", type'="text", placeholder="First Name", value=contact.Value(_.first))
showErrors <| nameof x.first
}
p() {
label(for'="last") { "Last Name" }
input(name="last", id="last", type'="text", placeholder="Last Name", value=contact.last)
span(class'="error") { contact.GetError("last") }
input(name=nameof x.last, id="last", type'="text", placeholder="Last Name", value=contact.Value(_.last))
showErrors <| nameof x.last
}
p() {
label(for'="phone") { "Phone" }
input(name="phone", id="phone", type'="text", placeholder="Phone", value=contact.phone)
span(class'="error") { contact.GetError("phone") }
input(name=nameof x.phone, id="phone", type'="text", placeholder="Phone", value=contact.Value(_.phone))
showErrors <| nameof x.phone
}
}
13 changes: 13 additions & 0 deletions examples/ContactApp/templates/shared/errors.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module ContactApp.templates.shared.errors

open Oxpecker.ViewEngine
open Oxpecker.ModelValidation

let showErrors (modelState: ModelState<_>) (fieldName: string) =
match modelState with
| ModelState.Invalid (_, modelErrors) ->
span(class'="error"){
System.String.Join(", ", modelErrors.ErrorMessagesFor(fieldName))
} :> HtmlElement
| _ ->
Fragment()
2 changes: 1 addition & 1 deletion examples/Empty/Empty.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Oxpecker" Version="1.0.0" />
<PackageReference Include="Oxpecker" Version="1.1.1" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion examples/WeatherApp/WeatherApp.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Oxpecker" Version="1.0.0" />
<PackageReference Include="Oxpecker" Version="1.1.1" />
<PackageReference Include="Oxpecker.ViewEngine" Version="1.0.0" />
<PackageReference Include="Oxpecker.Htmx" Version="1.0.0" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Oxpecker.OpenApi/Oxpecker.OpenApi.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<PackageReleaseNotes>Major release</PackageReleaseNotes>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Oxpecker" Version="1.0.0" />
<PackageReference Include="Oxpecker" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
Expand Down
Loading

0 comments on commit 1cdd77b

Please sign in to comment.