Skip to content

Commit 9981074

Browse files
SchlenkRSchlenkR
authored andcommitted
Enumerable as content, some other things
1 parent 7c407e4 commit 9981074

File tree

11 files changed

+208
-188
lines changed

11 files changed

+208
-188
lines changed

.vscode/launch.json

Lines changed: 0 additions & 26 deletions
This file was deleted.

.vscode/tasks.json

Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,34 @@
11
{
2-
"version": "2.0.0",
3-
"tasks": [
4-
{
5-
"label": "build",
6-
"command": "dotnet",
7-
"type": "process",
8-
"args": [
9-
"build",
10-
"${workspaceFolder}/src/Test.CSharp/Test.CSharp.csproj",
11-
"/property:GenerateFullPaths=true",
12-
"/consoleloggerparameters:NoSummary"
13-
],
14-
"problemMatcher": "$msCompile"
15-
},
16-
{
17-
"label": "publish",
18-
"command": "dotnet",
19-
"type": "process",
20-
"args": [
21-
"publish",
22-
"${workspaceFolder}/src/Test.CSharp/Test.CSharp.csproj",
23-
"/property:GenerateFullPaths=true",
24-
"/consoleloggerparameters:NoSummary"
25-
],
26-
"problemMatcher": "$msCompile"
27-
},
28-
{
29-
"label": "watch",
30-
"command": "dotnet",
31-
"type": "process",
32-
"args": [
33-
"watch",
34-
"run",
35-
"--project",
36-
"${workspaceFolder}/src/Test.CSharp/Test.CSharp.csproj"
37-
],
38-
"problemMatcher": "$msCompile"
39-
}
40-
]
41-
}
2+
"version": "2.0.0",
3+
"options": {
4+
"cwd": "."
5+
},
6+
"windows": {
7+
"options": {
8+
"shell": {
9+
"executable": "C:\\Program Files\\Git\\bin\\bash.exe",
10+
"args": ["-c"]
11+
}
12+
}
13+
},
14+
"tasks": [
15+
{
16+
"label": "FsHttp: Build Solution",
17+
"type": "shell",
18+
"command": "./build.sh",
19+
"group": {
20+
"kind": "build",
21+
"isDefault": false
22+
}
23+
},
24+
{
25+
"label": "FsHttp: Test Solution",
26+
"type": "shell",
27+
"command": "./test.sh",
28+
"group": {
29+
"kind": "build",
30+
"isDefault": false
31+
}
32+
}
33+
]
34+
}

Directory.Build.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
<FsDocsReleaseNotesLink>https://www.nuget.org/packages/FsHttp#release-body-tab</FsDocsReleaseNotesLink>
1717

1818
<PackageReleaseNotes>
19+
15.0.0
20+
- Added 'enumerable' and 'enumerablePart' as body content functions
21+
- Removed Utf8StringBufferingStream
22+
1923
14.5.1
2024
- Fixed untracked bug: using config_useBaseUrl as http template won't crash when printing unfinished requests
2125

build.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dotnet build

docu.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ if [ -d ./.fsdocs ]; then
1616
fi
1717

1818
dotnet tool restore
19-
dotnet build ./src/FsHttp/FsHttp.fsproj -c Release -f net6.0
19+
dotnet build ./src/FsHttp/FsHttp.fsproj -c Release -f net8.0
2020

2121
# what a hack...
2222
if [ -z "$1" ]; then
@@ -29,7 +29,7 @@ dotnet fsdocs \
2929
$mode \
3030
--clean \
3131
--sourcefolder ./src \
32-
--properties Configuration=Release TargetFramework=net6.0 \
32+
--properties Configuration=Release TargetFramework=net8.0 \
3333
--sourcerepo https://github.com/fsprojects/FsHttp/blob/master/src \
3434
--parameters root /FsHttp/ \
3535
--output ./.docsOutput \

