Added network call for facts

This commit is contained in:
Ben Kreeger 2025-11-21 16:53:24 -06:00
parent 36a58874e0
commit 2bfe9a90cd
Signed by: kreeger
GPG Key ID: D5CF8683D4BE4B50
10 changed files with 173 additions and 51 deletions

62
.gitignore vendored
View File

@ -0,0 +1,62 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build/
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

View File

@ -0,0 +1,15 @@
{
"originHash" : "b5a4d1a917ee731eae9d288a993a1e905ecfa9e87cb169c76452216c83b5b4f2",
"pins" : [
{
"identity" : "mocker",
"kind" : "remoteSourceControl",
"location" : "https://github.com/WeTransfer/Mocker",
"state" : {
"revision" : "95fa785c751f6bc40c49e112d433c3acf8417a97",
"version" : "3.0.2"
}
}
],
"version" : 3
}

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>App.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>C3BB763A2ED0C38800D56534</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>C3BB76472ED0C38900D56534</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>C3BB76512ED0C38900D56534</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>

8
Meow/.gitignore vendored
View File

@ -1,8 +0,0 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

@ -3,6 +3,14 @@
import PackageDescription
let commonSwiftSettings: [SwiftSetting] = [
.enableExperimentalFeature("StrictConcurrency"),
.enableUpcomingFeature("InferIsolatedConformances"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.defaultIsolation(nil),
.swiftLanguageMode(.v6)
]
let package = Package(
name: "Meow",
platforms: [.iOS(.v18), .macOS(.v15)],
@ -13,15 +21,26 @@ let package = Package(
targets: ["Meow"]
),
],
dependencies: [
.package(url: "https://github.com/WeTransfer/Mocker", from: "3.0.2"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "Meow"
name: "Meow",
swiftSettings: commonSwiftSettings
),
.testTarget(
name: "MeowTests",
dependencies: ["Meow"]
dependencies: [
"Meow",
"Mocker"
],
resources: [
.process("Fixtures")
],
swiftSettings: commonSwiftSettings
),
]
)

View File

@ -7,30 +7,37 @@ import Foundation
public struct Meow {
let baseURL: URL
let session: URLSession
let urlSession: URLSession
public init(baseURL: URL, session: URLSession = .shared) {
public init(baseURL: URL, urlSession: URLSession = .shared) {
self.baseURL = baseURL
self.session = session
self.urlSession = urlSession
}
public func getFacts() async throws -> [String] {
let request = try generateRequest(path: "/")
public func getFacts(count: Int? = nil) async throws -> [String] {
let params = count.map { ["count": $0] } ?? [:]
let request = try generateRequest(path: "/", params: params)
let response: FactsResponse = try await decodeRequest(request: request)
return response.data
}
// MARK: - Private functionality
private func generateRequest(path: String) throws -> URLRequest {
private func generateRequest(path: String, params: [String: CustomStringConvertible] = [:]) throws -> URLRequest {
guard let url = URL(string: path, relativeTo: baseURL) else {
throw MeowError.requestError("Couldn't generate URL from path: \(path), baseURL: \(baseURL)")
}
return URLRequest(url: url)
let queryItems = params
.compactMap { URLQueryItem(name: $0, value: $1.description) }
.sorted { a, b in a.name < b.name }
return queryItems.count > 0
? URLRequest(url: url.appending(queryItems: queryItems))
: URLRequest(url: url)
}
private func decodeRequest<T: Decodable>(request: URLRequest) async throws -> T {
let (data, response) = try await session.data(for: request)
let (data, response) = try await urlSession.data(for: request)
guard let response = response as? HTTPURLResponse else {
throw MeowError.connectionError("Couldn't get HTTP response from request \(request)")
}

View File

@ -0,0 +1,20 @@
//
// Fixtures.swift
// MeowTests
//
import Foundation
final class Fixtures {
static let facts: Data = loadFixture("facts")
private static func loadFixture(_ name: String) -> Data {
guard let url = Bundle.module.url(forResource: name, withExtension: "json") else {
fatalError("Failed to load fixture: \(name).json")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Unable to get Data from contents of \(url)")
}
return data
}
}

View File

@ -0,0 +1,9 @@
{
"data": [
"Abraham Lincoln loved cats. He had four of them while he lived in the White House.",
"Many cats cannot properly digest cows milk. Milk and milk products give them diarrhea.",
"Tylenol and chocolate are both poisonous to cats.",
"The average cat food meal is the equivalent to about five mice.",
"The way you treat kittens in the early stages of it's life will render it's personality traits later in life."
]
}

View File

@ -5,15 +5,45 @@
import Foundation
import Testing
import Mocker
@testable import Meow
@Suite("Meow Tests")
struct MeowTests {
private let baseURL = URL(string: "https://meow.meow")!
let urlSession: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.protocolClasses = [MockingURLProtocol.self]
return URLSession(configuration: configuration)
}()
@Test
func constructor() async throws {
let instance = Meow(baseURL: baseURL)
let instance = vendInstance()
#expect(instance.baseURL == baseURL)
}
@Test
func getFacts() async throws {
let instance = vendInstance()
let count = 5
mockPath("/", queryItems: ["count": count], data: [.get: Fixtures.facts])
let facts = try await instance.getFacts(count: count)
#expect(facts.count == count)
}
// MARK: - Private functionality
private func vendInstance() -> Meow {
return Meow(baseURL: baseURL, urlSession: urlSession)
}
private func mockPath(_ path: String, headers: [String: String] = [:], queryItems: [String: CustomStringConvertible] = [:], data: [Mock.HTTPMethod: Data] = [:]) {
let urlQueryItems = queryItems
.compactMap({ URLQueryItem(name: $0, value: $1.description) })
.sorted { a, b in a.name < b.name }
let base = baseURL.appendingPathComponent(path)
let url = urlQueryItems.isEmpty ? base : base.appending(queryItems: urlQueryItems)
Mock(url: url, ignoreQuery: false, contentType: .json, statusCode: 200, data: data, additionalHeaders: headers).register()
}
}