7 Commits

Author SHA1 Message Date
javier 9a30b69561 Implemented the Authentication endpoints (#9)
This PR contains the work done to implement the *Authentication* endpoints of the Discogs API:
* GET `/oauth/request_token`
* POST `/oauth/access_token`
* GET `/oauth/identity`

Reviewed-on: #9
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
2025-10-13 17:55:48 +00:00
javier de5b4ff5d0 Implemented the missing Database endpoints (#8)
This PR contains the work done to declare the missing, non-GET endpoints of the *Database* section in the [Discogs API documentation](https://www.discogs.com/developers#page:database) into the `OpenAPI` specification document.

Furthermore, documentation extensions have been defined for the `APIProtocol` and `Client` types in the `DocC` documentation catalog to match the categories of these endpoint calls to the categories in the Discogs documentation.

Reviewed-on: #8
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
2025-10-13 15:20:20 +00:00
javier d01b60e6dd Added the Rate Limiting headers (#7)
This PR contains the work done to define the `RateLimit`, `RateLimitRemaining` and the `RateLimitUsed` heades into the Open API specification document, as well as including these headers as part of the response for every existing endpoint declared in the mentioned document.

In addition, the `openapi-generator-config` files was also changed to have a different naming strategy.

Reviewed-on: #7
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
2025-10-13 13:09:10 +00:00
javier 791ebf4f78 Implemented the User Agent middleware (#6)
This PR contains the work done to implement the `UserAgentMiddleware` middleware that includes user agent information into a header of the requests sent by the `Client` type, as defined in the [Discogs documentation](https://www.discogs.com/developers/#page:home,header:home-general-information). For this purpose, the `CamelCaseValidationRule`, `SemanticVersionValidationRule` and `URLValidationRule` types were implemented and integrated into the existing `ValidateInputUseCase` type.

Reviewed-on: #6
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
2025-10-13 00:54:17 +00:00
javier 24d703b967 Added input validation to the Authentication middleware (#5)
This PR contains the work done to improve the existing `AuthMiddleware` type to provide input validations with the `SecureValidationRule` validation rule and also, by generating the authentication information at initialization time.

Reviewed-on: #5
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
2025-10-12 19:33:45 +00:00
javier a1a649838c Implemented an input validation mechanism (#4)
This PR contains the work done to implement the `ValidateInputUseCase` use case and the `InputValidationRule` protocol, that is essential to define custom validation rules for inputs. In addition, the `NotEmptyValidationRule` and `NotNilValidationRule` rules have also been implemented.

Reviewed-on: #4
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
2025-10-12 13:25:25 +00:00
javier bfc9e67d38 Implemented the Authentication middleware (#3)
This PR contains the work done to implement the `AuthMiddleware` middleware, to authenticate the requests sent to the backend service, based on [their specifications](https://www.discogs.com/developers/#page:authentication).

In addition, some documentation has been added/updated and some boilerplate source code has been removed from the project.

Reviewed-on: #3
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
2025-10-11 07:39:52 +00:00
3141 changed files with 5611 additions and 1939 deletions
+36
View File
@@ -1 +1,37 @@
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Frock-n-code%2Fdiscogs-service%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/rock-n-code/discogs-service)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Frock-n-code%2Fdiscogs-service%2Fbadge%3Ftype%3Dplatforms)](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
+336 -12
View File
@@ -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
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
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
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
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
@@ -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
@@ -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
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
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
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