Skip to content

Commit a306712

Browse files
committed
Refactor FsHttpUrl to FsHttpTarget and add header transformer
1 parent 49da06b commit a306712

11 files changed

+132
-70
lines changed

Directory.Build.props

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22

33
<PropertyGroup>
4-
<Version>13.3.0</Version>
4+
<Version>14.0.0</Version>
55

66
<Authors>Ronald Schlenker</Authors>
77
<Copyright>Copyright 2024 Ronald Schlenker</Copyright>
@@ -16,6 +16,14 @@
1616
<FsDocsReleaseNotesLink>https://www.nuget.org/packages/FsHttp#release-body-tab</FsDocsReleaseNotesLink>
1717

1818
<PackageReleaseNotes>
19+
v14.0.0
20+
- (Breaking change) Renamed types in Domain:
21+
BodyContent -> SinglepartContent
22+
RequestContent -> BodyContent
23+
FsHttpUrl -> FsHttpTarget
24+
- (Breaking change) FsHttpUrl (now FsHttpTarget) and Header restructured: method, address and queryParams are now part of the FsHttpTarget type.
25+
- Added `headerTransformers` to Config for better composability
26+
1927
v13.3.0
2028
- (Breaking change) All `Response._TAsync` functions (task based) in F# require a CancellationToken now.
2129
- (Breaking change) Extension methods rework

docs/Composability.fsx

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
(**
2+
---
3+
title: Composability
4+
category: Documentation
5+
categoryindex: 1
6+
index: 15
7+
---
8+
*)
9+
10+
(**
11+
## Composability
12+
13+
TODO: Use transformHeader to pre-configure URLs
14+
15+
*)

src/FsHttp/Defaults.fs

