Skip to content

Commit fbdefb1

Browse files
committed
updated tests
1 parent e9a2c27 commit fbdefb1

File tree

9 files changed

+397
-105
lines changed

9 files changed

+397
-105
lines changed

README.md

Lines changed: 216 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,218 @@
11
# SwiftHttp
22

3-
A description of this package.
3+
An awesome Swift HTTP library to rapidly setup the communication layer with API endpoints.
4+
5+
```swift
6+
import SwiftHttp
7+
8+
print(html)
9+
```
10+
11+
12+
## Install
13+
14+
You can simply use `SwiftHtml` as a dependency via the Swift Package Manager:
15+
16+
```swift
17+
.package(url: "https://github.com/binarybirds/swift-html", from: "1.6.0"),
18+
```
19+
20+
Add the `SwiftHtml` product from the `swift-html` package as a dependency to your target:
21+
22+
```swift
23+
.product(name: "SwiftHtml", package: "swift-html"),
24+
```
25+
26+
Import the framework:
27+
28+
```swift
29+
import SwiftHtml
30+
```
31+
32+
That's it.
33+
34+
35+
## Creating custom tags
36+
37+
You can define your own custom tags by subclassing the `Tag` or `EmptyTag` class.
38+
39+
You can follow the same pattern if you take a look at the core tags.
40+
41+
```swift
42+
open class Div: Tag {
43+
44+
}
45+
46+
// <div></div> - standard tag
47+
48+
open class Br: EmptyTag {
49+
50+
}
51+
// <br> - no closing tag
52+
53+
```
54+
55+
By default the name of the tag is automatically derived from the class name (lowercased), but you can also create your own tag type & name by overriding the `createNode()` class function.
56+
57+
```swift
58+
open class LastBuildDate: Tag {
59+
60+
open override class func createNode() -> Node {
61+
Node(type: .standard, name: "lastBuildDate")
62+
}
63+
}
64+
65+
// <lastBuildDate></lastBuildDate> - standard tag with custom name
66+
```
67+
68+
It is also possible to create tags with altered content or default attributes.
69+
70+
```swift
71+
open class Description: Tag {
72+
73+
public init(_ contents: String) {
74+
super.init()
75+
setContents("<![CDATA[" + contents + "]]>")
76+
}
77+
}
78+
// <description><![CDATA[lorem ipsum]]></description> - content wrapped in CDATA
79+
80+
open class Rss: Tag {
81+
82+
public init(@TagBuilder _ builder: () -> [Tag]) {
83+
super.init(builder())
84+
setAttributes([
85+
.init(key: "version", value: "2.0"),
86+
])
87+
}
88+
}
89+
// <rss version="2.0">...</rss> - tag with a default attribute
90+
```
91+
92+
## Attribute management
93+
94+
You can set, add or delete the attributes of a given tag.
95+
96+
```swift
97+
Leaf("example")
98+
// set (override) the current attributes
99+
.setAttributes([
100+
.init(key: "a", value: "foo"),
101+
.init(key: "b", value: "bar"),
102+
.init(key: "c", value: "baz"),
103+
])
104+
// add a new attribute using a key & value
105+
.attribute("foo", "example")
106+
// add a new flag attribute (without a value)
107+
.flagAttribute("bar")
108+
// delete an attribute by using a key
109+
.deleteAttribute("b")
110+
111+
// <leaf a="foo" c="baz" foo="example" bar></leaf>
112+
```
113+
114+
You can also manage the class atrribute through helper methods.
115+
116+
```swift
117+
Span("foo")
118+
// set (override) class values
119+
.class("a", "b", "c")
120+
// add new class values
121+
.class(add: ["d", "e", "f"])
122+
// add new class value if the condition is true
123+
.class(add: "b", true)
124+
/// remove multiple class values
125+
.class(remove: ["b", "c", "d"])
126+
/// remove a class value if the condition is true
127+
.class(remove: "e", true)
128+
129+
// <span class="a f"></span>
130+
```
131+
132+
You can create your own attribute modifier via an extension.
133+
134+
```swift
135+
public extension Guid {
136+
137+
func isPermalink(_ value: Bool = true) -> Self {
138+
attribute("isPermalink", String(value))
139+
}
140+
}
141+
```
142+
143+
There are other built-in type-safe attribute modifiers available on tags.
144+
145+
146+
## Composing tags
147+
148+
You can come up with your own `Tag` composition system by introducing a new protocol.
149+
150+
```swift
151+
protocol TagRepresentable {
152+
153+
func build() -> Tag
154+
}
155+
156+
struct ListComponent: TagRepresentable {
157+
158+
let items: [String]
159+
160+
init(_ items: [String]) {
161+
self.items = items
162+
}
163+
164+
@TagBuilder
165+
func build() -> Tag {
166+
Ul {
167+
items.map { Li($0) }
168+
}
169+
}
170+
}
171+
172+
let tag = ListComponent(["a", "b", "c"]).build()
173+
```
174+
175+
This way it is also possible to extend the `TagBuilder` to support the new protocol.
176+
177+
```swift
178+
extension TagBuilder {
179+
180+
static func buildExpression(_ expression: TagRepresentable) -> Tag {
181+
expression.build()
182+
}
183+
184+
static func buildExpression(_ expression: TagRepresentable) -> [Tag] {
185+
[expression.build()]
186+
}
187+
188+
static func buildExpression(_ expression: [TagRepresentable]) -> [Tag] {
189+
expression.map { $0.build() }
190+
}
191+
192+
static func buildExpression(_ expression: [TagRepresentable]) -> Tag {
193+
GroupTag {
194+
expression.map { $0.build() }
195+
}
196+
}
197+
}
198+
```
199+
200+
Sometimes you'll need extra parameters for the build function, so you have to call the build method by hand.
201+
202+
In those cases it is recommended to introduce a `render` function instead of using build.
203+
204+
```swift
205+
206+
let tag = WebIndexTemplate(ctx) {
207+
ListComponent(["a", "b", "c"])
208+
.render(req)
209+
}
210+
.render(req)
211+
```
212+
213+
If you want to create a lightweight template engine for the [Vapor](https://vapor.codes/) web framework using SwiftHtml, you can see a working example inside the [Feather CMS core](https://github.com/FeatherCMS/feather-core) repository.
214+
215+
216+
## Credits & references
217+
218+
- [HTML Reference](https://www.w3schools.com/tags/default.asp)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// CustomDecoder.swift
3+
// SwiftHttpTests
4+
//
5+
// Created by Tibor Bodecs on 2022. 03. 11..
6+
//
7+
8+
import Foundation
9+
import SwiftHttp
10+
11+
public extension HttpResponseDecoder {
12+
13+
static func custom() -> HttpResponseDecoder {
14+
.init(decoder: CustomDataDecoder(), validators: [
15+
HttpHeaderValidator(.key(.contentType)) {
16+
$0.contains("application/json")
17+
},
18+
])
19+
}
20+
}
21+
22+
/// custom data decoder that transforms from a wrong object to a nice object...
23+
struct CustomDataDecoder: HttpDataDecoder {
24+
struct WrongPost: Codable {
25+
let id: Int
26+
}
27+
28+
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
29+
let wrongPost = try JSONDecoder().decode(WrongPost.self, from: data)
30+
return Post(userId: 1, id: wrongPost.id, title: "lorem", body: "ipsum") as! T
31+
}
32+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Tibor Bodecs on 2022. 03. 11..
6+
//
7+
8+
import Foundation
9+
import SwiftHttp
10+
11+
struct FeatherError: Codable {
12+
let message: String
13+
}
14+
15+
struct FeatherApi {
16+
17+
let client = UrlSessionHttpClient(log: true)
18+
let apiBaseUrl = HttpUrl(scheme: "http", host: "test.binarybirds.com")
19+
20+
func test() async throws -> [Post] {
21+
let pipeline = HttpDecodablePipeline<[Post]>(url: apiBaseUrl.path("api", "test"),
22+
method: .get,
23+
validators: [
24+
HttpStatusCodeValidator(.ok)
25+
],
26+
decoder: .json())
27+
return try await pipeline.execute(client.dataTask)
28+
}
29+
30+
func testQueryParams() async throws -> String? {
31+
let pipeline = HttpRawPipeline(url: apiBaseUrl
32+
.path("api", "status")
33+
.query([
34+
"foo": "bar"
35+
]),
36+
method: .get,
37+
validators: [
38+
HttpStatusCodeValidator(.ok)
39+
])
40+
return try await pipeline.execute(client.dataTask).utf8String
41+
}
42+
}

Tests/SwiftHttpTests/Helpers/PostsApi.swift renamed to Tests/SwiftHttpTests/Helpers/PostApi.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
import Foundation
99
import SwiftHttp
1010

11-
12-
struct PostsApi: HttpCodablePipelineCollection {
11+
struct PostApi: HttpCodablePipelineCollection {
1312

1413
let client: HttpClient = UrlSessionHttpClient(log: true)
1514
let apiBaseUrl = HttpUrl(host: "jsonplaceholder.typicode.com")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Tibor Bodecs on 2022. 03. 11..
6+
//
7+
8+
import Foundation
9+
10+
struct Todo: Codable {
11+
let id: Int
12+
let title: String
13+
let completed: Bool
14+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Tibor Bodecs on 2022. 03. 11..
6+
//
7+
8+
import Foundation
9+
import SwiftHttp
10+
11+
struct TodoApi: HttpCodablePipelineCollection {
12+
13+
let client: HttpClient = UrlSessionHttpClient(log: true)
14+
let apiBaseUrl = HttpUrl(host: "jsonplaceholder.typicode.com")
15+
16+
17+
func list() async throws -> [Todo] {
18+
try await decodableRequest(executor: client.dataTask,
19+
url: apiBaseUrl.path("todos"),
20+
method: .get)
21+
}
22+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// PostTests.swift
3+
// SwiftHttpTests
4+
//
5+
// Created by Tibor Bodecs on 2022. 03. 11..
6+
//
7+
8+
import XCTest
9+
@testable import SwiftHttp
10+
11+
final class PostTests: XCTestCase {
12+
13+
let api = PostApi()
14+
15+
func testList() async throws {
16+
let posts = try await api.listPosts()
17+
XCTAssertEqual(posts.count, 100)
18+
}
19+
20+
func testGet() async throws {
21+
let post = try await api.getPost(1)
22+
XCTAssertEqual(post.id, 1)
23+
}
24+
25+
func testCreate() async throws {
26+
let object = Post(userId: 1, id: 1, title: "lorem ipsum", body: "dolor sit amet")
27+
let post = try await api.createPost(object)
28+
XCTAssertEqual(post.id, 101)
29+
}
30+
31+
func testUpdate() async throws {
32+
let object = Post.Update(userId: 1, title: "lorem ipsum", body: "dolor sit amet")
33+
let post = try await api.updatePost(1, object)
34+
XCTAssertEqual(post.id, 1)
35+
}
36+
37+
func testPatch() async throws {
38+
let object = Post.Update(userId: 1, title: "lorem ipsum", body: "dolor sit amet")
39+
let post = try await api.patchPost(1, object)
40+
XCTAssertEqual(post.id, 1)
41+
}
42+
43+
func testDelete() async throws {
44+
let res = try await api.deletePost(1)
45+
XCTAssertEqual(res.statusCode, .ok)
46+
}
47+
48+
}
49+

0 commit comments

Comments
 (0)