Skip to content

Commit

Permalink
Merge pull request #3 from Lucien/feat/optional-security-requirements
Browse files Browse the repository at this point in the history
Add support to optional Security Requirements and top-level inheritance yonaskolb#310
  • Loading branch information
arasan01_dev committed Apr 5, 2023
2 parents cb8f993 + 3782fe9 commit fd21a8f
Show file tree
Hide file tree
Showing 74 changed files with 224 additions and 83 deletions.
1 change: 1 addition & 0 deletions Sources/SwagGenKit/CodeFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ public class CodeFormatter {
var context: Context = [:]

context["name"] = securityRequirement.name
context["isRequired"] = securityRequirement.isRequired
context["scopes"] = securityRequirement.scopes
context["scope"] = securityRequirement.scopes.first

Expand Down
25 changes: 23 additions & 2 deletions Sources/Swagger/Operation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ public struct Operation {

extension Operation {

public init(path: String, method: Method, pathParameters: [PossibleReference<Parameter>], jsonDictionary: JSONDictionary) throws {
public init(path: String,
method: Method,
pathParameters: [PossibleReference<Parameter>],
jsonDictionary: JSONDictionary,
topLevelSecurityRequirements: [SecurityRequirement]?) throws {
json = jsonDictionary
self.path = path
self.method = method
Expand All @@ -57,7 +61,10 @@ extension Operation {

identifier = jsonDictionary.json(atKeyPath: "operationId")
tags = (jsonDictionary.json(atKeyPath: "tags")) ?? []
securityRequirements = jsonDictionary.json(atKeyPath: "security")
securityRequirements = type(of: self).getSecurityRequirements(
from: jsonDictionary,
topLevelSecurityRequirements: topLevelSecurityRequirements
)

let allResponses: [String: PossibleReference<Response>] = try jsonDictionary.json(atKeyPath: "responses")
var mappedResponses: [OperationResponse] = []
Expand Down Expand Up @@ -88,4 +95,18 @@ extension Operation {

deprecated = (jsonDictionary.json(atKeyPath: "deprecated")) ?? false
}

private static func getSecurityRequirements(
from jsonDictionary: JSONDictionary,
topLevelSecurityRequirements: [SecurityRequirement]?
) -> [SecurityRequirement]? {
// Even an empty list of operation security requirements
// can remove top-level security requirements.
guard let securityRequirements: [SecurityRequirement] =
jsonDictionary.json(atKeyPath: "security") else {
return topLevelSecurityRequirements
}

return securityRequirements.updatingRequiredFlag()
}
}
11 changes: 9 additions & 2 deletions Sources/Swagger/Path.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@ public struct Path {

extension Path: NamedMappable {

public init(name: String, jsonDictionary: JSONDictionary) throws {
public init(name: String,
jsonDictionary: JSONDictionary,
topLevelSecurityRequirements: [SecurityRequirement]?) throws {
path = name
parameters = (jsonDictionary.json(atKeyPath: "parameters")) ?? []

var mappedOperations: [Operation] = []
for (key, value) in jsonDictionary {
if let method = Operation.Method(rawValue: key) {
if let json = value as? [String: Any] {
let operation = try Operation(path: path, method: method, pathParameters: parameters, jsonDictionary: json)
let operation = try Operation(
path: path,
method: method,
pathParameters: parameters,
jsonDictionary: json,
topLevelSecurityRequirements: topLevelSecurityRequirements)
mappedOperations.append(operation)
}
}
Expand Down
29 changes: 27 additions & 2 deletions Sources/Swagger/Security/SecurityRequirement.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
import Foundation
import JSONUtilities

public struct SecurityRequirement: JSONObjectConvertible {
public struct SecurityRequirement: Equatable {
public let name: String
public let isRequired: Bool
public let scopes: [String]

public static let optionalMarker: SecurityRequirement = {
.init(name: "optional-marker", isRequired: false, scopes: [])
}()
}

extension SecurityRequirement: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
name = jsonDictionary.keys.first!
guard let firstKey = jsonDictionary.keys.first else {
self = .optionalMarker
return
}
name = firstKey
isRequired = true
scopes = try jsonDictionary.json(atKeyPath: .key(name))
}
}

extension Array where Element == SecurityRequirement {
func updatingRequiredFlag() -> [Element] {
// Check if there is an optional security requirement marker
guard (contains(where: { $0 == .optionalMarker })) else {
return self
}
// Remove the optional marker and set all security requirements as optional
return self
.drop { $0 == .optionalMarker }
.map { .init(name: $0.name, isRequired: false, scopes: $0.scopes) }
}
}
33 changes: 28 additions & 5 deletions Sources/Swagger/SwaggerSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ public enum TransferScheme: String {
}

public protocol NamedMappable {
init(name: String, jsonDictionary: JSONDictionary) throws
init(name: String,
jsonDictionary: JSONDictionary,
topLevelSecurityRequirements: [SecurityRequirement]?) throws
}

extension SwaggerSpec {
Expand Down Expand Up @@ -69,12 +71,20 @@ extension SwaggerSpec: JSONObjectConvertible {

public init(jsonDictionary: JSONDictionary) throws {

func decodeNamed<T: NamedMappable>(jsonDictionary: JSONDictionary, key: String) throws -> [T] {
func decodeNamed<T: NamedMappable>(
jsonDictionary: JSONDictionary,
key: String,
topLevelSecurityRequirements: [SecurityRequirement]?
) throws -> [T] {
var values: [T] = []
if let dictionary = jsonDictionary[key] as? [String: Any] {
for (key, value) in dictionary {
if let dictionary = value as? [String: Any] {
let value = try T(name: key, jsonDictionary: dictionary)
let value = try T(
name: key,
jsonDictionary: dictionary,
topLevelSecurityRequirements: topLevelSecurityRequirements
)
values.append(value)
}
}
Expand All @@ -91,13 +101,15 @@ extension SwaggerSpec: JSONObjectConvertible {

info = try jsonDictionary.json(atKeyPath: "info")
servers = jsonDictionary.json(atKeyPath: "servers") ?? []
securityRequirements = jsonDictionary.json(atKeyPath: "security")
securityRequirements = type(of: self).getSecurityRequirements(from: jsonDictionary)
if jsonDictionary["components"] != nil {
components = try jsonDictionary.json(atKeyPath: "components")
} else {
components = Components()
}
paths = try decodeNamed(jsonDictionary: jsonDictionary, key: "paths")
paths = try decodeNamed(jsonDictionary: jsonDictionary,
key: "paths",
topLevelSecurityRequirements: securityRequirements)
operations = paths.reduce([]) { $0 + $1.operations }
.sorted(by: { (lhs, rhs) -> Bool in
if lhs.path == rhs.path {
Expand All @@ -110,4 +122,15 @@ extension SwaggerSpec: JSONObjectConvertible {
try resolver.resolve()
self = resolver.spec
}

private static func getSecurityRequirements(
from jsonDictionary: JSONDictionary
) -> [SecurityRequirement]? {
guard let securityRequirements: [SecurityRequirement] =
jsonDictionary.json(atKeyPath: "security") else {
return nil
}

return securityRequirements.updatingRequiredFlag()
}
}
4 changes: 3 additions & 1 deletion Specs/Petstore/generated/Swift/Sources/APIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ extension APIService: CustomStringConvertible {

public struct SecurityRequirement {
public let type: String
public let isRequired: Bool
public let scopes: [String]

public init(type: String, scopes: [String]) {
public init(type: String, isRequired: Bool, scopes: [String]) {
self.type = type
self.isRequired = isRequired
self.scopes = scopes
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension Petstore.Pets {
/** Create a pet */
public enum CreatePets {

public static let service = APIService<Response>(id: "createPets", tag: "pets", method: "POST", path: "/pets", hasBody: false, securityRequirements: [SecurityRequirement(type: "petstore_auth", scopes: ["write:pets", "read:pets"])])
public static let service = APIService<Response>(id: "createPets", tag: "pets", method: "POST", path: "/pets", hasBody: false, securityRequirements: [SecurityRequirement(type: "petstore_auth", isRequired: true, scopes: ["write:pets", "read:pets"])])

public final class Request: APIRequest<Response> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension Petstore.Pets {
/** List all pets */
public enum ListPets {

public static let service = APIService<Response>(id: "listPets", tag: "pets", method: "GET", path: "/pets", hasBody: false, securityRequirements: [])
public static let service = APIService<Response>(id: "listPets", tag: "pets", method: "GET", path: "/pets", hasBody: false, securityRequirements: [SecurityRequirement(type: "petstore_auth", isRequired: false, scopes: ["read:pets"])])

public final class Request: APIRequest<Response> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension Petstore.Pets {
/** Info for a specific pet */
public enum ShowPetById {

public static let service = APIService<Response>(id: "showPetById", tag: "pets", method: "GET", path: "/pets/{petId}", hasBody: false, securityRequirements: [])
public static let service = APIService<Response>(id: "showPetById", tag: "pets", method: "GET", path: "/pets/{petId}", hasBody: false, securityRequirements: [SecurityRequirement(type: "petstore_auth", isRequired: true, scopes: ["read:pets"])])

public final class Request: APIRequest<Response> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension Petstore.Pets {
/** Updates a pet in the store with form data */
public enum UpdatePetWithForm {

public static let service = APIService<Response>(id: "updatePetWithForm", tag: "pets", method: "POST", path: "/pets/{petId}", hasBody: true, securityRequirements: [SecurityRequirement(type: "petstore_auth", scopes: ["write:pets", "read:pets"])])
public static let service = APIService<Response>(id: "updatePetWithForm", tag: "pets", method: "POST", path: "/pets/{petId}", hasBody: true, securityRequirements: [])

public final class Request: APIRequest<Response> {

Expand Down
13 changes: 9 additions & 4 deletions Specs/Petstore/spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ servers:
- x-name: Prod
description: Prod environment
url: http://petstore.swagger.io/v1
security:
- petstore_auth:
- read:pets
paths:
/pets:
get:
Expand Down Expand Up @@ -54,6 +57,10 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
security:
- {}
- petstore_auth:
- read:pets
post:
summary: Create a pet
operationId: createPets
Expand Down Expand Up @@ -129,9 +136,7 @@ paths:
"405":
description: Invalid input
security:
- petstore_auth:
- write:pets
- read:pets
- []
components:
securitySchemes:
petstore_auth:
Expand Down Expand Up @@ -174,4 +179,4 @@ components:
type: integer
format: int32
message:
type: string
type: string
4 changes: 3 additions & 1 deletion Specs/PetstoreTest/generated/Swift/Sources/APIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ extension APIService: CustomStringConvertible {

public struct SecurityRequirement {
public let type: String
public let isRequired: Bool
public let scopes: [String]

public init(type: String, scopes: [String]) {
public init(type: String, isRequired: Bool, scopes: [String]) {
self.type = type
self.isRequired = isRequired
self.scopes = scopes
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension PetstoreTest.Fake {
*/
public enum TestEndpointParameters {

public static let service = APIService<Response>(id: "testEndpointParameters", tag: "fake", method: "POST", path: "/fake", hasBody: true, isUpload: true, securityRequirements: [SecurityRequirement(type: "http_basic_test", scopes: [])])
public static let service = APIService<Response>(id: "testEndpointParameters", tag: "fake", method: "POST", path: "/fake", hasBody: true, isUpload: true, securityRequirements: [SecurityRequirement(type: "http_basic_test", isRequired: true, scopes: [])])

public final class Request: APIRequest<Response> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension PetstoreTest.Pet {
/** Add a new pet to the store */
public enum AddPet {

public static let service = APIService<Response>(id: "addPet", tag: "pet", method: "POST", path: "/pet", hasBody: true, securityRequirements: [SecurityRequirement(type: "petstore_auth", scopes: ["write:pets", "read:pets"])])
public static let service = APIService<Response>(id: "addPet", tag: "pet", method: "POST", path: "/pet", hasBody: true, securityRequirements: [SecurityRequirement(type: "petstore_auth", isRequired: true, scopes: ["write:pets", "read:pets"])])

public final class Request: APIRequest<Response> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension PetstoreTest.Pet {
/** Deletes a pet */
public enum DeletePet {

public static let service = APIService<Response>(id: "deletePet", tag: "pet", method: "DELETE", path: "/pet/{petId}", hasBody: false, securityRequirements: [SecurityRequirement(type: "petstore_auth", scopes: ["write:pets", "read:pets"])])
public static let service = APIService<Response>(id: "deletePet", tag: "pet", method: "DELETE", path: "/pet/{petId}", hasBody: false, securityRequirements: [SecurityRequirement(type: "petstore_auth", isRequired: true, scopes: ["write:pets", "read:pets"])])

public final class Request: APIRequest<Response> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension PetstoreTest.Pet {
*/
public enum FindPetsByStatus {

public static let service = APIService<Response>(id: "findPetsByStatus", tag: "pet", method: "GET", path: "/pet/findByStatus", hasBody: false, securityRequirements: [SecurityRequirement(type: "petstore_auth", scopes: ["write:pets", "read:pets"])])
public static let service = APIService<Response>(id: "findPetsByStatus", tag: "pet", method: "GET", path: "/pet/findByStatus", hasBody: false, securityRequirements: [SecurityRequirement(type: "petstore_auth", isRequired: true, scopes: ["write:pets", "read:pets"])])

/** Status values that need to be considered for filter */
public enum Status: String, Codable, Equatable, CaseIterable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension PetstoreTest.Pet {
*/
public enum FindPetsByTags {

public static let service = APIService<Response>(id: "findPetsByTags", tag: "pet", method: "GET", path: "/pet/findByTags", hasBody: false, securityRequirements: [SecurityRequirement(type: "petstore_auth", scopes: ["write:pets", "read:pets"])])
public static let service = APIService<Response>(id: "findPetsByTags", tag: "pet", method: "GET", path: "/pet/findByTags", hasBody: false, securityRequirements: [SecurityRequirement(type: "petstore_auth", isRequired: true, scopes: ["write:pets", "read:pets"])])

public final class Request: APIRequest<Response> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension PetstoreTest.Pet {
*/
public enum GetPetById {

public static let service = APIService<Response>(id: "getPetById", tag: "pet", method: "GET", path: "/pet/{petId}", hasBody: false, securityRequirements: [SecurityRequirement(type: "api_key", scopes: [])])
public static let service = APIService<Response>(id: "getPetById", tag: "pet", method: "GET", path: "/pet/{petId}", hasBody: false, securityRequirements: [SecurityRequirement(type: "api_key", isRequired: true, scopes: [])])

public final class Request: APIRequest<Response> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension PetstoreTest.Pet {
/** Update an existing pet */
public enum UpdatePet {

public static let service = APIService<Response>(id: "updatePet", tag: "pet", method: "PUT", path: "/pet", hasBody: true, securityRequirements: [SecurityRequirement(type: "petstore_auth", scopes: ["write:pets", "read:pets"])])
public static let service = APIService<Response>(id: "updatePet", tag: "pet", method: "PUT", path: "/pet", hasBody: true, securityRequirements: [SecurityRequirement(type: "petstore_auth", isRequired: true, scopes: ["write:pets", "read:pets"])])

public final class Request: APIRequest<Response> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension PetstoreTest.Pet {
/** Updates a pet in the store with form data */
public enum UpdatePetWithForm {

public static let service = APIService<Response>(id: "updatePetWithForm", tag: "pet", method: "POST", path: "/pet/{petId}", hasBody: true, securityRequirements: [SecurityRequirement(type: "petstore_auth", scopes: ["write:pets", "read:pets"])])
public static let service = APIService<Response>(id: "updatePetWithForm", tag: "pet", method: "POST", path: "/pet/{petId}", hasBody: true, securityRequirements: [SecurityRequirement(type: "petstore_auth", isRequired: true, scopes: ["write:pets", "read:pets"])])

public final class Request: APIRequest<Response> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension PetstoreTest.Pet {
/** uploads an image */
public enum UploadFile {

public static let service = APIService<Response>(id: "uploadFile", tag: "pet", method: "POST", path: "/pet/{petId}/uploadImage", hasBody: true, isUpload: true, securityRequirements: [SecurityRequirement(type: "petstore_auth", scopes: ["write:pets", "read:pets"])])
public static let service = APIService<Response>(id: "uploadFile", tag: "pet", method: "POST", path: "/pet/{petId}/uploadImage", hasBody: true, isUpload: true, securityRequirements: [SecurityRequirement(type: "petstore_auth", isRequired: true, scopes: ["write:pets", "read:pets"])])

public final class Request: APIRequest<Response> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension PetstoreTest.Store {
*/
public enum GetInventory {

public static let service = APIService<Response>(id: "getInventory", tag: "store", method: "GET", path: "/store/inventory", hasBody: false, securityRequirements: [SecurityRequirement(type: "api_key", scopes: [])])
public static let service = APIService<Response>(id: "getInventory", tag: "store", method: "GET", path: "/store/inventory", hasBody: false, securityRequirements: [SecurityRequirement(type: "api_key", isRequired: true, scopes: [])])

public final class Request: APIRequest<Response> {

Expand Down
4 changes: 3 additions & 1 deletion Specs/Rocket/generated/Swift/Sources/APIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ extension APIService: CustomStringConvertible {

public struct SecurityRequirement {
public let type: String
public let isRequired: Bool
public let scopes: [String]

public init(type: String, scopes: [String]) {
public init(type: String, isRequired: Bool, scopes: [String]) {
self.type = type
self.isRequired = isRequired
self.scopes = scopes
}
}
Expand Down
Loading

0 comments on commit fd21a8f

Please sign in to comment.