+18-16
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,22 @@ let defaultHeadersAndBodyPrintMode = {
6363

6464
let defaultDecompressionMethods = [ DecompressionMethods.All ]
6565

66-
let defaultConfig = {
67-
timeout = None
68-
defaultDecompressionMethods = defaultDecompressionMethods
69-
printHint = {
70-
requestPrintMode = HeadersAndBody(defaultHeadersAndBodyPrintMode)
71-
responsePrintMode = HeadersAndBody(defaultHeadersAndBodyPrintMode)
66+
let defaultConfig =
67+
{
68+
timeout = None
69+
defaultDecompressionMethods = defaultDecompressionMethods
70+
printHint = {
71+
requestPrintMode = HeadersAndBody(defaultHeadersAndBodyPrintMode)
72+
responsePrintMode = HeadersAndBody(defaultHeadersAndBodyPrintMode)
73+
}
74+
headerTransformers = []
75+
httpMessageTransformers = []
76+
httpClientHandlerTransformers = []
77+
httpClientTransformers = []
78+
httpClientFactory = defaultHttpClientFactory
79+
httpCompletionOption = HttpCompletionOption.ResponseHeadersRead
80+
proxy = None
81+
certErrorStrategy = Default
82+
bufferResponseContent = false
83+
cancellationToken = Threading.CancellationToken.None
7284
}
73-
httpMessageTransformers = []
74-
httpClientHandlerTransformers = []
75-
httpClientTransformers = []
76-
httpClientFactory = defaultHttpClientFactory
77-
httpCompletionOption = HttpCompletionOption.ResponseHeadersRead
78-
proxy = None
79-
certErrorStrategy = Default
80-
bufferResponseContent = false
81-
cancellationToken = Threading.CancellationToken.None
82-
}

src/FsHttp/Domain.fs

+32-41
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,13 @@ type Proxy = {
3434
credentials: System.Net.ICredentials option
3535
}
3636

37-
type HttpClientHandlerTransformer = (System.Net.Http.SocketsHttpHandler -> System.Net.Http.SocketsHttpHandler)
38-
39-
and Config = {
37+
type Config = {
4038
timeout: System.TimeSpan option
4139
defaultDecompressionMethods: System.Net.DecompressionMethods list
4240
printHint: PrintHint
41+
headerTransformers: list<Header -> Header>
4342
httpMessageTransformers: list<System.Net.Http.HttpRequestMessage -> System.Net.Http.HttpRequestMessage>
44-
httpClientHandlerTransformers: list<HttpClientHandlerTransformer>
43+
httpClientHandlerTransformers: list<System.Net.Http.SocketsHttpHandler -> System.Net.Http.SocketsHttpHandler>
4544
httpClientTransformers: list<System.Net.Http.HttpClient -> System.Net.Http.HttpClient>
4645
httpCompletionOption: System.Net.Http.HttpCompletionOption
4746
proxy: Proxy option
@@ -52,46 +51,30 @@ and Config = {
5251
cancellationToken: CancellationToken
5352
}
5453

55-
type ConfigTransformer = Config -> Config
54+
and ConfigTransformer = Config -> Config
5655

57-
type PrintHintTransformer = PrintHint -> PrintHint
56+
and PrintHintTransformer = PrintHint -> PrintHint
5857

59-
type FsHttpUrl = {
58+
and FsHttpTarget = {
59+
method: System.Net.Http.HttpMethod option
6060
address: string option
61-
additionalQueryParams: List<string * string>
61+
additionalQueryParams: list<string * string>
6262
}
6363

64-
type Header = {
65-
method: System.Net.Http.HttpMethod option
64+
and Header = {
65+
target: FsHttpTarget
6666
headers: Map<string, string>
6767
// We use a .Net type here, which we never do in other places.
6868
// Since Cookie is record style, I see no problem here.
6969
cookies: System.Net.Cookie list
7070
}
7171

72-
type ContentData =
73-
| TextContent of string
74-
| BinaryContent of byte array
75-
| StreamContent of System.IO.Stream
76-
| FormUrlEncodedContent of Map<string, string>
77-
| FileContent of string
78-
79-
type ContentType = {
80-
value: string
81-
charset: System.Text.Encoding option
82-
}
83-
84-
type ContentElement = {
85-
contentData: ContentData
86-
explicitContentType: ContentType option
87-
}
88-
89-
type RequestContent =
72+
type BodyContent =
9073
| Empty
91-
| Single of BodyContent
74+
| Single of SinglepartContent
9275
| Multi of MultipartContent
9376

94-
and BodyContent = {
77+
and SinglepartContent = {
9578
contentElement: ContentElement
9679
headers: Map<string, string>
9780
}
@@ -107,10 +90,26 @@ and MultipartElement = {
10790
fileName: string option
10891
}
10992

93+
and ContentData =
94+
| TextContent of string
95+
| BinaryContent of byte array
96+
| StreamContent of System.IO.Stream
97+
| FormUrlEncodedContent of Map<string, string>
98+
| FileContent of string
99+
100+
and ContentType = {
101+
value: string
102+
charset: System.Text.Encoding option
103+
}
104+
105+
and ContentElement = {
106+
contentData: ContentData
107+
explicitContentType: ContentType option
108+
}
109+
110110
type Request = {
111-
url: FsHttpUrl
112111
header: Header
113-
content: RequestContent
112+
content: BodyContent
114113
config: Config
115114
}
116115

@@ -140,7 +139,6 @@ and IToMultipartContext =
140139

141140
// TODO: Convert this to a class.
142141
and HeaderContext = {
143-
url: FsHttpUrl
144142
header: Header
145143
config: Config
146144
} with
@@ -155,15 +153,13 @@ and HeaderContext = {
155153

156154
interface IToRequest with
157155
member this.Transform() = {
158-
url = this.url
159156
header = this.header
160157
content = Empty
161158
config = this.config
162159
}
163160

164161
interface IToBodyContext with
165162
member this.Transform() = {
166-
url = this.url
167163
header = this.header
168164
bodyContent = {
169165
contentElement = {
@@ -177,7 +173,6 @@ and HeaderContext = {
177173

178174
interface IToMultipartContext with
179175
member this.Transform() = {
180-
url = this.url
181176
header = this.header
182177
config = this.config
183178
multipartContent = {
@@ -188,9 +183,8 @@ and HeaderContext = {
188183

189184
// TODO: Convert this to a class.
190185
and BodyContext = {
191-
url: FsHttpUrl
192186
header: Header
193-
bodyContent: BodyContent
187+
bodyContent: SinglepartContent
194188
config: Config
195189
} with
196190
interface IRequestContext<BodyContext> with
@@ -204,7 +198,6 @@ and BodyContext = {
204198

205199
interface IToRequest with
206200
member this.Transform() = {
207-
url = this.url
208201
header = this.header
209202
content = Single this.bodyContent
210203
config = this.config
@@ -215,7 +208,6 @@ and BodyContext = {
215208

216209
// TODO: Convert this to a class.
217210
and MultipartContext = {
218-
url: FsHttpUrl
219211
header: Header
220212
multipartContent: MultipartContent
221213
config: Config
@@ -231,7 +223,6 @@ and MultipartContext = {
231223

232224
interface IToRequest with
233225
member this.Transform() = {
234-
url = this.url
235226
header = this.header
236227
content = Multi this.multipartContent
237228
config = this.config

src/FsHttp/DomainExtensions.fs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ open System
55
open System.Net.Http.Headers
66
open FsHttp
77

8-
type FsHttpUrl with
8+
type FsHttpTarget with
99
member this.ToUriStringWithDefault(defaultValue) =
1010
let queryParamsString =
1111
this.additionalQueryParams

src/FsHttp/Dsl.CE.fs

+4
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,10 @@ type IRequestContext<'self> with
427427
member this.TimeoutInSeconds(context: IRequestContext<#IConfigure<ConfigTransformer, _>>, value) =
428428
Config.timeoutInSeconds value context.Self
429429

430+
[<CustomOperation("config_transformHeader")>]
431+
member this.TransformHeader(context: IRequestContext<#IConfigure<ConfigTransformer, _>>, transformer) =
432+
Config.transformHeader transformer context.Self
433+
430434
[<CustomOperation("config_setHttpClientFactory")>]
431435
member this.SetHttpClientFactory(context: IRequestContext<#IConfigure<ConfigTransformer, _>>, httpClientFactory) =
432436
Config.setHttpClientFactory httpClientFactory context.Self

src/FsHttp/Dsl.fs

+18-9
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ module Http =
3131
FsiInit.init ()
3232

3333
{
34-
url = {
35-
address = address
36-
additionalQueryParams = []
37-
}
3834
header = {
39-
method = method
35+
target = {
36+
method = method
37+
address = address
38+
additionalQueryParams = []
39+
}
4040
headers = Map.empty
4141
cookies = []
4242
}
@@ -98,7 +98,7 @@ module Header =
9898

9999
/// Adds a set of query parameters to the URL
100100
let query (queryParams: (string * string) list) (context: HeaderContext) =
101-
{ context with url.additionalQueryParams = context.url.additionalQueryParams @ queryParams }
101+
{ context with header.target.additionalQueryParams = context.header.target.additionalQueryParams @ queryParams }
102102

103103
/// Content-Types that are acceptable for the response
104104
let accept (contentType: string) (context: HeaderContext) =
@@ -453,11 +453,17 @@ module Multipart =
453453

454454
module Config =
455455
module With =
456-
let ignoreCertIssues config = { config with certErrorStrategy = AlwaysAccept }
456+
let ignoreCertIssues config =
457+
{ config with certErrorStrategy = AlwaysAccept }
458+
459+
let timeout value config =
460+
{ config with timeout = value }
457461

458-
let timeout value config = { config with timeout = value }
462+
let timeoutInSeconds value config =
463+
{ config with timeout = Some(TimeSpan.FromSeconds value) }
459464

460-
let timeoutInSeconds value config = { config with timeout = Some(TimeSpan.FromSeconds value) }
465+
let transformHeader transformer config =
466+
{ config with headerTransformers = config.headerTransformers @ [ transformer ] }
461467

462468
let setHttpClientFactory httpClientFactory config = { config with httpClientFactory = httpClientFactory }
463469

@@ -500,6 +506,9 @@ module Config =
500506
let timeoutInSeconds value context =
501507
context |> update (fun config -> config |> With.timeoutInSeconds value)
502508

509+
let transformHeader transformer context =
510+
context |> update (fun config -> config |> With.transformHeader transformer)
511+
503512
let setHttpClientFactory httpClientFactory context =
504513
context |> update (fun config -> config |> With.setHttpClientFactory httpClientFactory)
505514

src/FsHttp/Extensions.fs

+4
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,10 @@ type ConfigExtensions =
439439
static member TimeoutInSeconds(config: Domain.Config, value) =
440440
Config.With.timeoutInSeconds value config
441441

442+
[<Extension>]
443+
static member TransformHeader(config: Domain.Config, transformer) =
444+
Config.With.transformHeader transformer config
445+
442446
[<Extension>]
443447
static member SetHttpClientFactory(config: Domain.Config, httpClientFactory) =
444448
Config.With.setHttpClientFactory httpClientFactory config

src/FsHttp/Request.fs

+8-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ open FsHttp.Helper
99

1010
// TODO: Remove this
1111
let getAddressDefaults (request: Request) =
12-
let uri = request.url.ToUriStringWithDefault("")
13-
let method = request.header.method |> Option.defaultValue HttpMethod.Get
12+
let uri = request.header.target.ToUriStringWithDefault("")
13+
let method = request.header.target.method |> Option.defaultValue HttpMethod.Get
1414
uri, method
1515

1616
let addressToString (request: Request) =
@@ -133,6 +133,12 @@ let toAsync cancellationTokenOverride (context: IToRequest) =
133133
let request, requestMessage = toRequestAndMessage context
134134
do Fsi.logfn $"Sending request {addressToString request} ..."
135135

136+
let request =
137+
let mutable request = request
138+
for headerTransformer in request.config.headerTransformers do
139+
request <- { request with header = headerTransformer request.header }
140+
request
141+
136142
use finalRequestMessage =
137143
request.config.httpMessageTransformers
138144
|> List.fold (fun c n -> n c) requestMessage

src/Tests/Config.fs

+22
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,25 @@ let ``Cancellation token can be supplied by user`` () =
145145
|> should equal true
146146

147147
wasCancelled |> should equal true
148+
149+
150+
let [<TestCase>] ``Header Transformer``() =
151+
let url = "http://"
152+
let urlSuffix1 = "suffix1"
153+
let urlSuffix2 = "suffix2"
154+
155+
let request =
156+
let transformWith suffix =
157+
fun (header: Header) ->
158+
let address = header.target.address.Value
159+
{ header with target.address = Some $"{address}{suffix}" }
160+
161+
http {
162+
GET url
163+
config_transformHeader (transformWith urlSuffix1)
164+
config_transformHeader (transformWith urlSuffix2)
165+
}
166+
167+
request.header.target.address
168+
|> Option.defaultValue ""
169+
|> should equal $"{url}{urlSuffix1}{urlSuffix2}"

src/Tests/Misc.fs

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ let ``Response Decompression`` () =
127127
sr.ReadToEnd()
128128
|> should equal responseText
129129

130+
130131
//let [<TestCase>] ``Auto Redirects``() =
131132
// http {
132133
// GET (url @"")

0 commit comments

Comments
 (0)