fiddle/utf8StreamStuff.fsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
open System
2+
open System.IO
3+
open System.Text
4+
5+
let readUtf8StringAsync maxLen (stream: Stream) =
6+
async {
7+
use reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks = false, bufferSize = 1024, leaveOpen = true)
8+
let sb = StringBuilder()
9+
let mutable codePointsRead = 0
10+
let buffer = Array.zeroCreate<char> 1 // Buffer to read one character at a time
11+
while codePointsRead < maxLen && not reader.EndOfStream do
12+
// Read the next character asynchronously
13+
let! charsRead = reader.ReadAsync(buffer, 0, 1) |> Async.AwaitTask
14+
if charsRead > 0 then
15+
let c = buffer.[0]
16+
sb.Append(c) |> ignore
17+
// Check if the character is a high surrogate
18+
if Char.IsHighSurrogate(c) && not reader.EndOfStream then
19+
// Read the low surrogate asynchronously and append it
20+
let! nextCharsRead = reader.ReadAsync(buffer, 0, 1) |> Async.AwaitTask
21+
if nextCharsRead > 0 then
22+
let nextC = buffer.[0]
23+
sb.Append(nextC) |> ignore
24+
// Increment the code point count
25+
codePointsRead <- codePointsRead + 1
26+
return sb.ToString()
27+
}
28+
29+
let text = "a😉b🙁🙂d"
30+
31+
let test len (expected: string) =
32+
let res =
33+
new MemoryStream(Encoding.UTF8.GetBytes(text))
34+
|> readUtf8StringAsync len
35+
|> Async.RunSynchronously
36+
let s1 = Encoding.UTF8.GetBytes res |> Array.toList
37+
let s2 = Encoding.UTF8.GetBytes expected |> Array.toList
38+
let res = (s1 = s2)
39+
if not res then
40+
printfn ""
41+
printfn "count = %d" len
42+
printfn "expected = %s" expected
43+
printfn ""
44+
printfn "Expected: %A" s2
45+
printfn ""
46+
printfn "Actual : %A" s1
47+
printfn ""
48+
printfn " ----------------------------"
49+
50+
test 0 ""
51+
test 1 "a"
52+
test 2 "a😉"
53+
test 3 "a😉b"
54+
test 4 "a😉b🙁"
55+
test 5 "a😉b🙁🙂"
56+
test 6 "a😉b🙁🙂d"

src/FsHttp/Dsl.CE.fs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,10 @@ type IRequestContext<'self> with
329329
member this.Stream(context: IRequestContext<BodyContext>, stream) =
330330
Body.stream stream context.Self
331331

