Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a30b69561 | |||
| de5b4ff5d0 | |||
| d01b60e6dd | |||
| 791ebf4f78 | |||
| 24d703b967 | |||
| a1a649838c | |||
| bfc9e67d38 |
@@ -1 +1,37 @@
|
||||
[](https://swiftpackageindex.com/rock-n-code/discogs-service)
|
||||
[](https://swiftpackageindex.com/rock-n-code/discogs-service)
|
||||
|
||||
# Discogs Service
|
||||
|
||||
A library written entirely with [Swift](https://www.swift.org) that allow the developer to interact with the [Discogs API](https://www.discogs.com/developers/#) backend service.
|
||||
|
||||
## Installation
|
||||
|
||||
To use this library, then add it as a dependency in the `Package.swift` file of your project:
|
||||
|
||||
```swift
|
||||
let package = Package(
|
||||
// name, platforms, products, etc.
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/rock-n-code/discogs-service", from: "0.3.0"),
|
||||
// other dependencies
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "SomeTarget",
|
||||
dependencies: [
|
||||
.product(name: "DiscogsService", package: "discogs-service"),
|
||||
]
|
||||
)
|
||||
// other targets
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
It is also possible to use this library with your app in Xcode, then add it as a dependency in your Xcode project.
|
||||
|
||||
> important: Swift 5.10 or higher is required in order to compile this library.
|
||||
|
||||
## Documentation
|
||||
|
||||
Please refer to the [online documentation](https://rock-n-code.github.io/discogs-service/documentation/discogsservice/) for further informations about this library.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# ``APIProtocol``
|
||||
|
||||
## Topics
|
||||
|
||||
### Service endpoints
|
||||
|
||||
- ``APIProtocol/getService(_:)``
|
||||
- ``APIProtocol/getService(headers:)``
|
||||
|
||||
### Authentication endpoints
|
||||
|
||||
- ``APIProtocol/getRequestToken(_:)``
|
||||
- ``APIProtocol/getRequestToken(headers:)``
|
||||
- ``APIProtocol/postAccessToken(_:)``
|
||||
- ``APIProtocol/postAccessToken(headers:)``
|
||||
- ``APIProtocol/getUserIdentity(_:)``
|
||||
- ``APIProtocol/getUserIdentity(headers:)``
|
||||
|
||||
### Database endpoints
|
||||
|
||||
- ``APIProtocol/searchDatabase(_:)``
|
||||
- ``APIProtocol/searchDatabase(query:headers:)``
|
||||
- ``APIProtocol/getArtist(_:)``
|
||||
- ``APIProtocol/getArtist(path:headers:)``
|
||||
- ``APIProtocol/getArtistReleases(_:)``
|
||||
- ``APIProtocol/getArtistReleases(path:query:headers:)``
|
||||
- ``APIProtocol/getLabel(_:)``
|
||||
- ``APIProtocol/getLabel(path:headers:)``
|
||||
- ``APIProtocol/getLabelReleases(_:)``
|
||||
- ``APIProtocol/getLabelReleases(path:query:headers:)``
|
||||
- ``APIProtocol/getMaster(_:)``
|
||||
- ``APIProtocol/getMaster(path:headers:)``
|
||||
- ``APIProtocol/getMasterVersions(_:)``
|
||||
- ``APIProtocol/getMasterVersions(path:query:headers:)``
|
||||
- ``APIProtocol/getRelease(_:)``
|
||||
- ``APIProtocol/getRelease(path:query:headers:)``
|
||||
- ``APIProtocol/getReleaseRating(_:)``
|
||||
- ``APIProtocol/getReleaseRating(path:headers:)``
|
||||
- ``APIProtocol/getReleaseRatingByUser(_:)``
|
||||
- ``APIProtocol/getReleaseRatingByUser(path:headers:)``
|
||||
- ``APIProtocol/putReleaseRatingByUser(_:)``
|
||||
- ``APIProtocol/putReleaseRatingByUser(path:query:headers:)``
|
||||
- ``APIProtocol/deleteReleaseRatingByUser(_:)``
|
||||
- ``APIProtocol/deleteReleaseRatingByUser(path:headers:)``
|
||||
- ``APIProtocol/getReleaseStats(_:)``
|
||||
- ``APIProtocol/getReleaseStats(path:headers:)``
|
||||
@@ -0,0 +1,33 @@
|
||||
# ``Client``
|
||||
|
||||
## Topics
|
||||
|
||||
### Initializers
|
||||
|
||||
- ``Client/init(serverURL:configuration:transport:middlewares:)``
|
||||
|
||||
### Service endpoints
|
||||
|
||||
- ``Client/getService(_:)``
|
||||
|
||||
### Authentication endpoints
|
||||
|
||||
- ``Client/getRequestToken(_:)``
|
||||
- ``Client/postAccessToken(_:)``
|
||||
- ``Client/getUserIdentity(_:)``
|
||||
|
||||
### Database endpoints
|
||||
|
||||
- ``Client/searchDatabase(_:)``
|
||||
- ``Client/getArtist(_:)``
|
||||
- ``Client/getArtistReleases(_:)``
|
||||
- ``Client/getLabel(_:)``
|
||||
- ``Client/getLabelReleases(_:)``
|
||||
- ``Client/getMaster(_:)``
|
||||
- ``Client/getMasterVersions(_:)``
|
||||
- ``Client/getRelease(_:)``
|
||||
- ``Client/getReleaseRating(_:)``
|
||||
- ``Client/getReleaseRatingByUser(_:)``
|
||||
- ``Client/putReleaseRatingByUser(_:)``
|
||||
- ``Client/deleteReleaseRatingByUser(_:)``
|
||||
- ``Client/getReleaseStats(_:)``
|
||||
@@ -8,6 +8,35 @@
|
||||
|
||||
## Topics
|
||||
|
||||
### <!--@START_MENU_TOKEN@-->Group<!--@END_MENU_TOKEN@-->
|
||||
### Clients
|
||||
|
||||
- <!--@START_MENU_TOKEN@-->``Symbol``<!--@END_MENU_TOKEN@-->
|
||||
- ``Client``
|
||||
|
||||
### Servers
|
||||
|
||||
- ``LiveService``
|
||||
|
||||
### Authentication
|
||||
|
||||
- ``AuthMiddleware``
|
||||
- ``AuthMethod``
|
||||
- ``AuthTransport``
|
||||
|
||||
### User Agent
|
||||
|
||||
- ``UserAgentMiddleware``
|
||||
- ``Product``
|
||||
|
||||
### Types
|
||||
|
||||
- ``Components``
|
||||
- ``Operations``
|
||||
- ``Servers``
|
||||
|
||||
### Errors
|
||||
|
||||
- ``InputValidationError``
|
||||
|
||||
### Protocols
|
||||
|
||||
- ``APIProtocol``
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
extension String {
|
||||
/// An empty string.
|
||||
static let empty = ""
|
||||
|
||||
/// A namespaces assigned for the names of parameters.
|
||||
enum Parameter {
|
||||
/// A name for the consumer key.
|
||||
static let key = "key"
|
||||
/// A name for the consumer secret.
|
||||
static let secret = "secret"
|
||||
/// A name for the user token.
|
||||
static let token = "token"
|
||||
}
|
||||
/// A namespaces assigned for the formats of string values.
|
||||
enum Format {}
|
||||
|
||||
/// A namespaces assigned for the formats of regular expression patterns.
|
||||
enum Pattern {}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Checks whether a regular expression pattern fully matches a string or not.
|
||||
/// - Parameter pattern: A regular expression pattern to match a string against.
|
||||
/// - Returns: A flag that indicates whether a given pattern fully matches a string or not.
|
||||
func fullyMatch(pattern: String) -> Bool {
|
||||
do {
|
||||
if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 6.0, *) {
|
||||
let securityInput = try Regex(pattern)
|
||||
let matches = self.wholeMatch(of: securityInput)
|
||||
|
||||
return matches != nil
|
||||
} else {
|
||||
let securityInput = try NSRegularExpression(pattern: pattern)
|
||||
let matches = securityInput.matches(
|
||||
in: self,
|
||||
range: .init(location: 0, length: count)
|
||||
)
|
||||
|
||||
return !matches.isEmpty
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A protocol that defines an input validation rule to be applied to an input by the ``ValidateInputUseCase`` use case.
|
||||
protocol InputValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
#if swift(>=6.0)
|
||||
/// Validates a given input against a validation rule.
|
||||
/// - Parameter input: An input to be validated.
|
||||
/// - Returns: A flag that indicates whether an input has been validated or not.
|
||||
/// - Throws: An error of type ``InputValidationError`` in case a given input failed a validation.
|
||||
@discardableResult func validate(_ input: String?) throws(InputValidationError) -> Bool
|
||||
#else
|
||||
/// Validates a given input against a validation rule.
|
||||
/// - Parameter input: An input to be validated.
|
||||
/// - Returns: A flag that indicates whether an input has been validated or not.
|
||||
/// - Throws: An error of type ``InputValidationError`` in case a given input failed a validation.
|
||||
@discardableResult func validate(_ input: String?) throws -> Bool
|
||||
#endif
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A use case that validates an input against a set of validation rules.
|
||||
struct ValidateInputUseCase {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A list of validation rules to match an input against.
|
||||
private let rules: [any InputValidationRule]
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes this use case.
|
||||
/// - Parameter rules: A list of validation rules to match an input against.
|
||||
init(rules: any InputValidationRule...) {
|
||||
self.rules = rules
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
#if swift(>=6.0)
|
||||
/// Validates an input against a set of validation rules.
|
||||
/// - Parameter input: An input to be validated against a set of rules, if any.
|
||||
/// - Throws: An error of type ``InputValidationError`` in case an input failed any validation.
|
||||
func callAsFunction(_ input: String?) throws(InputValidationError) {
|
||||
for rule in rules {
|
||||
try rule.validate(input)
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// Validates an input against a set of validation rules.
|
||||
/// - Parameter input: An input to be validated against a set of rules, if any.
|
||||
/// - Throws: An error of type ``InputValidationError`` in case an input failed any validation.
|
||||
func callAsFunction(_ input: String?) throws {
|
||||
for rule in rules {
|
||||
try rule.validate(input)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A validation rule type that checks whether an input is camel-case or not.
|
||||
struct CamelCaseValidationRule: InputValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
#if swift(>=6.0)
|
||||
func validate(_ input: String?) throws(InputValidationError) -> Bool {
|
||||
try validate(input: input)
|
||||
}
|
||||
#else
|
||||
func validate(_ input: String?) throws -> Bool {
|
||||
try validate(input: input)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Definitions
|
||||
|
||||
extension InputValidationRule where Self == CamelCaseValidationRule {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// A validation rule that checks whether an input is camel-cased or not.
|
||||
static var camelCase: Self { .init() }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension CamelCaseValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Validates a given input.
|
||||
///
|
||||
/// > note: This helper function would not be necessary when support for *Swift 5.10* is discontinued.
|
||||
///
|
||||
/// - Parameter input: An input to be validated.
|
||||
/// - Returns: A flag that indicates whether a given input has been validated or not.
|
||||
/// - Throws: An error of type ``InputValidatorError`` in case the validation failed.
|
||||
func validate(input: String?) throws -> Bool {
|
||||
guard let input else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard input.fullyMatch(
|
||||
pattern: .init(format: .Pattern.camelCase)
|
||||
) else {
|
||||
throw InputValidationError.inputNotCamelCase
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension String.Pattern {
|
||||
/// A regular expression pattern that represents camel-cased inputs.
|
||||
static let camelCase = "([A-Z]([a-z]|[0-9])+)+"
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A validation rule type that checks whether an input is empty or not.
|
||||
struct NotEmptyValidationRule: InputValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
#if swift(>=6.0)
|
||||
func validate(_ input: String?) throws(InputValidationError) -> Bool {
|
||||
try validate(input: input)
|
||||
}
|
||||
#else
|
||||
func validate(_ input: String?) throws -> Bool {
|
||||
try validate(input: input)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Definitions
|
||||
|
||||
extension InputValidationRule where Self == NotEmptyValidationRule {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// A validation rule that checks whether an input is empty or not.
|
||||
static var notEmpty: Self { .init() }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension NotEmptyValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Validates a given input.
|
||||
///
|
||||
/// > note: This helper function would not be necessary when support for *Swift 5.10* is discontinued.
|
||||
///
|
||||
/// - Parameter input: An input to be validated.
|
||||
/// - Returns: A flag that indicates whether a given input has been validated or not.
|
||||
/// - Throws: An error of type ``InputValidatorError`` in case the validation failed.
|
||||
func validate(input: String?) throws -> Bool {
|
||||
guard let input else {
|
||||
return false
|
||||
}
|
||||
guard !input.isEmpty else {
|
||||
throw InputValidationError.inputIsEmpty
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A validation rule type that checks whether an input is nil or not.
|
||||
struct NotNilValidationRule: InputValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
#if swift(>=6.0)
|
||||
func validate(_ input: String?) throws(InputValidationError) -> Bool {
|
||||
try validate(input: input)
|
||||
}
|
||||
#else
|
||||
func validate(_ input: String?) throws -> Bool {
|
||||
try validate(input: input)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Definitions
|
||||
|
||||
extension InputValidationRule where Self == NotNilValidationRule {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// A validation rule that checks whether an input is nil or not.
|
||||
static var notNil: Self { .init() }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension NotNilValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Validates a given input.
|
||||
///
|
||||
/// > note: This helper function would not be necessary when support for *Swift 5.10* is discontinued.
|
||||
///
|
||||
/// - Parameter input: An input to be validated.
|
||||
/// - Returns: A flag that indicates whether a given input has been validated or not.
|
||||
/// - Throws: An error of type ``InputValidatorError`` in case the validation failed.
|
||||
func validate(input: String?) throws -> Bool {
|
||||
guard input != nil else {
|
||||
throw InputValidationError.inputIsNil
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A validation rule type that checks whether an input is secure or not.
|
||||
struct SecureValidationRule: InputValidationRule {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A representation of the available security input types.
|
||||
private let inputType: SecurityInput
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes this validation rule.
|
||||
/// - Parameter inputType: A representation of the available security input types.
|
||||
init(inputType: SecurityInput) {
|
||||
self.inputType = inputType
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
#if swift(>=6.0)
|
||||
func validate(_ input: String?) throws(InputValidationError) -> Bool {
|
||||
try validate(input: input)
|
||||
}
|
||||
#else
|
||||
func validate(_ input: String?) throws -> Bool {
|
||||
try validate(input: input)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Definitions
|
||||
|
||||
extension InputValidationRule where Self == SecureValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// A validation rule that checks whether an input is secure or not.
|
||||
/// - Parameter securityInput: A representation of the security input type to validate
|
||||
/// - Returns: A validation rule that has been configured and it is ready to use.
|
||||
static func secure(_ securityInput: SecurityInput) -> Self {
|
||||
.init(inputType: securityInput)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Enumerations
|
||||
|
||||
/// A representation of all the possible security input types, based on their respective character length expectations.
|
||||
enum SecurityInput: Int {
|
||||
/// A consumer key is 20 characters long.
|
||||
case consumerKey = 20
|
||||
/// A consumer key is 32 characters long.
|
||||
case consumerSecret = 32
|
||||
/// A consumer key is 40 characters long.
|
||||
case userToken = 40
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension SecureValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Validates a given input.
|
||||
///
|
||||
/// > note: This helper function would not be necessary when support for *Swift 5.10* is discontinued.
|
||||
///
|
||||
/// - Parameter input: An input to be validated.
|
||||
/// - Returns: A flag that indicates whether a given input has been validated or not.
|
||||
/// - Throws: An error of type ``InputValidatorError`` in case the validation failed.
|
||||
func validate(input: String?) throws -> Bool {
|
||||
guard let input else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard input.fullyMatch(
|
||||
pattern: .init(format: .Pattern.securityInput, inputType.rawValue)
|
||||
) else {
|
||||
switch inputType {
|
||||
case .consumerKey: throw InputValidationError.inputNotConsumerKey
|
||||
case .consumerSecret: throw InputValidationError.inputNotConsumerSecret
|
||||
case .userToken: throw InputValidationError.inputNotUserToken
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension String.Pattern {
|
||||
/// A regular expression pattern that represents security inputs.
|
||||
static let securityInput = "^([a-z]|[A-Z]){%d}$"
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A validation rule type that checks whether an input is a semantic version or not.
|
||||
///
|
||||
/// This validation rules follows the principles defined in the [Semantic Versioning 2.0.0 documentation](https://semver.org/spec/v2.0.0.html)
|
||||
struct SemanticVersionValidationRule: InputValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
#if swift(>=6.0)
|
||||
func validate(_ input: String?) throws(InputValidationError) -> Bool {
|
||||
try validate(input: input)
|
||||
}
|
||||
#else
|
||||
func validate(_ input: String?) throws -> Bool {
|
||||
try validate(input: input)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Definitions
|
||||
|
||||
extension InputValidationRule where Self == SemanticVersionValidationRule {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// A validation rule that checks whether an input is semantic version or not.
|
||||
static var semanticVersion: Self { .init() }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension SemanticVersionValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Validates a given input.
|
||||
///
|
||||
/// > note: This helper function would not be necessary when support for *Swift 5.10* is discontinued.
|
||||
///
|
||||
/// - Parameter input: An input to be validated.
|
||||
/// - Returns: A flag that indicates whether a given input has been validated or not.
|
||||
/// - Throws: An error of type ``InputValidatorError`` in case the validation failed.
|
||||
func validate(input: String?) throws -> Bool {
|
||||
guard let input else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard input.fullyMatch(
|
||||
pattern: .init(format: .Pattern.semanticVersioning)
|
||||
) else {
|
||||
throw InputValidationError.inputNotSemanticVersion
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension String.Pattern {
|
||||
/// A regular expression pattern that represents semantic version inputs.
|
||||
///
|
||||
/// This regular expression is based on the [suggested regular expression](https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string) of the *Semantic Versioning 2.0.0* documentation.
|
||||
static let semanticVersioning = "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A validation rule type that checks whether an input is a URL or not.
|
||||
///
|
||||
/// This validation rule doesn't necessarily follow the [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) standard.
|
||||
/// Thus it doesn't implement a complex regular expression pattern such as [this one](https://rgxdb.com/r/5JXUI5A2).
|
||||
/// Instead this validation implements a regular expression sufficient enough to satisfy the requirements for a [user agent definition](https://www.discogs.com/developers/#page:home,header:home-general-information).
|
||||
struct URLValidationRule: InputValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
#if swift(>=6.0)
|
||||
func validate(_ input: String?) throws(InputValidationError) -> Bool {
|
||||
try validate(input: input)
|
||||
}
|
||||
#else
|
||||
func validate(_ input: String?) throws -> Bool {
|
||||
try validate(input: input)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Definitions
|
||||
|
||||
extension InputValidationRule where Self == URLValidationRule {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// A validation rule that checks whether an input is a URL or not.
|
||||
static var url: Self { .init() }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension URLValidationRule {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Validates a given input.
|
||||
///
|
||||
/// > note: This helper function would not be necessary when support for *Swift 5.10* is discontinued.
|
||||
///
|
||||
/// - Parameter input: An input to be validated.
|
||||
/// - Returns: A flag that indicates whether a given input has been validated or not.
|
||||
/// - Throws: An error of type ``InputValidatorError`` in case the validation failed.
|
||||
func validate(input: String?) throws -> Bool {
|
||||
guard let input else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard input.fullyMatch(
|
||||
pattern: .init(format: .Pattern.url)
|
||||
) else {
|
||||
throw InputValidationError.inputNotURL
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension String.Pattern {
|
||||
/// A regular expression pattern that represents URL inputs.
|
||||
///
|
||||
/// This regular expression is based on [this regular expression](https://regex101.com/r/3fYy3x/1) found while researching the topic.
|
||||
static let url = "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A reference to a live (or production) service defined in the OpenAPI document.
|
||||
public typealias LiveService = Servers.Server1
|
||||
@@ -0,0 +1,33 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A representation of the available authentication methods at the Discogs service.
|
||||
///
|
||||
/// The differences between these authentication methods.
|
||||
///
|
||||
/// Credentials in request | Rate limiting? | Image URLs? |Authenticated as user?
|
||||
/// --- | :---: | :---: | :---:
|
||||
/// None | 🐢 Low tier | ❌ No |❌ No
|
||||
/// Only Consumer key/secret | 🐰 High tier | ✔️ Yes | ❌ No
|
||||
/// Personal access token | 🐰 High tier | ✔️ Yes | ✔️ Yes, for token holder only 👩
|
||||
///
|
||||
/// Please refer to the [Discogs documentation](https://www.discogs.com/developers#page:authentication,header:authentication-discogs-auth-flow) for further details.
|
||||
public enum AuthMethod: Equatable, Sendable {
|
||||
/// A consumer key and secret that allows access to endpoints that requires authentication.
|
||||
case consumer(key: String, secret: String)
|
||||
/// No authentication method defined.
|
||||
case none
|
||||
/// A user token that allows access to its own account information.
|
||||
case user(token: String)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A representation of the available transport options to send credentials in authenticated requests.
|
||||
public enum AuthTransport: CaseIterable, Sendable {
|
||||
/// Authentication credential are sent in a request as an `Authentication` header.
|
||||
///
|
||||
/// This means that the header will be added to any existing header in a request, like this:
|
||||
/// ```bash
|
||||
/// curl "https://api.discogs.com/database/search?q=Slayer" -H "Authorization: Discogs key=foo123, secret=bar456"
|
||||
/// curl "https://api.discogs.com/database/search?q=Slayer" -H "Authorization: Discogs token=abcxyz123456"
|
||||
/// ```
|
||||
case onHeader
|
||||
/// Authentication credential are sent in a request as parameters in the query string.
|
||||
///
|
||||
/// This means that the parameters will be injected into the query in a request, like this:
|
||||
/// ```bash
|
||||
/// curl "https://api.discogs.com/database/search?q=Slayer&key=foo123&secret=bar456"
|
||||
/// curl "https://api.discogs.com/database/search?q=Slayer&token=abcxyz123456"
|
||||
/// ```
|
||||
case onQuery
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A representation of all the possible validation error that could be thrown while validating an input.
|
||||
public enum InputValidationError: Error {
|
||||
/// An input is empty.
|
||||
case inputIsEmpty
|
||||
/// An input is nil.
|
||||
case inputIsNil
|
||||
/// An input is not camel-case.
|
||||
case inputNotCamelCase
|
||||
/// An input does not comply with the consumer key requirements.
|
||||
case inputNotConsumerKey
|
||||
/// An input does not comply with the consumer secret requirements.
|
||||
case inputNotConsumerSecret
|
||||
/// An input is not a semantic version.
|
||||
case inputNotSemanticVersion
|
||||
/// An input is not a URL.
|
||||
case inputNotURL
|
||||
/// An input does not comply with the user token requirements.
|
||||
case inputNotUserToken
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import class OpenAPIRuntime.HTTPBody
|
||||
|
||||
import protocol OpenAPIRuntime.ClientMiddleware
|
||||
|
||||
import struct Foundation.URL
|
||||
import struct Foundation.URLComponents
|
||||
import struct Foundation.URLQueryItem
|
||||
import struct HTTPTypes.HTTPField
|
||||
import struct HTTPTypes.HTTPFields
|
||||
import struct HTTPTypes.HTTPRequest
|
||||
import struct HTTPTypes.HTTPResponse
|
||||
|
||||
/// A middleware that attaches any defined authentication credentials into the requests to the service.
|
||||
///
|
||||
/// Please refer to the [Discogs documentation](https://www.discogs.com/developers#page:authentication) for further information.
|
||||
public struct AuthMiddleware {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A header field that contains the authentication information.
|
||||
let authField: HTTPField?
|
||||
|
||||
/// A list of query items that contains the authentication information.
|
||||
let authItems: [URLQueryItem]?
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes this middleware.
|
||||
/// - Parameters:
|
||||
/// - method: A representation of an authentication method to use to authenticate requests.
|
||||
/// - transport: A representation of a transport option to send credentials in requests.
|
||||
/// - Throws: An error of type ``InputValidationError`` in case an input failed any validation.
|
||||
public init(
|
||||
method: AuthMethod = .none,
|
||||
transport: AuthTransport
|
||||
) throws {
|
||||
switch method {
|
||||
case let .consumer(key, secret):
|
||||
let validateKey = ValidateInputUseCase(rules: .notNil, .notEmpty, .secure(.consumerKey))
|
||||
let validateSecret = ValidateInputUseCase(rules: .notNil, .notEmpty, .secure(.consumerSecret))
|
||||
|
||||
try validateKey(key)
|
||||
try validateSecret(secret)
|
||||
|
||||
self.authField = switch transport {
|
||||
case .onQuery: nil
|
||||
case .onHeader: .init(
|
||||
name: .authorization,
|
||||
value: .init(format: .Format.authConsumer, key, secret)
|
||||
)}
|
||||
|
||||
self.authItems = switch transport {
|
||||
case .onHeader: nil
|
||||
case .onQuery: [
|
||||
.init(name: .Parameter.key, value: key),
|
||||
.init(name: .Parameter.secret, value: secret)
|
||||
]}
|
||||
|
||||
case let .user(token):
|
||||
let validateToken = ValidateInputUseCase(rules: .notNil, .notEmpty, .secure(.userToken))
|
||||
|
||||
try validateToken(token)
|
||||
|
||||
self.authField = switch transport {
|
||||
case .onQuery: nil
|
||||
case .onHeader: .init(
|
||||
name: .authorization,
|
||||
value: .init(format: .Format.authUser, token)
|
||||
)}
|
||||
|
||||
self.authItems = switch transport {
|
||||
case .onHeader: nil
|
||||
case .onQuery: [
|
||||
.init(name: .Parameter.token, value: token)
|
||||
]
|
||||
}
|
||||
|
||||
case .none:
|
||||
self.authField = nil
|
||||
self.authItems = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// A flag that indicates whether the middleware should authenticate the intercepted request or not.
|
||||
var shouldAuthenticate: Bool {
|
||||
authField != nil || authItems != nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ClientMiddleware
|
||||
|
||||
extension AuthMiddleware: ClientMiddleware {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
public func intercept(
|
||||
_ request: HTTPRequest,
|
||||
body: HTTPBody?,
|
||||
baseURL: URL,
|
||||
operationID: String,
|
||||
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
|
||||
) async throws -> (HTTPResponse, HTTPBody?) {
|
||||
guard shouldAuthenticate else {
|
||||
return try await next(request, body, baseURL)
|
||||
}
|
||||
|
||||
return try await next(
|
||||
.init(
|
||||
method: request.method,
|
||||
scheme: request.scheme,
|
||||
authority: request.authority,
|
||||
path: authenticatePath(request.path),
|
||||
headerFields: authenticateHeader(request.headerFields)
|
||||
),
|
||||
body,
|
||||
baseURL
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension AuthMiddleware {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Adds an authorization header to the existing header fields.
|
||||
/// - Parameter fields: A set of header fields to update.
|
||||
/// - Returns: An updated set of header fields including the authorization header.
|
||||
func authenticateHeader(_ fields: HTTPFields) -> HTTPFields {
|
||||
var fields = fields
|
||||
|
||||
if let authField {
|
||||
fields.append(authField)
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
/// Adds the authentication parameters to the query of a path
|
||||
/// - Parameter path: A request path to authenticate.
|
||||
/// - Returns: An updated request path including the authentication parameters.
|
||||
func authenticatePath(_ path: String?) -> String? {
|
||||
guard
|
||||
let authItems,
|
||||
let path,
|
||||
var urlComponents = URLComponents(string: path)
|
||||
else {
|
||||
return path
|
||||
}
|
||||
|
||||
var queryItems = urlComponents.queryItems ?? []
|
||||
|
||||
queryItems.append(contentsOf: authItems)
|
||||
|
||||
urlComponents.queryItems = queryItems
|
||||
|
||||
return if let urlQuery = urlComponents.query {
|
||||
urlComponents.path + "?" + urlQuery
|
||||
} else {
|
||||
urlComponents.path
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension String.Format {
|
||||
/// A format for the consumer authentication header.
|
||||
static let authConsumer = "Discogs \(String.Parameter.key)=%@, \(String.Parameter.secret)=%@"
|
||||
/// A format for the user authentication header.
|
||||
static let authUser = "Discogs \(String.Parameter.token)=%@"
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import class OpenAPIRuntime.HTTPBody
|
||||
|
||||
import protocol OpenAPIRuntime.ClientMiddleware
|
||||
|
||||
import struct Foundation.URL
|
||||
import struct HTTPTypes.HTTPField
|
||||
import struct HTTPTypes.HTTPFields
|
||||
import struct HTTPTypes.HTTPRequest
|
||||
import struct HTTPTypes.HTTPResponse
|
||||
|
||||
/// A middleware that attaches the user agent header into the requests to the service.
|
||||
///
|
||||
/// Please refer to the [Discogs documentation](https://www.discogs.com/developers/#page:home,header:home-general-information) for further information.
|
||||
public struct UserAgentMiddleware {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A formatted value for the user agent header.
|
||||
let agentField: HTTPField
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes this middleware.
|
||||
/// - Parameter product: A product from which the user agent will be generated from.
|
||||
/// - Throws: An error of type ``InputValidationError`` in case an input failed any validation.
|
||||
public init(product: Product) throws {
|
||||
let agentName = ValidateInputUseCase(rules: .notNil, .notEmpty, .camelCase)
|
||||
let agentVersion = ValidateInputUseCase(rules: .notNil, .notEmpty, .semanticVersion)
|
||||
let agentURL = ValidateInputUseCase(rules: .notNil, .notEmpty, .url)
|
||||
|
||||
try agentName(product.name)
|
||||
try agentVersion(product.version)
|
||||
try agentURL(product.url)
|
||||
|
||||
self.agentField = .init(
|
||||
name: .userAgent,
|
||||
value: .init(format: .Format.userAgent, product.name, product.version, product.url)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ClientMiddleware
|
||||
|
||||
extension UserAgentMiddleware: ClientMiddleware {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
public func intercept(
|
||||
_ request: HTTPRequest,
|
||||
body: HTTPBody?,
|
||||
baseURL: URL,
|
||||
operationID: String,
|
||||
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
|
||||
) async throws -> (HTTPResponse, HTTPBody?) {
|
||||
return try await next(
|
||||
.init(
|
||||
method: request.method,
|
||||
scheme: request.scheme,
|
||||
authority: request.authority,
|
||||
path: request.path,
|
||||
headerFields: userAgentHeader(request.headerFields)
|
||||
),
|
||||
body,
|
||||
baseURL
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension UserAgentMiddleware {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Adds a user agent header to the existing header fields.
|
||||
/// - Parameter fields: A set of header fields to update.
|
||||
/// - Returns: An updated set of header fields including the user agent header.
|
||||
func userAgentHeader(_ fields: HTTPFields) -> HTTPFields {
|
||||
var fields = fields
|
||||
|
||||
fields.append(agentField)
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension String.Format {
|
||||
/// A format for the user agent header.
|
||||
static let userAgent = "%@/%@ +%@"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A type that represents a product that uses the ``Client`` client.
|
||||
public struct Product: Sendable {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A camel-cased name of a product.
|
||||
let name: String
|
||||
|
||||
/// A URI link related to a product.
|
||||
let url: String
|
||||
|
||||
/// A semantic version of a product.
|
||||
let version: String
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes this model.
|
||||
/// - Parameters:
|
||||
/// - name: A camel-cased name of a product.
|
||||
/// - version: A semantic version of a product.
|
||||
/// - url: A URI link related to a product.
|
||||
public init(
|
||||
name: String,
|
||||
version: String,
|
||||
url: String
|
||||
) {
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.version = version
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// The Swift Programming Language
|
||||
// https://docs.swift.org/swift-book
|
||||
@@ -15,5 +15,5 @@
|
||||
generate:
|
||||
- types
|
||||
- client
|
||||
namingStrategy: defensive
|
||||
namingStrategy: idiomatic
|
||||
accessModifier: public
|
||||
|
||||
@@ -227,7 +227,9 @@ servers:
|
||||
description: Live Server
|
||||
tags:
|
||||
- name: Service
|
||||
description: Access data related to the service.
|
||||
description: Access data on the service.
|
||||
- name: Authentication
|
||||
description: Access data on authenticating to the service.
|
||||
- name: Database
|
||||
description: Access data on artists, labels, and releases.
|
||||
- name: Marketplace
|
||||
@@ -253,24 +255,131 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: Service information returned successfully.
|
||||
headers:
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Service'
|
||||
'500':
|
||||
$ref: '#/components/responses/Unavailable'
|
||||
/oauth/request_token:
|
||||
get:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Get details about a OAuth request token.
|
||||
description: |
|
||||
Retrieve a request token to initialize an *OAuth* authentication process.
|
||||
|
||||
This endpoint represents the [2nd step of the OAuth flow process](https://www.discogs.com/developers#header-2.-send-a-get-request-to-the-discogs-request-token-url), thus it requires to configure an *Authorization* header to have a value like this:
|
||||
|
||||
```
|
||||
OAuth oauth_consumer_key="your_consumer_key", oauth_nonce="random_string_or_timestamp", oauth_signature="your_consumer_secret&", oauth_signature_method="PLAINTEXT", oauth_timestamp="current_timestamp", oauth_callback="your_callback"
|
||||
```
|
||||
|
||||
For further details about this process, please refer to the [OAuth flow](https://www.discogs.com/developers#page:authentication,header:authentication-oauth-flow) section in the [Discogs API authentication](https://www.discogs.com/developers#page:authentication) documentation.
|
||||
operationId: getRequestToken
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/ContentType'
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
- $ref: '#/components/parameters/UserAgent'
|
||||
responses:
|
||||
'200':
|
||||
description: |
|
||||
Successfully retrieved request token details.
|
||||
|
||||
With this request token, then it is possible to continue with the [next step in the OAuth flow](https://www.discogs.com/developers#header-3.-redirect-your-user-to-the-discogs-authorize-page) process.
|
||||
headers:
|
||||
oauth_token:
|
||||
$ref: '#/components/headers/OAuthToken'
|
||||
oauth_token_secret:
|
||||
$ref: '#/components/headers/OAuthSecret'
|
||||
oauth_callback-confirmed:
|
||||
$ref: '#/components/headers/OAuthCallback'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
/oauth/access_token:
|
||||
post:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Provide required credentials data to obtain an access token.
|
||||
description: |
|
||||
Provide to the service some required credentials details to obtain an access token at the end of the *OAuth* process.
|
||||
|
||||
This endpoint represents the [4th step of the OAuth flow process](https://www.discogs.com/developers#header-4.-send-a-post-request-to-the-discogs-access-token-url), thus it requires to configure an *Authorization* header to have a value like this:
|
||||
|
||||
```
|
||||
OAuth oauth_consumer_key="your_consumer_key", oauth_nonce="random_string_or_timestamp", oauth_token="oauth_token_received_from_step_2" oauth_signature="your_consumer_secret&", oauth_signature_method="PLAINTEXT", oauth_timestamp="current_timestamp", oauth_verifier="users_verifier"
|
||||
```
|
||||
|
||||
For further details about this process, please refer to the [OAuth flow](https://www.discogs.com/developers#page:authentication,header:authentication-oauth-flow) section in the [Discogs API authentication](https://www.discogs.com/developers#page:authentication) documentation.
|
||||
operationId: postAccessToken
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/ContentType'
|
||||
- $ref: '#/components/parameters/Authorization'
|
||||
- $ref: '#/components/parameters/UserAgent'
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully retrieved an access token at the end of the OAuth authentication process.
|
||||
headers:
|
||||
oauth_token:
|
||||
$ref: '#/components/headers/OAuthToken'
|
||||
oauth_token_secret:
|
||||
$ref: '#/components/headers/OAuthSecret'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
/oauth/identity:
|
||||
get:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Get information about an authenticated user.
|
||||
description: |
|
||||
Retrieve basic information about the authenticated user.
|
||||
|
||||
This endpoint represents the (optional) [5th step of the OAuth flow process](https://www.discogs.com/developers#header-5-send-authenticated-requests-to-discogs-endpoints), as it is advised to perform a sanity check to ensure the *OAuth* process finished successfully.
|
||||
|
||||
For further details about this process, please refer to the [OAuth flow](https://www.discogs.com/developers#page:authentication,header:authentication-oauth-flow) section in the [Discogs API authentication](https://www.discogs.com/developers#page:authentication) documentation.
|
||||
operationId: getUserIdentity
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully retrieved information about an authenticated user.
|
||||
headers:
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserIdentity'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
/artists/{artist_id}:
|
||||
get:
|
||||
summary: Get information about an artist.
|
||||
operationId: getArtist
|
||||
description: Retrieves any available information for a specific artist.
|
||||
tags:
|
||||
- Database
|
||||
summary: Get information about an artist.
|
||||
description: Retrieves any available information for a specific artist.
|
||||
operationId: getArtist
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/ArtistId'
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully retrieved artist details.
|
||||
headers:
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -299,6 +408,12 @@ paths:
|
||||
headers:
|
||||
Link:
|
||||
$ref: '#/components/headers/Link'
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -324,6 +439,13 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully retrieved label details.
|
||||
headers:
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -347,9 +469,15 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: A paginated list of the label's releases.
|
||||
headers:
|
||||
Link:
|
||||
$ref: '#/components/headers/Link'
|
||||
headers:
|
||||
Link:
|
||||
$ref: '#/components/headers/Link'
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -378,6 +506,13 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully retrieved master release details.
|
||||
headers:
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -410,6 +545,12 @@ paths:
|
||||
headers:
|
||||
Link:
|
||||
$ref: '#/components/headers/Link'
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -436,6 +577,13 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully retrieved release details.
|
||||
headers:
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -454,6 +602,13 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully retrieved release rating details.
|
||||
headers:
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -461,6 +616,38 @@ paths:
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
/releases/{release_id}/rating/{username}:
|
||||
delete:
|
||||
tags:
|
||||
- Database
|
||||
summary: Delete information about a rating of release by a user.
|
||||
description: |
|
||||
Updates a rating of a release for a given user.
|
||||
|
||||
This endpoint requires authentication.
|
||||
operationId: deleteReleaseRatingByUser
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/ReleaseId'
|
||||
- $ref: '#/components/parameters/Username'
|
||||
security:
|
||||
- Token: []
|
||||
- KeySecret: []
|
||||
- OAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully deleted a rating for a specific release by a given user.
|
||||
headers:
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
get:
|
||||
tags:
|
||||
- Database
|
||||
@@ -473,12 +660,56 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully retrieved release rating details by a given user.
|
||||
headers:
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReleaseRatingByUser'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
put:
|
||||
tags:
|
||||
- Database
|
||||
summary: Update information about a rating of release by a user.
|
||||
description: |
|
||||
Updates a rating of a release for a given user.
|
||||
|
||||
This endpoint requires authentication.
|
||||
operationId: putReleaseRatingByUser
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/ReleaseId'
|
||||
- $ref: '#/components/parameters/Username'
|
||||
- $ref: '#/components/parameters/Rating'
|
||||
security:
|
||||
- Token: []
|
||||
- KeySecret: []
|
||||
- OAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully updated a rating for a specific release by a given user.
|
||||
headers:
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReleaseRatingByUser'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
/releases/{release_id}/stats:
|
||||
get:
|
||||
tags:
|
||||
@@ -494,6 +725,13 @@ paths:
|
||||
Successfully retrieved release rating details by a given user.
|
||||
|
||||
> warning: There is a discrepancy about this response between was is documented and what the endpoints actually responds. In the [documentation](https://www.discogs.com/developers#page:database,header:database-release-stats), it is defined that a type containing a statistical data would be returned but the actual response returns an object that contains a boolean flag instead.
|
||||
headers:
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -506,7 +744,7 @@ paths:
|
||||
required:
|
||||
- is_offensive
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
$ref: '#/components/responses/NotFound'
|
||||
/database/search:
|
||||
get:
|
||||
tags:
|
||||
@@ -628,6 +866,12 @@ paths:
|
||||
headers:
|
||||
Link:
|
||||
$ref: '#/components/headers/Link'
|
||||
X-Discogs-RateLimit:
|
||||
$ref: '#/components/headers/RateLimit'
|
||||
X-Discogs-RateLimit-Used:
|
||||
$ref: '#/components/headers/RateLimitUsed'
|
||||
X-Discogs-RateLimit-Remaining:
|
||||
$ref: '#/components/headers/RateLimitRemaining'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -658,6 +902,44 @@ components:
|
||||
schema:
|
||||
type: string
|
||||
example: <https://api.discogs.com/artists/1/releases?page=2&per_page=75>; rel="next", <https://api.discogs.com/artists/1/releases?page=30&per_page=75>; rel="last"
|
||||
required: true
|
||||
OAuthCallback:
|
||||
description: An OAuth callback confirmed.
|
||||
schema:
|
||||
type: boolean
|
||||
default: true
|
||||
OAuthSecret:
|
||||
description: An OAuth request token secret.
|
||||
schema:
|
||||
type: string
|
||||
OAuthToken:
|
||||
description: An OAuth request token.
|
||||
schema:
|
||||
type: string
|
||||
RateLimit:
|
||||
description: A total number of requests that can be made in a minute window.
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 25
|
||||
maximum: 60
|
||||
example: 60
|
||||
required: true
|
||||
RateLimitRemaining:
|
||||
description: A number of remaining requests that can be made in an existing rate limit window.
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 60
|
||||
example: 59
|
||||
required: true
|
||||
RateLimitUsed:
|
||||
description: A number of requests that have been made in an existing rate limit window.
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 60
|
||||
example: 1
|
||||
required: true
|
||||
parameters:
|
||||
ArtistId:
|
||||
name: artist_id
|
||||
@@ -678,6 +960,23 @@ components:
|
||||
- year
|
||||
- title
|
||||
- format
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: A string to authenticate a user with a service by carrying credentials.
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
ContentType:
|
||||
name: Content-Type
|
||||
description: A content type for a response.
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- application/json
|
||||
- application/x-www-form-urlencoded
|
||||
Country:
|
||||
description: A filter by country.
|
||||
name: country
|
||||
@@ -751,12 +1050,15 @@ components:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
Released:
|
||||
description: A filter by release year.
|
||||
name: released
|
||||
Rating:
|
||||
description: A number (between 1 and 5) for a rating of a release.
|
||||
name: rating
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 5
|
||||
ReleaseId:
|
||||
description: An identifier of a release.
|
||||
name: release_id
|
||||
@@ -765,6 +1067,12 @@ components:
|
||||
schema:
|
||||
type: integer
|
||||
example: 249504
|
||||
Released:
|
||||
description: A filter by release year.
|
||||
name: released
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
SortOrder:
|
||||
description: The order to sort the results.
|
||||
name: sort_order
|
||||
@@ -775,6 +1083,13 @@ components:
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
UserAgent:
|
||||
name: User-Agent
|
||||
description: A name of a software agent responsible for interacting with the service.
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
Username:
|
||||
description: A username of a user.
|
||||
name: username
|
||||
@@ -1757,6 +2072,15 @@ components:
|
||||
- id
|
||||
- resource_url
|
||||
- username
|
||||
UserIdentity:
|
||||
description: A type that represents a user identity.
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UserId'
|
||||
- type: object
|
||||
properties:
|
||||
consumer_name:
|
||||
description: A name of an application a user utilizes to interacts with the service.
|
||||
type: string
|
||||
Video:
|
||||
description: A type that represents a video.
|
||||
type: object
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Testing
|
||||
|
||||
@testable import DiscogsService
|
||||
|
||||
@Suite("String Functions", .tags(.extension))
|
||||
struct StringFunctionsTests {
|
||||
|
||||
// MARK: Functions tests
|
||||
|
||||
#if swift(>=6.2)
|
||||
@Test(arguments: zip(
|
||||
Input.stringsToMatch,
|
||||
Output.stringsToMatch
|
||||
))
|
||||
func `fully match`(
|
||||
string: String,
|
||||
expects isMatch: Bool
|
||||
) {
|
||||
assertFullyMatch(
|
||||
string: string,
|
||||
pattern: .Pattern.sample,
|
||||
expects: isMatch
|
||||
)
|
||||
}
|
||||
#else
|
||||
@Test("fully match", arguments: zip(
|
||||
Input.stringsToMatch,
|
||||
Output.stringsToMatch
|
||||
))
|
||||
func fullyMatch(
|
||||
string: String,
|
||||
expects isMatch: Bool
|
||||
) {
|
||||
assertFullyMatch(
|
||||
string: string,
|
||||
pattern: .Pattern.sample,
|
||||
expects: isMatch
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Assertions
|
||||
|
||||
private extension StringFunctionsTests {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Asserts the result of the `fullyMatch` function.
|
||||
/// - Parameters:
|
||||
/// - string: A string to match against a pattern.
|
||||
/// - pattern: A regular expression pattern to match a string against.
|
||||
/// - isMatch: An expected flag that indicates whether there is a match or not.
|
||||
func assertFullyMatch(
|
||||
string: String,
|
||||
pattern: String,
|
||||
expects isMatch: Bool
|
||||
) {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
let result = string.fullyMatch(pattern: pattern)
|
||||
|
||||
// THEN
|
||||
#expect(result == isMatch)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension Input {
|
||||
/// A list of strings to match against a regular expression pattern in test cases.
|
||||
static let stringsToMatch: [String] = ["Some Pattern", "Some", "Some Other Pattern", "Pattern", .empty]
|
||||
}
|
||||
|
||||
private extension Output {
|
||||
/// A list of expected results from matching a sample string against a sample regular expression pattern in test cases.
|
||||
static let stringsToMatch: [Bool] = [true, false, false, false, false]
|
||||
}
|
||||
|
||||
private extension String.Pattern {
|
||||
/// A sample regular expression pattern to match against.
|
||||
static let sample = "Some Pattern"
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Testing
|
||||
|
||||
@testable import DiscogsService
|
||||
|
||||
@Suite("Validate Input Use Cases", .tags(.useCase))
|
||||
struct ValidateInputUseCaseTests {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
#if swift(>=6.2)
|
||||
@Test(arguments: zip(
|
||||
Input.inputsAgentName,
|
||||
Output.inputsAgentName
|
||||
)) func `validate camel case`(
|
||||
input: String,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .agentName,
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
Input.inputsNotEmpty,
|
||||
Output.inputsNotEmpty
|
||||
)) func `validates not empty`(
|
||||
input: String,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .notEmpty,
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
Input.inputsNotNil,
|
||||
Output.inputsNotNil
|
||||
)) func `validate not nil`(
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .notNil,
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
Input.inputsSecureConsumerKey,
|
||||
Output.inputsSecureConsumerKey
|
||||
)) func `validate secure (consumer key)`(
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .secure(.consumerKey),
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
Input.inputsSecureConsumerSecret,
|
||||
Output.inputsSecureConsumerSecret
|
||||
)) func `validate secure (consumer secret)`(
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .secure(.consumerSecret),
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
Input.inputsSecureUserToken,
|
||||
Output.inputsSecureUserToken
|
||||
)) func `validate secure (user token)`(
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .secure(.userToken),
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
Input.inputsSemanticVersion,
|
||||
Output.inputsSemanticVersion
|
||||
)) func `validate semantic version`(
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .semanticVersion,
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
Input.inputsURL,
|
||||
Output.inputsURL
|
||||
)) func `validate url`(
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .url,
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
#else
|
||||
@Test("validate camel case", arguments: zip(
|
||||
Input.inputsCamelCase,
|
||||
Output.inputsCamelCase
|
||||
)) func validateCamelCase(
|
||||
input: String,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .camelCase,
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test("validate not empty", arguments: zip(
|
||||
Input.inputsNotEmpty,
|
||||
Output.inputsNotEmpty
|
||||
)) func validateNotEmpty(
|
||||
input: String,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .notEmpty,
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test("validate not nil", arguments: zip(
|
||||
Input.inputsNotNil,
|
||||
Output.inputsNotNil
|
||||
)) func validateNotNil(
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .notNil,
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test("validate secure (consumer key)", arguments: zip(
|
||||
Input.inputsSecureConsumerKey,
|
||||
Output.inputsSecureConsumerKey
|
||||
)) func validateSecureConsumerKey(
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .secure(.consumerKey),
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test("validate secure (consumer secret)", arguments: zip(
|
||||
Input.inputsSecureConsumerSecret,
|
||||
Output.inputsSecureConsumerSecret
|
||||
)) func validateSecureConsumerSecret(
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .secure(.consumerSecret),
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test("validate secure (user token)", arguments: zip(
|
||||
Input.inputsSecureUserToken,
|
||||
Output.inputsSecureUserToken
|
||||
)) func validateSecureUserToken(
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .secure(.userToken),
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test("validate semantic version", arguments: zip(
|
||||
Input.inputsSemanticVersion,
|
||||
Output.inputsSemanticVersion
|
||||
)) func validateSemanticVersion(
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .semanticVersion,
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
|
||||
@Test("validate url", arguments: zip(
|
||||
Input.inputsURL,
|
||||
Output.inputsURL
|
||||
)) func validateURL(
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertValidate(
|
||||
rule: .url,
|
||||
input: input,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Assertions
|
||||
|
||||
private extension ValidateInputUseCaseTests {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Asserts an input validation of a ``ValidateInputUseCase`` use case.
|
||||
/// - Parameters:
|
||||
/// - rule: A validation rule to test.
|
||||
/// - input: An input to validate, if any.
|
||||
/// - error: An expected error, if any.
|
||||
/// - Throws: An error of type ``InputValidationError`` in case of an unexpected test case scenario.
|
||||
func assertValidate(
|
||||
rule: InputValidationRule,
|
||||
input: String?,
|
||||
expects error: InputValidationError?
|
||||
) throws {
|
||||
// GIVEN
|
||||
let validate = ValidateInputUseCase(rules: rule)
|
||||
|
||||
// WHEN
|
||||
// THEN
|
||||
if let error {
|
||||
#expect(throws: error) {
|
||||
try validate(input)
|
||||
}
|
||||
} else {
|
||||
#expect(throws: Never.self) {
|
||||
try validate(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension Input {
|
||||
/// A list of inputs to validate against a camel-case validation rule.
|
||||
static let inputsCamelCase: [String] = ["SampleApp", "Sample4pp", "SampleApp1", "SampleApp🚀", "Sample App", "Sample-App", "Sample_App"]
|
||||
/// A list of inputs to validate against the not empty validation rule.
|
||||
static let inputsNotEmpty: [String] = ["Something", .empty]
|
||||
/// A list of inputs to validate against the not nil validation rule.
|
||||
static let inputsNotNil: [String?] = [.empty, nil]
|
||||
/// A list of inputs to validate against the secure (consumer key) validation rule.
|
||||
static let inputsSecureConsumerKey: [String] = ["aAbBcCdDeEfFgGhHiIjJ", "aAbBcCdDeEfFgGhH", "aAbBcCdDeEfFgGhHiIjJkK", "a4bBcCdDe3fFg6hH1Ij7"]
|
||||
/// A list of inputs to validate against the secure (consumer secret) validation rule.
|
||||
static let inputsSecureConsumerSecret: [String] = ["aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP", "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoO", "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQ", "a4bBcCdDe3fFg6hH1IjJkK1LmMnNo0p9"]
|
||||
/// A list of inputs to validate against the secure (user token) validation rule.
|
||||
static let inputsSecureUserToken: [String] = ["aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStT", "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsS", "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuU", "a4bBcCdDe3fFg6hH1IjJkK1LmMnNo0p9qQrRs5t7"]
|
||||
/// A list of inputs to validate against the semantic version validation rule.
|
||||
static let inputsSemanticVersion: [String] = ["0.0.4","1.2.3","10.20.30","1.1.2-prerelease+meta","1.1.2+meta","1.1.2+meta-valid","1.0.0-alpha","1.0.0-beta","1.0.0-alpha.beta","1.0.0-alpha.beta.1","1.0.0-alpha.1","1.0.0-alpha0.valid","1.0.0-alpha.0valid","1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay","1.0.0-rc.1+build.1","2.0.0-rc.1+build.123","1.2.3-beta","10.2.3-DEV-SNAPSHOT","1.2.3-SNAPSHOT-123","1.0.0","2.0.0","1.1.7","2.0.0+build.1848","2.0.1-alpha.1227","1.0.0-alpha+beta","1.2.3----RC-SNAPSHOT.12.9.1--.12+788","1.2.3----R-S.12.9.1--.12+meta","1.2.3----RC-SNAPSHOT.12.9.1--.12","1.0.0+0.build.1-rc.10000aaa-kk-0.1","99999999999999999999999.999999999999999999.99999999999999999","1.0.0-0A.is.legal","1","1.2","1.2.3-0123","1.2.3-0123.0123","1.1.2+.123","+invalid","-invalid","-invalid+invalid","-invalid.01","alpha","alpha.beta","alpha.beta.1","alpha.1","alpha+beta","alpha_beta","alpha.","alpha..","beta","1.0.0-alpha_beta","-alpha.","1.0.0-alpha..","1.0.0-alpha..1","1.0.0-alpha...1","1.0.0-alpha....1","1.0.0-alpha.....1","1.0.0-alpha......1","1.0.0-alpha.......1","01.1.1","1.01.1","1.1.01","1.2","1.2.3.DEV","1.2-SNAPSHOT","1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788","1.2-RC-SNAPSHOT","-1.0.3-gamma+b7718","+justmeta","9.8.7+meta+meta","9.8.7-whatever+meta+meta","99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12"]
|
||||
/// A list of inputs to validate against the URL validation rule.
|
||||
static let inputsURL: [String] = ["https://www.google.com", "http://www.google.com", "https://google.com/q=search", "http://google.com/q=search", "3333-768-0948", "1133.168.0248", "7678*999-8978", "httpq://google.com/q=search", "www.google.com", "www.google.com/?search=qppoao", "www . google.com/?search=qppoao", "https : //google.com/q=search", "htt://www.google.com", "://www.google.com", .empty]
|
||||
}
|
||||
|
||||
private extension Output {
|
||||
/// A list of expected input validation errors to be thrown after validating inputs against the camel-case validation rule.
|
||||
static let inputsCamelCase: [InputValidationError?] = [nil, nil, nil, .inputNotCamelCase, .inputNotCamelCase, .inputNotCamelCase, .inputNotCamelCase]
|
||||
/// A list of expected input validation errors to be thrown after validating inputs against the not empty validation rule.
|
||||
static let inputsNotEmpty: [InputValidationError?] = [nil, .inputIsEmpty]
|
||||
/// A list of expected input validation errors to be thrown after validating inputs against the not nil validation rule.
|
||||
static let inputsNotNil: [InputValidationError?] = [nil, .inputIsNil]
|
||||
/// A list of expected input validation errors to be thrown after validating inputs against the secure (consumer key) validation rule.
|
||||
static let inputsSecureConsumerKey: [InputValidationError?] = [nil, .inputNotConsumerKey, .inputNotConsumerKey, .inputNotConsumerKey]
|
||||
/// A list of expected input validation errors to be thrown after validating inputs against the secure (consumer secret) validation rule.
|
||||
static let inputsSecureConsumerSecret: [InputValidationError?] = [nil, .inputNotConsumerSecret, .inputNotConsumerSecret, .inputNotConsumerSecret]
|
||||
/// A list of expected input validation errors to be thrown after validating inputs against the secure (user token) validation rule.
|
||||
static let inputsSecureUserToken: [InputValidationError?] = [nil, .inputNotUserToken, .inputNotUserToken, .inputNotUserToken]
|
||||
/// A list of expected input validation errors to be thrown after validating inputs against the semantic version validation rule.
|
||||
static let inputsSemanticVersion: [InputValidationError?] = [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion]
|
||||
/// A list of expected input validation errors to be thrown after validating inputs against the URL validation rule.
|
||||
static let inputsURL: [InputValidationError?] = [nil, nil, nil, nil, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL]
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import struct Foundation.URL
|
||||
import struct Foundation.URLComponents
|
||||
import struct Foundation.URLQueryItem
|
||||
import struct HTTPTypes.HTTPFields
|
||||
import struct HTTPTypes.HTTPRequest
|
||||
import struct HTTPTypes.HTTPResponse
|
||||
|
||||
import Testing
|
||||
|
||||
@testable import DiscogsService
|
||||
|
||||
@Suite("Auth Middleware", .tags(.middleware))
|
||||
struct AuthMiddlewareTests {
|
||||
|
||||
// MARK: Initializers tests
|
||||
|
||||
#if swift(>=6.2)
|
||||
@Test(arguments: Input.authMethods)
|
||||
func `initialize`(
|
||||
_ authMethod: AuthMethod
|
||||
) async throws {
|
||||
try assertInit(
|
||||
authMethod: authMethod,
|
||||
authTransport: try randomTransport
|
||||
)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
Input.authMethodsThrows,
|
||||
Output.authMethodsThrows
|
||||
))
|
||||
func `initialize throws`(
|
||||
_ authMethod: AuthMethod,
|
||||
expects error: InputValidationError?
|
||||
) async throws {
|
||||
try assertInitThrows(
|
||||
authMethod: authMethod,
|
||||
authTransport: try randomTransport,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
#else
|
||||
@Test("initialize", arguments: Input.authMethods)
|
||||
func initialize(
|
||||
_ authMethod: AuthMethod
|
||||
) throws {
|
||||
try assertInit(
|
||||
authMethod: authMethod,
|
||||
authTransport: try randomTransport
|
||||
)
|
||||
}
|
||||
|
||||
@Test("initialize throws", arguments: zip(
|
||||
Input.authMethodsThrows,
|
||||
Output.authMethodsThrows
|
||||
))
|
||||
func initializeThrows(
|
||||
_ authMethod: AuthMethod,
|
||||
expects error: InputValidationError?
|
||||
) throws {
|
||||
assertInitThrows(
|
||||
authMethod: authMethod,
|
||||
authTransport: try randomTransport,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: Properties tests
|
||||
#if swift(>=6.2)
|
||||
@Test(arguments: zip(
|
||||
Input.authMethods,
|
||||
Output.authMethodsShouldAuthenticate
|
||||
))
|
||||
func `should authenticate`(
|
||||
_ authMethod: AuthMethod,
|
||||
expects flag: Bool
|
||||
) throws {
|
||||
try assertShouldAuthenticate(
|
||||
authMethod: authMethod,
|
||||
authTransport: try randomTransport,
|
||||
expects: flag
|
||||
)
|
||||
}
|
||||
#else
|
||||
@Test("should authenticate", arguments: zip(
|
||||
Input.authMethods,
|
||||
Output.authMethodsShouldAuthenticate
|
||||
))
|
||||
func shouldAuthenticate(
|
||||
_ authMethod: AuthMethod,
|
||||
expects flag: Bool
|
||||
) throws {
|
||||
try assertShouldAuthenticate(
|
||||
authMethod: authMethod,
|
||||
authTransport: try randomTransport,
|
||||
expects: flag
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: Functions tests
|
||||
|
||||
#if swift(>=6.2)
|
||||
@Test(arguments: Input.authMethods)
|
||||
func `intercept with authorization on header`(
|
||||
_ authMethod: AuthMethod
|
||||
) async throws {
|
||||
try await assertIntercept(
|
||||
authMethod: authMethod,
|
||||
authTransport: .onHeader,
|
||||
path: "/some/path/to/resource"
|
||||
)
|
||||
}
|
||||
|
||||
@Test(arguments: Input.authMethods)
|
||||
func `intercept with authorization on query`(
|
||||
_ authMethod: AuthMethod
|
||||
) async throws {
|
||||
try await assertIntercept(
|
||||
authMethod: authMethod,
|
||||
authTransport: .onQuery,
|
||||
path: "/some/path/to/resource"
|
||||
)
|
||||
}
|
||||
|
||||
@Test(arguments: Input.authMethods)
|
||||
func `intercept with authorization on header when headers populated`(
|
||||
_ authMethod: AuthMethod
|
||||
) async throws {
|
||||
try await assertIntercept(
|
||||
authMethod: authMethod,
|
||||
authTransport: .onHeader,
|
||||
path: "/some/path/to/resource",
|
||||
headerFields: [.accept: "*/*"]
|
||||
)
|
||||
}
|
||||
|
||||
@Test(arguments: Input.authMethods)
|
||||
func `intercept with authorization on query when query is populated`(
|
||||
_ authMethod: AuthMethod
|
||||
) async throws {
|
||||
try await assertIntercept(
|
||||
authMethod: authMethod,
|
||||
authTransport: .onQuery,
|
||||
path: "/some/path/to/resource?key=value"
|
||||
)
|
||||
}
|
||||
#else
|
||||
@Test("intercept with authorization on header", arguments: Input.authMethods)
|
||||
func intercept_withAuthOnHeader(
|
||||
_ authMethod: AuthMethod
|
||||
) async throws {
|
||||
try await assertIntercept(
|
||||
authMethod: authMethod,
|
||||
authTransport: .onHeader,
|
||||
path: "/some/path/to/resource"
|
||||
)
|
||||
}
|
||||
|
||||
@Test("intercept with authorization on query", arguments: Input.authMethods)
|
||||
func intercept_withAuthOnQuery(
|
||||
_ authMethod: AuthMethod
|
||||
) async throws {
|
||||
try await assertIntercept(
|
||||
authMethod: authMethod,
|
||||
authTransport: .onQuery,
|
||||
path: "/some/path/to/resource"
|
||||
)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"intercept with authorization on header when headers are populated",
|
||||
arguments: Input.authMethods
|
||||
)
|
||||
func intercept_withAuthOnHeader_whenHeadersPopulated(
|
||||
_ authMethod: AuthMethod
|
||||
) async throws {
|
||||
try await assertIntercept(
|
||||
authMethod: authMethod,
|
||||
authTransport: .onHeader,
|
||||
path: "/some/path/to/resource",
|
||||
headerFields: [.accept: "*/*"]
|
||||
)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"intercept with authorization on query when query is populated",
|
||||
arguments: Input.authMethods
|
||||
)
|
||||
func intercept_withAuthOnQuery_whenQueryPopulated(
|
||||
_ authMethod: AuthMethod
|
||||
) async throws {
|
||||
try await assertIntercept(
|
||||
authMethod: authMethod,
|
||||
authTransport: .onQuery,
|
||||
path: "/some/path/to/resource?key=value"
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Assertions
|
||||
|
||||
private extension AuthMiddlewareTests {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Asserts the initialization of the middleware, especially the assignment of its properties.
|
||||
/// - Parameters:
|
||||
/// - authMethod: A representation of an authentication method.
|
||||
/// - authTransport: A representation of an authentication transport.
|
||||
/// - Throws: an error of type ``InputValidationError`` in case of an unexpected error occurs while running test cases.
|
||||
func assertInit(
|
||||
authMethod: AuthMethod,
|
||||
authTransport: AuthTransport,
|
||||
) throws {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
let middleware = try AuthMiddleware(
|
||||
method: authMethod,
|
||||
transport: authTransport
|
||||
)
|
||||
|
||||
// THEN
|
||||
switch (authMethod, authTransport) {
|
||||
case let (.consumer(key, secret), .onHeader):
|
||||
#expect(middleware.authItems == nil)
|
||||
#expect(middleware.authField == .init(
|
||||
name: .authorization,
|
||||
value: "Discogs \(String.Parameter.key)=\(key), \(String.Parameter.secret)=\(secret)"
|
||||
))
|
||||
|
||||
case let (.consumer(key, secret), .onQuery):
|
||||
#expect(middleware.authField == nil)
|
||||
#expect(middleware.authItems == [
|
||||
.init(name: .Parameter.key, value: key),
|
||||
.init(name: .Parameter.secret, value: secret)
|
||||
])
|
||||
|
||||
case let (.user(token), .onHeader):
|
||||
#expect(middleware.authItems == nil)
|
||||
#expect(middleware.authField == .init(
|
||||
name: .authorization,
|
||||
value: "Discogs \(String.Parameter.token)=\(token)"
|
||||
))
|
||||
|
||||
case let (.user(token), .onQuery):
|
||||
#expect(middleware.authField == nil)
|
||||
#expect(middleware.authItems == [
|
||||
.init(name: .Parameter.token, value: token)
|
||||
])
|
||||
|
||||
case (.none, _):
|
||||
#expect(middleware.authField == nil)
|
||||
#expect(middleware.authItems == nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts the error throwing (if justified) during the initialization of a middleware.
|
||||
/// - Parameters:
|
||||
/// - authMethod: A representation of an authentication method.
|
||||
/// - authTransport: A representation of an authentication transport.
|
||||
/// - error: An expected error of type ``InputValidationError`` during the initialization of a middleware.
|
||||
func assertInitThrows(
|
||||
authMethod: AuthMethod,
|
||||
authTransport: AuthTransport,
|
||||
expects error: InputValidationError?
|
||||
) {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
// THEN
|
||||
if let error {
|
||||
#expect(throws: error) {
|
||||
try AuthMiddleware(
|
||||
method: authMethod,
|
||||
transport: authTransport
|
||||
)
|
||||
}
|
||||
} else {
|
||||
#expect(throws: Never.self) {
|
||||
try AuthMiddleware(
|
||||
method: authMethod,
|
||||
transport: authTransport
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts the interception of a request to add its authentication.
|
||||
/// - Parameters:
|
||||
/// - authMethod: A representation of an authentication method.
|
||||
/// - authTransport: A representation of an authentication transport.
|
||||
/// - path: A URI path for a request.
|
||||
/// - headerFields: A set of header fields for a request.
|
||||
/// - Throws:An error in case of an unexpected issue encountered while running a test case.
|
||||
func assertIntercept(
|
||||
authMethod: AuthMethod,
|
||||
authTransport: AuthTransport,
|
||||
path: String,
|
||||
headerFields: HTTPFields = [:],
|
||||
) async throws {
|
||||
// GIVEN
|
||||
let middleware = try AuthMiddleware(
|
||||
method: authMethod,
|
||||
transport: authTransport
|
||||
)
|
||||
let request = HTTPRequest(
|
||||
path: path,
|
||||
headerFields: headerFields
|
||||
)
|
||||
|
||||
// WHEN
|
||||
_ = try await confirmation { confirmation in
|
||||
try await middleware.intercept(
|
||||
request,
|
||||
body: nil,
|
||||
baseURL: .Sample.baseURL,
|
||||
operationID: .Sample.operationId
|
||||
) { request, _, _ in
|
||||
// THEN
|
||||
switch (authMethod, authTransport) {
|
||||
case (.consumer, .onHeader):
|
||||
#expect(request.path == path)
|
||||
#expect(request.headerFields != headerFields)
|
||||
#expect(request.headerFields.contains(where: { $0.name == .authorization }))
|
||||
|
||||
case (.consumer, .onQuery):
|
||||
#expect(request.headerFields == headerFields)
|
||||
try assertAuthInPath(request.path, authMethod)
|
||||
|
||||
case (.user, .onHeader):
|
||||
#expect(request.path == path)
|
||||
#expect(request.headerFields != headerFields)
|
||||
#expect(request.headerFields.contains(where: { $0.name == .authorization }))
|
||||
|
||||
case (.user, .onQuery):
|
||||
#expect(request.headerFields == headerFields)
|
||||
try assertAuthInPath(request.path, authMethod)
|
||||
|
||||
case (.none, _):
|
||||
#expect(request.path == path)
|
||||
#expect(request.headerFields == headerFields)
|
||||
}
|
||||
|
||||
confirmation()
|
||||
|
||||
return (.init(status: .ok) , nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts the value of `shouldAuthenticate` flag after an initialization of a middleware.
|
||||
/// - Parameters:
|
||||
/// - authMethod: A representation of an authentication method.
|
||||
/// - authTransport: A representation of an authentication transport.
|
||||
/// - flag: An expected flag that indicates whether the middleware should authenticate its requests or not.
|
||||
/// - Throws: An error of type ``InputValidationError`` in case of an unexpected issue occurs while running test cases.
|
||||
func assertShouldAuthenticate(
|
||||
authMethod: AuthMethod,
|
||||
authTransport: AuthTransport,
|
||||
expects flag: Bool
|
||||
) throws {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
let middleware = try AuthMiddleware(
|
||||
method: authMethod,
|
||||
transport: authTransport
|
||||
)
|
||||
|
||||
// THEN
|
||||
#expect(middleware.shouldAuthenticate == flag)
|
||||
}
|
||||
|
||||
/// Asserts a request path to contain authentication parameters in its query.
|
||||
/// - Parameters:
|
||||
/// - path: A request path
|
||||
/// - authMethod: A representation of an authentication method.
|
||||
/// - Throws:An error in case of an unexpected issue encountered while unwrapping the optionals.
|
||||
func assertAuthInPath(
|
||||
_ path: String?,
|
||||
_ authMethod: AuthMethod
|
||||
) throws {
|
||||
let pathRequest = try #require(path)
|
||||
let urlComponents = try #require(URLComponents(string: pathRequest))
|
||||
let queryItems = try #require(urlComponents.queryItems)
|
||||
|
||||
switch authMethod {
|
||||
case .consumer:
|
||||
#expect(queryItems.count >= 2)
|
||||
#expect(queryItems.contains(where: { $0.name == .Parameter.key }))
|
||||
#expect(queryItems.contains(where: { $0.name == .Parameter.secret }))
|
||||
case .user:
|
||||
#expect(queryItems.count >= 1)
|
||||
#expect(queryItems.contains(where: { $0.name == .Parameter.token }))
|
||||
case .none: break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension AuthMiddlewareTests {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// Provides a random authentication transport representation.
|
||||
var randomTransport: AuthTransport {
|
||||
get throws {
|
||||
try #require(AuthTransport.allCases.randomElement())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension Input {
|
||||
/// A list of authentication methods to use in most of the test cases.
|
||||
static let authMethods: [AuthMethod] = [
|
||||
.consumer(key: "aAbBcCdDeEfFgGhHiIjJ", secret: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP"),
|
||||
.user(token: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStT"),
|
||||
.none
|
||||
]
|
||||
/// A list of authentication methods to use in the initialization throw test cases.
|
||||
static let authMethodsThrows: [AuthMethod] = authMethods + [
|
||||
.consumer(key: .empty, secret: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP"),
|
||||
.consumer(key: "aAbBcCdDeEfFgGhHiI", secret: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP"),
|
||||
.consumer(key: "aAbBcCdDeEfFgGhHiIjJkK", secret: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP"),
|
||||
.consumer(key: "a4bBcCdDe3fFg6hH1Ij7", secret: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP"),
|
||||
.consumer(key: "aAbBcCdDeEfFgGhHiIjJ", secret: .empty),
|
||||
.consumer(key: "aAbBcCdDeEfFgGhHiIjJ", secret: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoO"),
|
||||
.consumer(key: "aAbBcCdDeEfFgGhHiIjJ", secret: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQ"),
|
||||
.consumer(key: "aAbBcCdDeEfFgGhHiIjJ", secret: "a4bBcCdDe3fFg6hH1IjJkK1LmMnNo0p9"),
|
||||
.user(token: .empty),
|
||||
.user(token: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsS"),
|
||||
.user(token: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuU"),
|
||||
.user(token: "a4bBcCdDe3fFg6hH1IjJkK1LmMnNo0p9qQrRs5t7"),
|
||||
]
|
||||
}
|
||||
|
||||
private extension Output {
|
||||
/// A list of expected input validation errors (if thrown) coming from the initialization throw test cases.
|
||||
static let authMethodsThrows: [InputValidationError?] = [nil, nil, nil, .inputIsEmpty, .inputNotConsumerKey, .inputNotConsumerKey, .inputNotConsumerKey, .inputIsEmpty, .inputNotConsumerSecret, .inputNotConsumerSecret, .inputNotConsumerSecret, .inputIsEmpty, .inputNotUserToken, .inputNotUserToken, .inputNotUserToken]
|
||||
/// A list of expected boolean flags coming from the should authenticate test cases.
|
||||
static let authMethodsShouldAuthenticate: [Bool] = [true, true, false]
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import struct HTTPTypes.HTTPField
|
||||
import struct HTTPTypes.HTTPFields
|
||||
import struct HTTPTypes.HTTPRequest
|
||||
|
||||
import Testing
|
||||
|
||||
@testable import DiscogsService
|
||||
|
||||
@Suite("User Agent Middleware", .tags(.middleware))
|
||||
struct UserAgentMiddlewareTests {
|
||||
|
||||
// MARK: Initializers tests
|
||||
|
||||
#if swift(>=6.2)
|
||||
@Test(arguments: Input.userAgents)
|
||||
func `initialize`(
|
||||
product: Product
|
||||
) throws {
|
||||
try assertInit(product: product)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
Input.userAgentsThrows,
|
||||
Output.userAgentsThrows
|
||||
))
|
||||
func `initialize throws`(
|
||||
product: Product,
|
||||
expect error: InputValidationError?
|
||||
) {
|
||||
assertInitThrows(
|
||||
product: product,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
#else
|
||||
@Test("initialize", arguments: Input.userAgents)
|
||||
func initialize(
|
||||
product: Product
|
||||
) throws {
|
||||
try assertInit(product: product)
|
||||
}
|
||||
|
||||
@Test("initialize throws", arguments: zip(
|
||||
Input.userAgentsThrows,
|
||||
Output.userAgentsThrows
|
||||
))
|
||||
func initializeThrows(
|
||||
product: Product,
|
||||
expect error: InputValidationError?
|
||||
) {
|
||||
assertInitThrows(
|
||||
product: product,
|
||||
expects: error
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: Functions tests
|
||||
|
||||
#if swift(>=6.2)
|
||||
@Test(arguments: Input.userAgents)
|
||||
func `intercept with user agent on headers`(
|
||||
product: Product
|
||||
) async throws {
|
||||
try await assertIntercept(product: product)
|
||||
}
|
||||
|
||||
@Test(arguments: Input.userAgents)
|
||||
func `intercept with user agent on headers when headers are populated`(
|
||||
product: Product
|
||||
) async throws {
|
||||
try await assertIntercept(
|
||||
product: product,
|
||||
headerFields: [.accept: "*/*"]
|
||||
)
|
||||
}
|
||||
#else
|
||||
@Test("intercept with user agent on headers", arguments: Input.userAgents)
|
||||
func intercept_withUserAgentOnHeaders(
|
||||
product: Product
|
||||
) async throws {
|
||||
try await assertIntercept(product: product)
|
||||
}
|
||||
|
||||
@Test("intercept with user agent on headers when headers are populated", arguments: Input.userAgents)
|
||||
func intercept_withUserAgentOnHeaders_whenHeadersPopulated(
|
||||
product: Product
|
||||
) async throws {
|
||||
try await assertIntercept(
|
||||
product: product,
|
||||
headerFields: [.accept: "*/*"]
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Assertions
|
||||
|
||||
private extension UserAgentMiddlewareTests {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Asserts the initialization of the middleware , especially the assignments of its properties.
|
||||
/// - Parameter product: A product to initialize a middleware.
|
||||
/// - Throws: an error of type ``InputValidationError`` in case of an unexpected error occurs while running test cases.
|
||||
func assertInit(
|
||||
product: Product
|
||||
) throws {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
let middleware = try UserAgentMiddleware(product: product)
|
||||
|
||||
// THEN
|
||||
#expect(middleware.agentField == .init(
|
||||
name: .userAgent,
|
||||
value: "\(product.name)/\(product.version) +\(product.url)"
|
||||
))
|
||||
}
|
||||
|
||||
/// Asserts the error throwing (if justified) during the initialization of the middleware.
|
||||
/// - Parameters:
|
||||
/// - product: A product to initialize a middleware.
|
||||
/// - error: An expected error of type ``InputValidationError`` during the initialization of a middleware.
|
||||
func assertInitThrows(
|
||||
product: Product,
|
||||
expects error: InputValidationError?
|
||||
) {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
// THEN
|
||||
if let error {
|
||||
#expect(throws: error) {
|
||||
try UserAgentMiddleware(product: product)
|
||||
}
|
||||
} else {
|
||||
#expect(throws: Never.self) {
|
||||
try UserAgentMiddleware(product: product)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts the interception of a request to add the user agent in its header.
|
||||
/// - Parameters:
|
||||
/// - product: A product to initialize a middleware.
|
||||
/// - path: A URI path for a request.
|
||||
/// - headerFields: A set of header fields for a request.
|
||||
func assertIntercept(
|
||||
product: Product,
|
||||
path: String? = nil,
|
||||
headerFields: HTTPFields = [:]
|
||||
) async throws {
|
||||
// GIVEN
|
||||
let middleware = try UserAgentMiddleware(product: product)
|
||||
let request = HTTPRequest(
|
||||
path: path,
|
||||
headerFields: headerFields
|
||||
)
|
||||
|
||||
// WHEN
|
||||
_ = try await confirmation { confirmation in
|
||||
try await middleware.intercept(
|
||||
request,
|
||||
body: nil,
|
||||
baseURL: .Sample.baseURL,
|
||||
operationID: .Sample.operationId
|
||||
) { request, _, _ in
|
||||
// THEN
|
||||
#expect(request.path == path)
|
||||
#expect(request.headerFields != headerFields)
|
||||
#expect(request.headerFields.count == headerFields.count + 1)
|
||||
#expect(request.headerFields.contains(where: { $0.name == .userAgent }))
|
||||
|
||||
confirmation()
|
||||
|
||||
return (.init(status: .ok) , nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension Input {
|
||||
/// A list of products to successfully initialize user agent middleware instances.
|
||||
static let userAgents: [Product] = [
|
||||
.init(name: "SomeApp", version: "0.0.1", url: "http://www.some.app"),
|
||||
.init(name: "SomeOther4pp", version: "1.2.3-b1", url: "https://some-other.app"),
|
||||
.init(name: "Yet4notherApp", version: "0.8.8+alpha", url: "https://yet.another.app")
|
||||
]
|
||||
/// A list of products to use in the initialization throw test cases.
|
||||
static let userAgentsThrows: [Product] = userAgents + [
|
||||
.init(name: "Some App", version: "0.0.1", url: "http://www.some.app"),
|
||||
.init(name: "Some-App", version: "0.0.1", url: "http://www.some.app"),
|
||||
.init(name: .empty, version: "0.0.1", url: "http://www.some.app"),
|
||||
.init(name: "SomeApp", version: "v0.0.1", url: "http://www.some.app"),
|
||||
.init(name: "SomeApp", version: "0.1", url: "http://www.some.app"),
|
||||
.init(name: "SomeApp", version: .empty, url: "http://www.some.app"),
|
||||
.init(name: "SomeApp", version: "0.0.1", url: "www.some.app"),
|
||||
.init(name: "SomeApp", version: "0.0.1", url: "some.app"),
|
||||
.init(name: "SomeApp", version: "0.0.1", url: .empty),
|
||||
.init(name: "Some App", version: "v0.0.1", url: "www.some.app"),
|
||||
.init(name: "SomeApp", version: "v0.0.1", url: "www.some.app"),
|
||||
.init(name: "Some App", version: "0.0.1", url: "www.some.app"),
|
||||
]
|
||||
}
|
||||
|
||||
private extension Output {
|
||||
/// A list of expected input validation errors (if thrown) coming from the initialization throw test cases.
|
||||
static let userAgentsThrows: [InputValidationError?] = [nil, nil, nil, .inputNotCamelCase, .inputNotCamelCase, .inputIsEmpty, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputIsEmpty, .inputNotURL, .inputNotURL, .inputIsEmpty, .inputNotCamelCase, .inputNotSemanticVersion, .inputNotCamelCase]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import struct HTTPTypes.HTTPFields
|
||||
import struct HTTPTypes.HTTPRequest
|
||||
|
||||
extension HTTPRequest {
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes a HTTP request conveniently.
|
||||
/// - Parameters:
|
||||
/// - method: A request method.
|
||||
/// - path: A value of the “:path” pseudo header field.
|
||||
/// - headerFields: A dictionary of request header fields.
|
||||
init(
|
||||
method: HTTPRequest.Method = .get,
|
||||
path: String?,
|
||||
headerFields: HTTPFields = [:]
|
||||
) {
|
||||
self.init(
|
||||
method: method,
|
||||
scheme: nil,
|
||||
authority: nil,
|
||||
path: path,
|
||||
headerFields: headerFields
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Testing
|
||||
|
||||
extension Tag {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// A tag that indicates tests for a type extension.
|
||||
@Tag static var `extension`: Self
|
||||
|
||||
/// A tag that indicates tests for a middleware type.
|
||||
@Tag static var middleware: Self
|
||||
|
||||
/// A tag that indicates tests for a use case type.
|
||||
@Tag static var useCase: Self
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A namespace assigned for input arguments on test cases.
|
||||
enum Input {}
|
||||
@@ -0,0 +1,16 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
/// A namespace assigned for output arguments on test cases, that are expected results.
|
||||
enum Output {}
|
||||
@@ -0,0 +1,21 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
extension String {
|
||||
/// A namespace assigned for string samples on test cases.
|
||||
enum Sample {
|
||||
/// An operation ID sample.
|
||||
static let operationId = "SomeOperationId"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// ===----------------------------------------------------------------------===
|
||||
//
|
||||
// This source file is part of the DiscogsService open source project
|
||||
//
|
||||
// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors
|
||||
// Licensed under Apache license v2.0
|
||||
//
|
||||
// See LICENSE for license information
|
||||
// See CONTRIBUTORS for the list of DiscogsService project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ===----------------------------------------------------------------------===
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
/// A namespace assigned for URL samples on test cases.
|
||||
enum Sample {
|
||||
/// A base URL sample.
|
||||
static let baseURL = URL(string: "https://sample.domain.com")!
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import Testing
|
||||
@testable import discogs_service
|
||||
|
||||
@Test func example() async throws {
|
||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"primaryContentSections":[{"kind":"declarations","declarations":[{"tokens":[{"kind":"keyword","text":"static"},{"kind":"text","text":" "},{"kind":"keyword","text":"func"},{"kind":"text","text":" "},{"kind":"identifier","text":"!="},{"kind":"text","text":" "},{"kind":"text","text":"("},{"kind":"internalParam","text":"lhs"},{"kind":"text","text":": "},{"kind":"typeIdentifier","text":"Self"},{"kind":"text","text":", "},{"kind":"internalParam","text":"rhs"},{"kind":"text","text":": "},{"kind":"typeIdentifier","text":"Self"},{"kind":"text","text":") -> "},{"kind":"typeIdentifier","text":"Bool","preciseIdentifier":"s:Sb"}],"platforms":["macOS"],"languages":["swift"]}]},{"kind":"parameters","parameters":[{"content":[{"type":"paragraph","inlineContent":[{"text":"A value to compare.","type":"text"}]}],"name":"lhs"},{"content":[{"type":"paragraph","inlineContent":[{"text":"Another value to compare.","type":"text"}]}],"name":"rhs"}]},{"kind":"content","content":[{"type":"heading","level":2,"anchor":"discussion","text":"Discussion"},{"type":"paragraph","inlineContent":[{"type":"text","text":"Inequality is the inverse of equality. For any values "},{"type":"codeVoice","code":"a"},{"type":"text","text":" and "},{"type":"codeVoice","code":"b"},{"type":"text","text":", "},{"type":"codeVoice","code":"a != b"},{"type":"text","text":" "},{"type":"text","text":"implies that "},{"type":"codeVoice","code":"a == b"},{"type":"text","text":" is "},{"type":"codeVoice","code":"false"},{"type":"text","text":"."}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"This is the default implementation of the not-equal-to operator ("},{"type":"codeVoice","code":"!="},{"type":"text","text":")"},{"type":"text","text":" "},{"type":"text","text":"for any type that conforms to "},{"type":"codeVoice","code":"Equatable"},{"type":"text","text":"."}]}]}],"metadata":{"roleHeading":"Operator","fragments":[{"kind":"keyword","text":"static"},{"kind":"text","text":" "},{"kind":"keyword","text":"func"},{"kind":"text","text":" "},{"kind":"identifier","text":"!="},{"text":" ","kind":"text"},{"kind":"text","text":"("},{"kind":"typeIdentifier","text":"Self"},{"text":", ","kind":"text"},{"kind":"typeIdentifier","text":"Self"},{"text":") -> ","kind":"text"},{"preciseIdentifier":"s:Sb","text":"Bool","kind":"typeIdentifier"}],"extendedModule":"Swift","symbolKind":"op","title":"!=(_:_:)","role":"symbol","externalID":"s:SQsE2neoiySbx_xtFZ::SYNTHESIZED::s:14DiscogsService10AuthMethodO","modules":[{"name":"DiscogsService","relatedModules":["Swift"]}]},"variants":[{"paths":["\/documentation\/discogsservice\/authmethod\/!=(_:_:)"],"traits":[{"interfaceLanguage":"swift"}]}],"kind":"symbol","schemaVersion":{"minor":3,"patch":0,"major":0},"abstract":[{"type":"text","text":"Returns a Boolean value indicating whether two values are not equal."}],"identifier":{"interfaceLanguage":"swift","url":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod\/!=(_:_:)"},"hierarchy":{"paths":[["doc:\/\/DiscogsService\/documentation\/DiscogsService","doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod"]]},"sections":[],"references":{"doc://DiscogsService/documentation/DiscogsService/AuthMethod/!=(_:_:)":{"url":"\/documentation\/discogsservice\/authmethod\/!=(_:_:)","role":"symbol","abstract":[{"type":"text","text":"Returns a Boolean value indicating whether two values are not equal."}],"identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod\/!=(_:_:)","fragments":[{"text":"static","kind":"keyword"},{"kind":"text","text":" "},{"text":"func","kind":"keyword"},{"kind":"text","text":" "},{"text":"!=","kind":"identifier"},{"text":" ","kind":"text"},{"text":"(","kind":"text"},{"text":"Self","kind":"typeIdentifier"},{"text":", ","kind":"text"},{"text":"Self","kind":"typeIdentifier"},{"text":") -> ","kind":"text"},{"text":"Bool","kind":"typeIdentifier","preciseIdentifier":"s:Sb"}],"kind":"symbol","type":"topic","title":"!=(_:_:)"},"doc://DiscogsService/documentation/DiscogsService/AuthMethod":{"url":"\/documentation\/discogsservice\/authmethod","role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod","abstract":[{"type":"text","text":"A representation of the available authentication methods at the Discogs service."}],"fragments":[{"kind":"keyword","text":"enum"},{"text":" ","kind":"text"},{"kind":"identifier","text":"AuthMethod"}],"kind":"symbol","title":"AuthMethod","type":"topic","navigatorTitle":[{"text":"AuthMethod","kind":"identifier"}]},"doc://DiscogsService/documentation/DiscogsService":{"title":"DiscogsService","abstract":[],"role":"collection","type":"topic","kind":"symbol","url":"\/documentation\/discogsservice","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService"}}}
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":{"major":0,"minor":3,"patch":0},"abstract":[{"type":"text","text":"A consumer key and secret that allows access to endpoints that requires authentication."}],"identifier":{"url":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod\/consumer(key:secret:)","interfaceLanguage":"swift"},"sections":[],"kind":"symbol","metadata":{"modules":[{"name":"DiscogsService"}],"title":"AuthMethod.consumer(key:secret:)","role":"symbol","symbolKind":"case","externalID":"s:14DiscogsService10AuthMethodO8consumeryACSS_SStcACmF","roleHeading":"Case","fragments":[{"text":"case","kind":"keyword"},{"text":" ","kind":"text"},{"text":"consumer","kind":"identifier"},{"text":"(","kind":"text"},{"text":"key","kind":"externalParam"},{"text":": ","kind":"text"},{"text":"String","kind":"typeIdentifier","preciseIdentifier":"s:SS"},{"text":", ","kind":"text"},{"text":"secret","kind":"externalParam"},{"text":": ","kind":"text"},{"kind":"typeIdentifier","text":"String","preciseIdentifier":"s:SS"},{"kind":"text","text":")"}]},"primaryContentSections":[{"kind":"declarations","declarations":[{"tokens":[{"kind":"keyword","text":"case"},{"kind":"text","text":" "},{"kind":"identifier","text":"consumer"},{"kind":"text","text":"("},{"kind":"externalParam","text":"key"},{"kind":"text","text":": "},{"kind":"typeIdentifier","text":"String","preciseIdentifier":"s:SS"},{"kind":"text","text":", "},{"kind":"externalParam","text":"secret"},{"kind":"text","text":": "},{"kind":"typeIdentifier","text":"String","preciseIdentifier":"s:SS"},{"kind":"text","text":")"}],"languages":["swift"],"platforms":["macOS"]}]}],"hierarchy":{"paths":[["doc:\/\/DiscogsService\/documentation\/DiscogsService","doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod"]]},"variants":[{"traits":[{"interfaceLanguage":"swift"}],"paths":["\/documentation\/discogsservice\/authmethod\/consumer(key:secret:)"]}],"references":{"doc://DiscogsService/documentation/DiscogsService/AuthMethod/consumer(key:secret:)":{"abstract":[{"type":"text","text":"A consumer key and secret that allows access to endpoints that requires authentication."}],"identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod\/consumer(key:secret:)","url":"\/documentation\/discogsservice\/authmethod\/consumer(key:secret:)","title":"AuthMethod.consumer(key:secret:)","role":"symbol","type":"topic","kind":"symbol","fragments":[{"text":"case","kind":"keyword"},{"text":" ","kind":"text"},{"text":"consumer","kind":"identifier"},{"text":"(","kind":"text"},{"text":"key","kind":"externalParam"},{"text":": ","kind":"text"},{"text":"String","kind":"typeIdentifier","preciseIdentifier":"s:SS"},{"text":", ","kind":"text"},{"text":"secret","kind":"externalParam"},{"text":": ","kind":"text"},{"text":"String","kind":"typeIdentifier","preciseIdentifier":"s:SS"},{"text":")","kind":"text"}]},"doc://DiscogsService/documentation/DiscogsService":{"title":"DiscogsService","abstract":[],"role":"collection","type":"topic","kind":"symbol","url":"\/documentation\/discogsservice","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService"},"doc://DiscogsService/documentation/DiscogsService/AuthMethod":{"url":"\/documentation\/discogsservice\/authmethod","role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod","abstract":[{"type":"text","text":"A representation of the available authentication methods at the Discogs service."}],"fragments":[{"kind":"keyword","text":"enum"},{"text":" ","kind":"text"},{"kind":"identifier","text":"AuthMethod"}],"kind":"symbol","title":"AuthMethod","type":"topic","navigatorTitle":[{"text":"AuthMethod","kind":"identifier"}]}}}
|
||||
@@ -0,0 +1 @@
|
||||
{"variants":[{"traits":[{"interfaceLanguage":"swift"}],"paths":["\/documentation\/discogsservice\/authmethod\/equatable-implementations"]}],"hierarchy":{"paths":[["doc:\/\/DiscogsService\/documentation\/DiscogsService","doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod"]]},"identifier":{"interfaceLanguage":"swift","url":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod\/Equatable-Implementations"},"topicSections":[{"generated":true,"anchor":"Operators","title":"Operators","identifiers":["doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod\/!=(_:_:)"]}],"kind":"article","sections":[],"metadata":{"title":"Equatable Implementations","modules":[{"name":"DiscogsService"}],"role":"collectionGroup","roleHeading":"API Collection"},"schemaVersion":{"major":0,"minor":3,"patch":0},"references":{"doc://DiscogsService/documentation/DiscogsService/AuthMethod":{"url":"\/documentation\/discogsservice\/authmethod","role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod","abstract":[{"type":"text","text":"A representation of the available authentication methods at the Discogs service."}],"fragments":[{"kind":"keyword","text":"enum"},{"text":" ","kind":"text"},{"kind":"identifier","text":"AuthMethod"}],"kind":"symbol","title":"AuthMethod","type":"topic","navigatorTitle":[{"text":"AuthMethod","kind":"identifier"}]},"doc://DiscogsService/documentation/DiscogsService":{"title":"DiscogsService","abstract":[],"role":"collection","type":"topic","kind":"symbol","url":"\/documentation\/discogsservice","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService"},"doc://DiscogsService/documentation/DiscogsService/AuthMethod/!=(_:_:)":{"url":"\/documentation\/discogsservice\/authmethod\/!=(_:_:)","role":"symbol","abstract":[{"type":"text","text":"Returns a Boolean value indicating whether two values are not equal."}],"identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod\/!=(_:_:)","fragments":[{"text":"static","kind":"keyword"},{"kind":"text","text":" "},{"text":"func","kind":"keyword"},{"kind":"text","text":" "},{"text":"!=","kind":"identifier"},{"text":" ","kind":"text"},{"text":"(","kind":"text"},{"text":"Self","kind":"typeIdentifier"},{"text":", ","kind":"text"},{"text":"Self","kind":"typeIdentifier"},{"text":") -> ","kind":"text"},{"text":"Bool","kind":"typeIdentifier","preciseIdentifier":"s:Sb"}],"kind":"symbol","type":"topic","title":"!=(_:_:)"}}}
|
||||
@@ -0,0 +1 @@
|
||||
{"primaryContentSections":[{"kind":"declarations","declarations":[{"platforms":["macOS"],"tokens":[{"kind":"keyword","text":"case"},{"kind":"text","text":" "},{"kind":"identifier","text":"none"}],"languages":["swift"]}]}],"metadata":{"modules":[{"name":"DiscogsService"}],"fragments":[{"kind":"keyword","text":"case"},{"kind":"text","text":" "},{"kind":"identifier","text":"none"}],"symbolKind":"case","title":"AuthMethod.none","role":"symbol","externalID":"s:14DiscogsService10AuthMethodO4noneyA2CmF","roleHeading":"Case"},"variants":[{"paths":["\/documentation\/discogsservice\/authmethod\/none"],"traits":[{"interfaceLanguage":"swift"}]}],"kind":"symbol","schemaVersion":{"patch":0,"major":0,"minor":3},"abstract":[{"type":"text","text":"No authentication method defined."}],"identifier":{"interfaceLanguage":"swift","url":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod\/none"},"hierarchy":{"paths":[["doc:\/\/DiscogsService\/documentation\/DiscogsService","doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod"]]},"sections":[],"references":{"doc://DiscogsService/documentation/DiscogsService/AuthMethod/none":{"type":"topic","url":"\/documentation\/discogsservice\/authmethod\/none","kind":"symbol","title":"AuthMethod.none","abstract":[{"type":"text","text":"No authentication method defined."}],"role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod\/none","fragments":[{"text":"case","kind":"keyword"},{"text":" ","kind":"text"},{"text":"none","kind":"identifier"}]},"doc://DiscogsService/documentation/DiscogsService":{"title":"DiscogsService","abstract":[],"role":"collection","type":"topic","kind":"symbol","url":"\/documentation\/discogsservice","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService"},"doc://DiscogsService/documentation/DiscogsService/AuthMethod":{"url":"\/documentation\/discogsservice\/authmethod","role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod","abstract":[{"type":"text","text":"A representation of the available authentication methods at the Discogs service."}],"fragments":[{"kind":"keyword","text":"enum"},{"text":" ","kind":"text"},{"kind":"identifier","text":"AuthMethod"}],"kind":"symbol","title":"AuthMethod","type":"topic","navigatorTitle":[{"text":"AuthMethod","kind":"identifier"}]}}}
|
||||
@@ -0,0 +1 @@
|
||||
{"primaryContentSections":[{"kind":"declarations","declarations":[{"tokens":[{"kind":"keyword","text":"case"},{"kind":"text","text":" "},{"kind":"identifier","text":"user"},{"kind":"text","text":"("},{"text":"token","kind":"externalParam"},{"text":": ","kind":"text"},{"text":"String","kind":"typeIdentifier","preciseIdentifier":"s:SS"},{"text":")","kind":"text"}],"languages":["swift"],"platforms":["macOS"]}]}],"metadata":{"roleHeading":"Case","fragments":[{"kind":"keyword","text":"case"},{"kind":"text","text":" "},{"kind":"identifier","text":"user"},{"kind":"text","text":"("},{"text":"token","kind":"externalParam"},{"text":": ","kind":"text"},{"text":"String","preciseIdentifier":"s:SS","kind":"typeIdentifier"},{"text":")","kind":"text"}],"symbolKind":"case","title":"AuthMethod.user(token:)","role":"symbol","externalID":"s:14DiscogsService10AuthMethodO4useryACSS_tcACmF","modules":[{"name":"DiscogsService"}]},"variants":[{"traits":[{"interfaceLanguage":"swift"}],"paths":["\/documentation\/discogsservice\/authmethod\/user(token:)"]}],"kind":"symbol","schemaVersion":{"major":0,"minor":3,"patch":0},"abstract":[{"text":"A user token that allows access to its own account information.","type":"text"}],"identifier":{"url":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod\/user(token:)","interfaceLanguage":"swift"},"hierarchy":{"paths":[["doc:\/\/DiscogsService\/documentation\/DiscogsService","doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod"]]},"sections":[],"references":{"doc://DiscogsService/documentation/DiscogsService/AuthMethod/user(token:)":{"type":"topic","url":"\/documentation\/discogsservice\/authmethod\/user(token:)","kind":"symbol","title":"AuthMethod.user(token:)","abstract":[{"type":"text","text":"A user token that allows access to its own account information."}],"role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod\/user(token:)","fragments":[{"text":"case","kind":"keyword"},{"text":" ","kind":"text"},{"text":"user","kind":"identifier"},{"text":"(","kind":"text"},{"text":"token","kind":"externalParam"},{"text":": ","kind":"text"},{"text":"String","kind":"typeIdentifier","preciseIdentifier":"s:SS"},{"text":")","kind":"text"}]},"doc://DiscogsService/documentation/DiscogsService/AuthMethod":{"url":"\/documentation\/discogsservice\/authmethod","role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMethod","abstract":[{"type":"text","text":"A representation of the available authentication methods at the Discogs service."}],"fragments":[{"kind":"keyword","text":"enum"},{"text":" ","kind":"text"},{"kind":"identifier","text":"AuthMethod"}],"kind":"symbol","title":"AuthMethod","type":"topic","navigatorTitle":[{"text":"AuthMethod","kind":"identifier"}]},"doc://DiscogsService/documentation/DiscogsService":{"title":"DiscogsService","abstract":[],"role":"collection","type":"topic","kind":"symbol","url":"\/documentation\/discogsservice","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService"}}}
|
||||
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
{"schemaVersion":{"major":0,"minor":3,"patch":0},"identifier":{"interfaceLanguage":"swift","url":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMiddleware\/ClientMiddleware-Implementations"},"sections":[],"kind":"article","metadata":{"title":"ClientMiddleware Implementations","modules":[{"name":"DiscogsService"}],"role":"collectionGroup","roleHeading":"API Collection"},"topicSections":[{"anchor":"Instance-Methods","identifiers":["doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMiddleware\/intercept(_:body:baseURL:operationID:next:)"],"generated":true,"title":"Instance Methods"}],"hierarchy":{"paths":[["doc:\/\/DiscogsService\/documentation\/DiscogsService","doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMiddleware"]]},"variants":[{"traits":[{"interfaceLanguage":"swift"}],"paths":["\/documentation\/discogsservice\/authmiddleware\/clientmiddleware-implementations"]}],"references":{"doc://DiscogsService/documentation/DiscogsService/AuthMiddleware/intercept(_:body:baseURL:operationID:next:)":{"identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMiddleware\/intercept(_:body:baseURL:operationID:next:)","kind":"symbol","fragments":[{"text":"func","kind":"keyword"},{"text":" ","kind":"text"},{"text":"intercept","kind":"identifier"},{"text":"(","kind":"text"},{"text":"HTTPRequest","kind":"typeIdentifier","preciseIdentifier":"s:9HTTPTypes11HTTPRequestV"},{"text":", ","kind":"text"},{"text":"body","kind":"externalParam"},{"text":": ","kind":"text"},{"text":"HTTPBody","kind":"typeIdentifier","preciseIdentifier":"s:14OpenAPIRuntime8HTTPBodyC"},{"text":"?, ","kind":"text"},{"text":"baseURL","kind":"externalParam"},{"kind":"text","text":": "},{"preciseIdentifier":"s:10Foundation3URLV","kind":"typeIdentifier","text":"URL"},{"kind":"text","text":", "},{"text":"operationID","kind":"externalParam"},{"kind":"text","text":": "},{"preciseIdentifier":"s:SS","kind":"typeIdentifier","text":"String"},{"kind":"text","text":", "},{"kind":"externalParam","text":"next"},{"text":": (","kind":"text"},{"text":"HTTPRequest","preciseIdentifier":"s:9HTTPTypes11HTTPRequestV","kind":"typeIdentifier"},{"text":", ","kind":"text"},{"text":"HTTPBody","preciseIdentifier":"s:14OpenAPIRuntime8HTTPBodyC","kind":"typeIdentifier"},{"text":"?, ","kind":"text"},{"text":"URL","preciseIdentifier":"s:10Foundation3URLV","kind":"typeIdentifier"},{"kind":"text","text":") "},{"kind":"keyword","text":"async"},{"kind":"text","text":" "},{"kind":"keyword","text":"throws"},{"kind":"text","text":" -> ("},{"kind":"typeIdentifier","text":"HTTPResponse","preciseIdentifier":"s:9HTTPTypes12HTTPResponseV"},{"kind":"text","text":", "},{"kind":"typeIdentifier","text":"HTTPBody","preciseIdentifier":"s:14OpenAPIRuntime8HTTPBodyC"},{"kind":"text","text":"?)) "},{"kind":"keyword","text":"async"},{"kind":"text","text":" "},{"kind":"keyword","text":"throws"},{"kind":"text","text":" -> ("},{"kind":"typeIdentifier","text":"HTTPResponse","preciseIdentifier":"s:9HTTPTypes12HTTPResponseV"},{"kind":"text","text":", "},{"kind":"typeIdentifier","text":"HTTPBody","preciseIdentifier":"s:14OpenAPIRuntime8HTTPBodyC"},{"kind":"text","text":"?)"}],"title":"intercept(_:body:baseURL:operationID:next:)","abstract":[],"role":"symbol","url":"\/documentation\/discogsservice\/authmiddleware\/intercept(_:body:baseurl:operationid:next:)","type":"topic"},"doc://DiscogsService/documentation/DiscogsService":{"title":"DiscogsService","abstract":[],"role":"collection","type":"topic","kind":"symbol","url":"\/documentation\/discogsservice","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService"},"doc://DiscogsService/documentation/DiscogsService/AuthMiddleware":{"identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthMiddleware","kind":"symbol","fragments":[{"kind":"keyword","text":"struct"},{"text":" ","kind":"text"},{"kind":"identifier","text":"AuthMiddleware"}],"navigatorTitle":[{"kind":"identifier","text":"AuthMiddleware"}],"title":"AuthMiddleware","abstract":[{"type":"text","text":"A middleware that attaches any defined authentication credentials into the requests to the service."}],"role":"symbol","url":"\/documentation\/discogsservice\/authmiddleware","type":"topic"}}}
|
||||
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"primaryContentSections":[{"kind":"declarations","declarations":[{"languages":["swift"],"platforms":["macOS"],"tokens":[{"kind":"keyword","text":"static"},{"kind":"text","text":" "},{"kind":"keyword","text":"func"},{"kind":"text","text":" "},{"kind":"identifier","text":"!="},{"kind":"text","text":" "},{"kind":"text","text":"("},{"kind":"internalParam","text":"lhs"},{"kind":"text","text":": "},{"kind":"typeIdentifier","text":"Self"},{"kind":"text","text":", "},{"kind":"internalParam","text":"rhs"},{"kind":"text","text":": "},{"kind":"typeIdentifier","text":"Self"},{"kind":"text","text":") -> "},{"kind":"typeIdentifier","text":"Bool","preciseIdentifier":"s:Sb"}]}]},{"kind":"parameters","parameters":[{"name":"lhs","content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"A value to compare."}]}]},{"name":"rhs","content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"Another value to compare."}]}]}]},{"kind":"content","content":[{"text":"Discussion","type":"heading","level":2,"anchor":"discussion"},{"type":"paragraph","inlineContent":[{"type":"text","text":"Inequality is the inverse of equality. For any values "},{"type":"codeVoice","code":"a"},{"type":"text","text":" and "},{"type":"codeVoice","code":"b"},{"type":"text","text":", "},{"type":"codeVoice","code":"a != b"},{"type":"text","text":" "},{"type":"text","text":"implies that "},{"type":"codeVoice","code":"a == b"},{"type":"text","text":" is "},{"type":"codeVoice","code":"false"},{"type":"text","text":"."}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"This is the default implementation of the not-equal-to operator ("},{"type":"codeVoice","code":"!="},{"type":"text","text":")"},{"type":"text","text":" "},{"type":"text","text":"for any type that conforms to "},{"type":"codeVoice","code":"Equatable"},{"type":"text","text":"."}]}]}],"metadata":{"roleHeading":"Operator","fragments":[{"text":"static","kind":"keyword"},{"text":" ","kind":"text"},{"text":"func","kind":"keyword"},{"text":" ","kind":"text"},{"text":"!=","kind":"identifier"},{"text":" ","kind":"text"},{"text":"(","kind":"text"},{"text":"Self","kind":"typeIdentifier"},{"text":", ","kind":"text"},{"text":"Self","kind":"typeIdentifier"},{"text":") -> ","kind":"text"},{"text":"Bool","kind":"typeIdentifier","preciseIdentifier":"s:Sb"}],"extendedModule":"Swift","symbolKind":"op","title":"!=(_:_:)","role":"symbol","externalID":"s:SQsE2neoiySbx_xtFZ::SYNTHESIZED::s:14DiscogsService13AuthTransportO","modules":[{"name":"DiscogsService","relatedModules":["Swift"]}]},"variants":[{"paths":["\/documentation\/discogsservice\/authtransport\/!=(_:_:)"],"traits":[{"interfaceLanguage":"swift"}]}],"kind":"symbol","schemaVersion":{"major":0,"minor":3,"patch":0},"abstract":[{"text":"Returns a Boolean value indicating whether two values are not equal.","type":"text"}],"identifier":{"interfaceLanguage":"swift","url":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport\/!=(_:_:)"},"hierarchy":{"paths":[["doc:\/\/DiscogsService\/documentation\/DiscogsService","doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport"]]},"sections":[],"references":{"doc://DiscogsService/documentation/DiscogsService/AuthTransport":{"url":"\/documentation\/discogsservice\/authtransport","role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport","abstract":[{"type":"text","text":"A representation of the available transport options to send credentials in authenticated requests."}],"fragments":[{"text":"enum","kind":"keyword"},{"text":" ","kind":"text"},{"text":"AuthTransport","kind":"identifier"}],"kind":"symbol","type":"topic","title":"AuthTransport","navigatorTitle":[{"text":"AuthTransport","kind":"identifier"}]},"doc://DiscogsService/documentation/DiscogsService/AuthTransport/!=(_:_:)":{"url":"\/documentation\/discogsservice\/authtransport\/!=(_:_:)","role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport\/!=(_:_:)","abstract":[{"text":"Returns a Boolean value indicating whether two values are not equal.","type":"text"}],"fragments":[{"kind":"keyword","text":"static"},{"kind":"text","text":" "},{"kind":"keyword","text":"func"},{"kind":"text","text":" "},{"kind":"identifier","text":"!="},{"kind":"text","text":" "},{"kind":"text","text":"("},{"kind":"typeIdentifier","text":"Self"},{"kind":"text","text":", "},{"kind":"typeIdentifier","text":"Self"},{"kind":"text","text":") -> "},{"kind":"typeIdentifier","text":"Bool","preciseIdentifier":"s:Sb"}],"kind":"symbol","title":"!=(_:_:)","type":"topic"},"doc://DiscogsService/documentation/DiscogsService":{"title":"DiscogsService","abstract":[],"role":"collection","type":"topic","kind":"symbol","url":"\/documentation\/discogsservice","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService"}}}
|
||||
@@ -0,0 +1 @@
|
||||
{"kind":"article","topicSections":[{"anchor":"Operators","identifiers":["doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport\/!=(_:_:)"],"generated":true,"title":"Operators"}],"schemaVersion":{"major":0,"minor":3,"patch":0},"metadata":{"title":"Equatable Implementations","modules":[{"name":"DiscogsService"}],"role":"collectionGroup","roleHeading":"API Collection"},"sections":[],"variants":[{"paths":["\/documentation\/discogsservice\/authtransport\/equatable-implementations"],"traits":[{"interfaceLanguage":"swift"}]}],"hierarchy":{"paths":[["doc:\/\/DiscogsService\/documentation\/DiscogsService","doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport"]]},"identifier":{"interfaceLanguage":"swift","url":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport\/Equatable-Implementations"},"references":{"doc://DiscogsService/documentation/DiscogsService":{"title":"DiscogsService","abstract":[],"role":"collection","type":"topic","kind":"symbol","url":"\/documentation\/discogsservice","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService"},"doc://DiscogsService/documentation/DiscogsService/AuthTransport/!=(_:_:)":{"url":"\/documentation\/discogsservice\/authtransport\/!=(_:_:)","role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport\/!=(_:_:)","abstract":[{"text":"Returns a Boolean value indicating whether two values are not equal.","type":"text"}],"fragments":[{"kind":"keyword","text":"static"},{"kind":"text","text":" "},{"kind":"keyword","text":"func"},{"kind":"text","text":" "},{"kind":"identifier","text":"!="},{"kind":"text","text":" "},{"kind":"text","text":"("},{"kind":"typeIdentifier","text":"Self"},{"kind":"text","text":", "},{"kind":"typeIdentifier","text":"Self"},{"kind":"text","text":") -> "},{"kind":"typeIdentifier","text":"Bool","preciseIdentifier":"s:Sb"}],"kind":"symbol","title":"!=(_:_:)","type":"topic"},"doc://DiscogsService/documentation/DiscogsService/AuthTransport":{"url":"\/documentation\/discogsservice\/authtransport","role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport","abstract":[{"type":"text","text":"A representation of the available transport options to send credentials in authenticated requests."}],"fragments":[{"text":"enum","kind":"keyword"},{"text":" ","kind":"text"},{"text":"AuthTransport","kind":"identifier"}],"kind":"symbol","type":"topic","title":"AuthTransport","navigatorTitle":[{"text":"AuthTransport","kind":"identifier"}]}}}
|
||||
@@ -0,0 +1 @@
|
||||
{"primaryContentSections":[{"kind":"declarations","declarations":[{"platforms":["macOS"],"languages":["swift"],"tokens":[{"kind":"keyword","text":"case"},{"kind":"text","text":" "},{"kind":"identifier","text":"onHeader"}]}]},{"content":[{"anchor":"discussion","text":"Discussion","type":"heading","level":2},{"type":"paragraph","inlineContent":[{"type":"text","text":"This means that the header will be added to any existing header in a request, like this:"}]},{"syntax":"bash","code":["curl \"https:\/\/api.discogs.com\/database\/search?q=Slayer\" -H \"Authorization: Discogs key=foo123, secret=bar456\"","curl \"https:\/\/api.discogs.com\/database\/search?q=Slayer\" -H \"Authorization: Discogs token=abcxyz123456\""],"type":"codeListing"}],"kind":"content"}],"abstract":[{"text":"Authentication credential are sent in a request as an ","type":"text"},{"code":"Authentication","type":"codeVoice"},{"text":" header.","type":"text"}],"variants":[{"paths":["\/documentation\/discogsservice\/authtransport\/onheader"],"traits":[{"interfaceLanguage":"swift"}]}],"kind":"symbol","sections":[],"hierarchy":{"paths":[["doc:\/\/DiscogsService\/documentation\/DiscogsService","doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport"]]},"schemaVersion":{"minor":3,"patch":0,"major":0},"metadata":{"title":"AuthTransport.onHeader","modules":[{"name":"DiscogsService"}],"role":"symbol","fragments":[{"kind":"keyword","text":"case"},{"kind":"text","text":" "},{"kind":"identifier","text":"onHeader"}],"symbolKind":"case","roleHeading":"Case","externalID":"s:14DiscogsService13AuthTransportO8onHeaderyA2CmF"},"identifier":{"url":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport\/onHeader","interfaceLanguage":"swift"},"references":{"doc://DiscogsService/documentation/DiscogsService/AuthTransport":{"url":"\/documentation\/discogsservice\/authtransport","role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport","abstract":[{"type":"text","text":"A representation of the available transport options to send credentials in authenticated requests."}],"fragments":[{"text":"enum","kind":"keyword"},{"text":" ","kind":"text"},{"text":"AuthTransport","kind":"identifier"}],"kind":"symbol","type":"topic","title":"AuthTransport","navigatorTitle":[{"text":"AuthTransport","kind":"identifier"}]},"doc://DiscogsService/documentation/DiscogsService":{"title":"DiscogsService","abstract":[],"role":"collection","type":"topic","kind":"symbol","url":"\/documentation\/discogsservice","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService"},"doc://DiscogsService/documentation/DiscogsService/AuthTransport/onHeader":{"url":"\/documentation\/discogsservice\/authtransport\/onheader","kind":"symbol","role":"symbol","type":"topic","fragments":[{"text":"case","kind":"keyword"},{"text":" ","kind":"text"},{"text":"onHeader","kind":"identifier"}],"abstract":[{"type":"text","text":"Authentication credential are sent in a request as an "},{"type":"codeVoice","code":"Authentication"},{"type":"text","text":" header."}],"identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport\/onHeader","title":"AuthTransport.onHeader"}}}
|
||||
@@ -0,0 +1 @@
|
||||
{"identifier":{"url":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport\/onQuery","interfaceLanguage":"swift"},"hierarchy":{"paths":[["doc:\/\/DiscogsService\/documentation\/DiscogsService","doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport"]]},"variants":[{"paths":["\/documentation\/discogsservice\/authtransport\/onquery"],"traits":[{"interfaceLanguage":"swift"}]}],"primaryContentSections":[{"declarations":[{"languages":["swift"],"platforms":["macOS"],"tokens":[{"text":"case","kind":"keyword"},{"text":" ","kind":"text"},{"text":"onQuery","kind":"identifier"}]}],"kind":"declarations"},{"content":[{"anchor":"discussion","type":"heading","level":2,"text":"Discussion"},{"inlineContent":[{"text":"This means that the parameters will be injected into the query in a request, like this:","type":"text"}],"type":"paragraph"},{"code":["curl \"https:\/\/api.discogs.com\/database\/search?q=Slayer&key=foo123&secret=bar456\"","curl \"https:\/\/api.discogs.com\/database\/search?q=Slayer&token=abcxyz123456\""],"type":"codeListing","syntax":"bash"}],"kind":"content"}],"kind":"symbol","abstract":[{"type":"text","text":"Authentication credential are sent in a request as parameters in the query string."}],"sections":[],"metadata":{"symbolKind":"case","title":"AuthTransport.onQuery","externalID":"s:14DiscogsService13AuthTransportO7onQueryyA2CmF","roleHeading":"Case","role":"symbol","fragments":[{"text":"case","kind":"keyword"},{"text":" ","kind":"text"},{"text":"onQuery","kind":"identifier"}],"modules":[{"name":"DiscogsService"}]},"schemaVersion":{"major":0,"patch":0,"minor":3},"references":{"doc://DiscogsService/documentation/DiscogsService":{"title":"DiscogsService","abstract":[],"role":"collection","type":"topic","kind":"symbol","url":"\/documentation\/discogsservice","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService"},"doc://DiscogsService/documentation/DiscogsService/AuthTransport":{"url":"\/documentation\/discogsservice\/authtransport","role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport","abstract":[{"type":"text","text":"A representation of the available transport options to send credentials in authenticated requests."}],"fragments":[{"text":"enum","kind":"keyword"},{"text":" ","kind":"text"},{"text":"AuthTransport","kind":"identifier"}],"kind":"symbol","type":"topic","title":"AuthTransport","navigatorTitle":[{"text":"AuthTransport","kind":"identifier"}]},"doc://DiscogsService/documentation/DiscogsService/AuthTransport/onQuery":{"url":"\/documentation\/discogsservice\/authtransport\/onquery","role":"symbol","identifier":"doc:\/\/DiscogsService\/documentation\/DiscogsService\/AuthTransport\/onQuery","abstract":[{"type":"text","text":"Authentication credential are sent in a request as parameters in the query string."}],"fragments":[{"text":"case","kind":"keyword"},{"text":" ","kind":"text"},{"text":"onQuery","kind":"identifier"}],"kind":"symbol","type":"topic","title":"AuthTransport.onQuery"}}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user