diff --git a/.gitignore b/.gitignore
index e69de29..52fe2f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/Meow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 0000000..7c5d580
--- /dev/null
+++ b/Meow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -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
+}
diff --git a/Meow.xcodeproj/project.xcworkspace/xcuserdata/bkreeger.xcuserdatad/UserInterfaceState.xcuserstate b/Meow.xcodeproj/project.xcworkspace/xcuserdata/bkreeger.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index aeb479c..0000000
Binary files a/Meow.xcodeproj/project.xcworkspace/xcuserdata/bkreeger.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/Meow.xcodeproj/xcuserdata/bkreeger.xcuserdatad/xcschemes/xcschememanagement.plist b/Meow.xcodeproj/xcuserdata/bkreeger.xcuserdatad/xcschemes/xcschememanagement.plist
deleted file mode 100644
index d3f60a2..0000000
--- a/Meow.xcodeproj/xcuserdata/bkreeger.xcuserdatad/xcschemes/xcschememanagement.plist
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
- SchemeUserState
-
- App.xcscheme_^#shared#^_
-
- orderHint
- 0
-
-
- SuppressBuildableAutocreation
-
- C3BB763A2ED0C38800D56534
-
- primary
-
-
- C3BB76472ED0C38900D56534
-
- primary
-
-
- C3BB76512ED0C38900D56534
-
- primary
-
-
-
-
-
diff --git a/Meow/.gitignore b/Meow/.gitignore
deleted file mode 100644
index 0023a53..0000000
--- a/Meow/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-.DS_Store
-/.build
-/Packages
-xcuserdata/
-DerivedData/
-.swiftpm/configuration/registries.json
-.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
-.netrc
diff --git a/Meow/Package.swift b/Meow/Package.swift
index f410d81..3c48479 100644
--- a/Meow/Package.swift
+++ b/Meow/Package.swift
@@ -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
),
]
)
diff --git a/Meow/Sources/Meow/Meow.swift b/Meow/Sources/Meow/Meow.swift
index 0a53ca7..f9a08dd 100644
--- a/Meow/Sources/Meow/Meow.swift
+++ b/Meow/Sources/Meow/Meow.swift
@@ -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(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)")
}
diff --git a/Meow/Tests/MeowTests/Fixtures.swift b/Meow/Tests/MeowTests/Fixtures.swift
new file mode 100644
index 0000000..dbd192c
--- /dev/null
+++ b/Meow/Tests/MeowTests/Fixtures.swift
@@ -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
+ }
+}
diff --git a/Meow/Tests/MeowTests/Fixtures/facts.json b/Meow/Tests/MeowTests/Fixtures/facts.json
new file mode 100644
index 0000000..4b26196
--- /dev/null
+++ b/Meow/Tests/MeowTests/Fixtures/facts.json
@@ -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."
+ ]
+}
diff --git a/Meow/Tests/MeowTests/MeowTests.swift b/Meow/Tests/MeowTests/MeowTests.swift
index 1456482..c442d0e 100644
--- a/Meow/Tests/MeowTests/MeowTests.swift
+++ b/Meow/Tests/MeowTests/MeowTests.swift
@@ -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()
+ }
}