332+
[<CustomOperation("enumerable")>]
333+
member this.Enumerable(context: IRequestContext<BodyContext>, data) =
334+
Body.enumerable data context.Self
335+
332336
[<CustomOperation("text")>]
333337
member this.Text(context: IRequestContext<BodyContext>, text) =
334338
Body.text text context.Self
@@ -403,6 +407,10 @@ type IRequestContext<'self> with
403407
member this.StreamPart(context: IRequestContext<#IToMultipartContext>, value, name, ?fileName) =
404408
Multipart.streamPart value name fileName context.Self
405409

410+
[<CustomOperation("enumerablePart")>]
411+
member this.EnumerablePart(context: IRequestContext<#IToMultipartContext>, value, name, ?fileName) =
412+
Multipart.enumerablePart value name fileName context.Self
413+
406414

407415
// ---------
408416
// Config

src/FsHttp/Dsl.fs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,10 @@ module Body =
344344
(StreamContent stream)
345345
context
346346

347+
let enumerable (sequence: byte seq) (context: IToBodyContext) =
348+
let enumerableStream = new EnumerableStream(sequence)
349+
stream enumerableStream context
350+
347351
let text (text: string) (context: IToBodyContext) =
348352
content
349353
{
@@ -450,6 +454,10 @@ module Multipart =
450454
let streamPart (value: System.IO.Stream) name fileName (context: IToMultipartContext) =
451455
part (StreamContent value) name fileName context
452456

457+
let enumerablePart (value: byte seq) name fileName (context: IToMultipartContext) =
458+
let enumerableStream = new EnumerableStream(value)
459+
streamPart enumerableStream name fileName context
460+
453461

454462
module Config =
455463

src/FsHttp/Helper.fs

Lines changed: 65 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -64,87 +64,36 @@ module Url =
6464
let b = (norm url2).TrimStart(delTrim).TrimEnd(delTrim)
6565
a + sdel + b
6666

67-
// TODO: Inefficient
68-
type Utf8StringBufferingStream(baseStream: Stream, readBufferLimit: int option) =
69-
inherit Stream()
70-
let notSeekable () = failwith "Stream is not seekable."
71-
let notWritable () = failwith "Stream is not writable."
72-
let readBuffer = ResizeArray<byte>()
73-
override _.Flush() = baseStream.Flush()
74-
75-
override _.Read(buffer, offset, count) =
76-
let readCount = baseStream.Read(buffer, offset, count)
77-
78-
match readCount, readBufferLimit with
79-
| 0, _ -> ()
80-
| readCount, None -> readBuffer.AddRange(buffer[offset .. readCount - 1])
81-
| readCount, Some limit ->
82-
let remaining = limit - readBuffer.Count
83-
let copyCount = min remaining readCount
84-
85-
if copyCount > 0 then
86-
readBuffer.AddRange(buffer[offset .. copyCount - 1])
87-
88-
readCount
89-
90-
override _.Seek(_, _) = notSeekable ()
91-
override _.SetLength(_) = notWritable ()
92-
override _.Write(_, _, _) = notWritable ()
93-
override _.CanRead = true
94-
override _.CanSeek = false
95-
override _.CanWrite = false
96-
override _.Length = baseStream.Length
97-
98-
override _.Position
99-
with get () = baseStream.Position
100-
and set (_) = notSeekable ()
101-
102-
member _.GetUtf8String() =
103-
let buffer = CollectionsMarshal.AsSpan(readBuffer)
104-
let s = Encoding.UTF8.GetString(buffer).AsSpan()
105-
106-
if s.Length = 0 then
107-
s.ToString()
108-
else
109-
let s =
110-
if s[s.Length - 1] |> Char.IsHighSurrogate then
111-
s.Slice(0, s.Length - 1)
112-
else
113-
s
114-
115-
s.ToString()
116-
11767
[<RequireQualifiedAccess>]
11868
module Stream =
119-
let readUtf8StringAsync maxUtf16CharCount (stream: Stream) =
120-
let sr = new StreamReader(stream, Encoding.UTF8)
121-
let sb = StringBuilder()
122-
let charBuffer = Array.zeroCreate<char> (50)
123-
let mutable lastCharIsHighSurrogate = false
124-
125-
let rec read () =
126-
async {
127-
if sb.Length < maxUtf16CharCount then
128-
let! readCount = sr.ReadAsync(charBuffer, 0, charBuffer.Length) |> Async.AwaitTask
129-
let remaining = maxUtf16CharCount - sb.Length
130-
let copyCount = min remaining readCount
131-
132-
if copyCount > 0 then
133-
sb.Append(charBuffer, 0, copyCount) |> ignore
134-
lastCharIsHighSurrogate <- charBuffer[copyCount - 1] |> Char.IsHighSurrogate
135-
do! read ()
136-
}
137-
69+
let readUtf8StringAsync maxLen (stream: Stream) =
70+
// we could definitely optimize this
13871
async {
139-
do! read ()
140-
141-
do
142-
sr.Dispose()
143-
stream.Dispose()
144-
145-
return sb.ToString(0, (if lastCharIsHighSurrogate then sb.Length - 1 else sb.Length))
72+
use reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks = false, bufferSize = 1024, leaveOpen = true)
73+
let sb = StringBuilder()
74+
let mutable codePointsRead = 0
75+
let buffer = Array.zeroCreate<char> 1 // Buffer to read one character at a time
76+
while codePointsRead < maxLen && not reader.EndOfStream do
77+
// Read the next character asynchronously
78+
let! charsRead = reader.ReadAsync(buffer, 0, 1) |> Async.AwaitTask
79+
if charsRead > 0 then
80+
let c = buffer.[0]
81+
sb.Append(c) |> ignore
82+
// Check if the character is a high surrogate
83+
if Char.IsHighSurrogate(c) && not reader.EndOfStream then
84+
// Read the low surrogate asynchronously and append it
85+
let! nextCharsRead = reader.ReadAsync(buffer, 0, 1) |> Async.AwaitTask
86+
if nextCharsRead > 0 then
87+
let nextC = buffer.[0]
88+
sb.Append(nextC) |> ignore
89+
// Increment the code point count
90+
codePointsRead <- codePointsRead + 1
91+
return sb.ToString()
14692
}
14793

94+
let readUtf8StringTAsync maxLength (stream: Stream) =
95+
readUtf8StringAsync maxLength stream |> Async.StartAsTask
96+
14897
let copyToCallbackAsync (target: Stream) callback (source: Stream) =
14998
async {
15099
let buffer = Array.create 1024 (byte 0)
@@ -218,3 +167,43 @@ module Stream =
218167
}
219168

220169
let saveFileTAsync fileName source = saveFileAsync fileName source |> Async.StartAsTask
170+
171+
172+
type EnumerableStream(source: byte seq) =
173+
inherit Stream()
174+
175+
let enumerator = source.GetEnumerator()
176+
let mutable isDisposed = false
177+
let mutable position = 0L
178+
179+
override _.CanRead = true
180+
override _.CanSeek = false
181+
override _.CanWrite = false
182+
183+
override _.Length = raise (NotSupportedException())
184+
185+
override _.Position
186+
with get() = position
187+
and set(_) = raise (NotSupportedException())
188+
189+
override _.Flush() = ()
190+
191+
override _.Read(buffer: byte[], offset: int, count: int) =
192+
let bytesToRead = Math.Min(count, buffer.Length - int position)
193+
if bytesToRead <= 0 then 0
194+
else
195+
let mutable bytesRead = 0
196+
while bytesRead < bytesToRead && enumerator.MoveNext() do
197+
buffer.[offset + bytesRead] <- enumerator.Current
198+
bytesRead <- bytesRead + 1
199+
position <- position + int64 bytesRead
200+
bytesRead
201+
202+
override _.Seek(_: int64, _: SeekOrigin) = raise (NotSupportedException())
203+
override _.SetLength(_: int64) = raise (NotSupportedException())
204+
override _.Write(_: byte[], _: int, _: int) = raise (NotSupportedException())
205+
206+
override _.Dispose(disposing: bool) =
207+
if not isDisposed then
208+
isDisposed <- true
209+
enumerator.Dispose()

0 commit comments

Comments
 (0)