├── Snaps
├── Home1.png
├── Home2.png
└── Screenshot 2021-04-07 at 7.06.52 PM.png
├── Patchman
├── Assets.xcassets
│ ├── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Patchman.entitlements
├── Info.plist
├── PatchmanApp.swift
├── Patchman.swift
└── ContentView.swift
├── Patchman.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── praneets.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
├── xcuserdata
│ └── praneets.xcuserdatad
│ │ ├── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
├── README.md
└── LICENSE
/Snaps/Home1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/praneet-suresh/Patchman/HEAD/Snaps/Home1.png
--------------------------------------------------------------------------------
/Snaps/Home2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/praneet-suresh/Patchman/HEAD/Snaps/Home2.png
--------------------------------------------------------------------------------
/Patchman/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Patchman/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Snaps/Screenshot 2021-04-07 at 7.06.52 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/praneet-suresh/Patchman/HEAD/Snaps/Screenshot 2021-04-07 at 7.06.52 PM.png
--------------------------------------------------------------------------------
/Patchman.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Patchman/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Patchman.xcodeproj/xcuserdata/praneets.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/Patchman.xcodeproj/project.xcworkspace/xcuserdata/praneets.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/praneet-suresh/Patchman/HEAD/Patchman.xcodeproj/project.xcworkspace/xcuserdata/praneets.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Patchman.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Patchman.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "CodeViewer",
6 | "repositoryURL": "https://github.com/dwarvesf/CodeViewer",
7 | "state": {
8 | "branch": null,
9 | "revision": "44bd04af81046ce65a4e38dc97a9dd3d387f069e",
10 | "version": "1.2.4"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Patchman.xcodeproj/xcuserdata/praneets.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Patchman.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Patchman/Patchman.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.assets.movies.read-write
8 |
9 | com.apple.security.assets.music.read-write
10 |
11 | com.apple.security.assets.pictures.read-write
12 |
13 | com.apple.security.files.downloads.read-write
14 |
15 | com.apple.security.files.user-selected.read-write
16 |
17 | com.apple.security.network.client
18 |
19 | com.apple.security.network.server
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Patchman
2 | A macOS application to test APIs with HTTP methods (Decluttered Postman), built with SwiftUI.
3 | ## Features:
4 | 1. Supports the GET, POST, PUT, PATCH, DELETE methods
5 | 2. Easy to use / adaptive UI
6 | 3. Supports bulk requests (takes in all request bodies in the form of a CSV)
7 | 4. Can save request profiles as well as presets for commonly used headers and request body fields
8 | 5. Can save / open request profiles on / from disk to share with your team!
9 | ## JSON Editor support for request body / headers
10 | 
11 | ## Easy to use UI
12 | 
13 | ## Supports bulk requests
14 | 
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Praneet
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Patchman/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x",
6 | "size" : "16x16"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "1x",
16 | "size" : "32x32"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "2x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "1x",
26 | "size" : "128x128"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "1x",
36 | "size" : "256x256"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "2x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "1x",
46 | "size" : "512x512"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "2x",
51 | "size" : "512x512"
52 | }
53 | ],
54 | "info" : {
55 | "author" : "xcode",
56 | "version" : 1
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Patchman/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 2.0.1
19 | CFBundleVersion
20 | 1
21 | LSMinimumSystemVersion
22 | $(MACOSX_DEPLOYMENT_TARGET)
23 | UTExportedTypeDeclarations
24 |
25 |
26 | UTTypeDescription
27 |
28 | UTTypeIcons
29 |
30 | UTTypeIconText
31 |
32 |
33 | UTTypeIdentifier
34 |
35 | UTTypeTagSpecification
36 |
37 | public.filename-extension
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/Patchman/PatchmanApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PatchmanApp.swift
3 | // Patchman
4 | //
5 | // Created by Praneet S on 16/03/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | func openRequestProfile() -> String? {
11 | let dialog = NSOpenPanel();
12 |
13 | dialog.title = "Choose a CSV file";
14 | dialog.showsResizeIndicator = true;
15 | dialog.showsHiddenFiles = false;
16 | dialog.allowsMultipleSelection = false;
17 | dialog.canChooseDirectories = false;
18 | dialog.allowedFileTypes = ["patchman"];
19 |
20 | if (dialog.runModal() == NSApplication.ModalResponse.OK) {
21 | let result = dialog.url
22 | if (result != nil) {
23 | let path: String = result!.path
24 | return path
25 | }
26 | } else {
27 | return nil
28 | }
29 | return nil
30 | }
31 |
32 | @main
33 | struct PatchmanApp: App {
34 | var body: some Scene {
35 | WindowGroup {
36 | ContentView()
37 | }
38 | .commands(content: {
39 | CommandMenu("Quickies", content: {
40 | Button("Open", action: {
41 | let requestProfilePath = openRequestProfile()
42 | guard let requestProfileURL = requestProfilePath else {
43 | return
44 | }
45 | let profile = try? JSONDecoder().decode(Profile.self, from: Data(contentsOf: URL(fileURLWithPath: requestProfileURL), options: []))
46 | guard let profileUnwrapped = profile else {
47 | return
48 | }
49 | profileUnwrapped.save()
50 | Defaults.shared.profiles.append(profileUnwrapped)
51 | })
52 | })
53 | })
54 | .windowStyle(HiddenTitleBarWindowStyle())
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Patchman/Patchman.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Patchman.swift
3 | // Patchman
4 | //
5 | // Created by Praneet S on 16/03/21.
6 | //
7 |
8 | import Foundation
9 | import Cocoa
10 |
11 | class Defaults : ObservableObject {
12 | private init() {
13 | profiles = retreiveDefaultProfiles()
14 | presets = retreiveDefaultPresets()
15 | }
16 | public static var shared: Defaults = Defaults()
17 | @Published var profiles: [Profile] = []
18 | @Published var presets: [Preset] = []
19 | }
20 |
21 | func retreiveDefaultPresets() -> [Preset] {
22 | if let presets = UserDefaults.standard.object(forKey: "presets") as? Data {
23 | let decoder = JSONDecoder()
24 | if let presetsArray = try? decoder.decode([Preset].self, from: presets) {
25 | return presetsArray
26 | }
27 | }
28 | return []
29 | }
30 |
31 | func retreiveDefaultProfiles() -> [Profile] {
32 | if let profiles = UserDefaults.standard.object(forKey: "profiles") as? Data {
33 | let decoder = JSONDecoder()
34 | if let profilesArray = try? decoder.decode([Profile].self, from: profiles) {
35 | return profilesArray
36 | }
37 | }
38 | return []
39 | }
40 |
41 | struct Preset: Codable {
42 | let presetType: Int
43 | let key: String
44 | let value: String
45 | let presetName: String
46 |
47 | func save() {
48 | let encoder = JSONEncoder()
49 | if let encoded = try? encoder.encode([self]) {
50 | let defaults = UserDefaults.standard
51 | if let presets = defaults.object(forKey: "presets") as? Data {
52 | let decoder = JSONDecoder()
53 | if var presetsArray = try? decoder.decode([Preset].self, from: presets) {
54 | presetsArray.append(self)
55 | Defaults.shared.presets = presetsArray
56 | if let presetsArrayObj = try? encoder.encode(presetsArray) {
57 | defaults.set(presetsArrayObj, forKey: "presets")
58 | }
59 | }
60 | } else {
61 | defaults.set(encoded, forKey: "presets")
62 | }
63 | }
64 | }
65 | }
66 |
67 | enum CodingKeys: CodingKey {
68 | case string
69 | case int
70 | case double
71 | case bool
72 | case object
73 | case array
74 | }
75 |
76 | public enum JSONValue: Codable {
77 |
78 | public func encode(to encoder: Encoder) throws {
79 | var container = encoder.container(keyedBy: CodingKeys.self)
80 | switch self {
81 | case .string(let str):
82 | try container.encode(str, forKey: .string)
83 | case .int(let int):
84 | try container.encode(int, forKey: .int)
85 | case .double(let dbl):
86 | try container.encode(dbl, forKey: .double)
87 | case .bool(let bool):
88 | try container.encode(bool, forKey: .bool)
89 | case .object(let obj):
90 | try container.encode(obj, forKey: .object)
91 | case .array(let array):
92 | try container.encode(array, forKey: .array)
93 | }
94 | }
95 |
96 | case string(String)
97 | case int(Int)
98 | case double(Double)
99 | case bool(Bool)
100 | case object([String: JSONValue])
101 | case array([JSONValue])
102 |
103 | public init(from decoder: Decoder) throws {
104 | let container = try decoder.singleValueContainer()
105 | if let value = try? container.decode(String.self) {
106 | self = .string(value)
107 | } else if let value = try? container.decode(Int.self) {
108 | self = .int(value)
109 | } else if let value = try? container.decode(Double.self) {
110 | self = .double(value)
111 | } else if let value = try? container.decode(Bool.self) {
112 | self = .bool(value)
113 | } else if let value = try? container.decode([String: JSONValue].self) {
114 | self = .object(value)
115 | } else if let value = try? container.decode([JSONValue].self) {
116 | self = .array(value)
117 | } else {
118 | throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: container.codingPath, debugDescription: "Not a JSON"))
119 | }
120 | }
121 | }
122 |
123 | struct Profile: Codable {
124 | let profileName: String
125 | let method: Int
126 | let url: String
127 | let headers: [String : String]
128 | let requestBody: [String : JSONValue]
129 | let isHeadersEnabled: Bool
130 | let isBulkRequest: Bool
131 | let bulkRequestBody: [[String : JSONValue]]
132 |
133 | func saveOnDisk() {
134 | let encodedProfile = try? JSONEncoder().encode(self)
135 | let savePanel = NSSavePanel()
136 | savePanel.canCreateDirectories = true
137 | savePanel.showsTagField = false
138 | savePanel.nameFieldStringValue = "\(self.profileName)_request.patchman"
139 | savePanel.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.modalPanelWindow)))
140 | savePanel.begin { (result) in
141 | if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
142 | guard let url = savePanel.url else {
143 | return
144 | }
145 | try? encodedProfile?.write(to: url)
146 | }
147 | }
148 | }
149 |
150 | func save() {
151 | let encoder = JSONEncoder()
152 | if let encoded = try? encoder.encode([self]) {
153 | let defaults = UserDefaults.standard
154 | if let profiles = defaults.object(forKey: "profiles") as? Data {
155 | let decoder = JSONDecoder()
156 | if var profilesArray = try? decoder.decode([Profile].self, from: profiles) {
157 | profilesArray.append(self)
158 | Defaults.shared.profiles = profilesArray
159 | if let profilesArrayObj = try? encoder.encode(profilesArray) {
160 | defaults.set(profilesArrayObj, forKey: "profiles")
161 | }
162 | }
163 | } else {
164 | defaults.set(encoded, forKey: "profiles")
165 | }
166 | }
167 | }
168 |
169 | }
170 |
171 | enum RequestMethod: String, CaseIterable {
172 | case GET = "GET"
173 | case POST = "POST"
174 | case PUT = "PUT"
175 | case PATCH = "PATCH"
176 | case DELETE = "DELETE"
177 | }
178 |
179 | enum cachePolicies: String, CaseIterable {
180 | case reloadIgnoringLocalAndRemoteCacheData = "Reload ignoring local and remote cache data"
181 | case reloadIgnoringLocalCacheData = "Reload ignoring local cache data"
182 | case reloadRevalidatingCacheData = "Reload revalidating cache data"
183 | case returnCacheDataDontLoad = "Return cache data don't load"
184 | case returnCacheDataElseLoad = "Return cache data else load"
185 | case useProtocolCachePolicy = "Use protocol cache policy"
186 | case reloadIgnoringCacheData = "Reload ignoring cache data"
187 | }
188 |
189 | struct Response{
190 | let response: Data
191 | let responseStatus: HTTPURLResponse?
192 | }
193 |
194 | class Request {
195 | public var url: URL?
196 | public var method: RequestMethod
197 | public var requestPolicy: URLRequest.CachePolicy
198 | public var requestBody: [String : Any]?
199 | public var requestHeaders: [String : String]?
200 |
201 | init(url: String, method: RequestMethod, cachingPolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, requestBody: [String : Any], requestHeaders: Dictionary) {
202 | self.url = URL(string: url)
203 | self.method = method
204 | self.requestPolicy = cachingPolicy
205 | if !requestBody.isEmpty {
206 | self.requestBody = requestBody
207 | }
208 | self.requestHeaders = requestHeaders
209 | }
210 |
211 | func executeRequest() -> Response {
212 | guard let instanceURL = url else { return Response(response: "Invalid URL".data(using: .utf8)!, responseStatus: nil) }
213 | let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
214 | var request = URLRequest(url: instanceURL, cachePolicy: requestPolicy)
215 | request.httpMethod = self.method.rawValue
216 | if let requestBody = self.requestBody {
217 | let jsonData = try? JSONSerialization.data(withJSONObject: requestBody)
218 | request.httpBody = jsonData
219 | }
220 | request.allHTTPHeaderFields = self.requestHeaders
221 | var responseData: Data?
222 | var responseStatus: HTTPURLResponse?
223 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
224 | if let data = data {
225 | responseData = data
226 | } else {
227 | responseData = error?.localizedDescription.data(using: .utf8)
228 | }
229 | if let response = response {
230 | responseStatus = response as? HTTPURLResponse
231 | }
232 | semaphore.signal()
233 | }
234 | task.resume()
235 | semaphore.wait()
236 | return Response(response: responseData ?? "Unknown error".data(using: .utf8)!, responseStatus: responseStatus)
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/Patchman.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | B780945F26004E8C00AEB870 /* PatchmanApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B780945E26004E8C00AEB870 /* PatchmanApp.swift */; };
11 | B780946126004E8C00AEB870 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B780946026004E8C00AEB870 /* ContentView.swift */; };
12 | B780946326004E8D00AEB870 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B780946226004E8D00AEB870 /* Assets.xcassets */; };
13 | B780946626004E8D00AEB870 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B780946526004E8D00AEB870 /* Preview Assets.xcassets */; };
14 | B780947026004EA200AEB870 /* Patchman.swift in Sources */ = {isa = PBXBuildFile; fileRef = B780946F26004EA200AEB870 /* Patchman.swift */; };
15 | B7BBE58A2609B4160087E461 /* CodeViewer in Frameworks */ = {isa = PBXBuildFile; productRef = B7BBE5892609B4160087E461 /* CodeViewer */; };
16 | /* End PBXBuildFile section */
17 |
18 | /* Begin PBXFileReference section */
19 | B780945B26004E8C00AEB870 /* Patchman.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Patchman.app; sourceTree = BUILT_PRODUCTS_DIR; };
20 | B780945E26004E8C00AEB870 /* PatchmanApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchmanApp.swift; sourceTree = ""; };
21 | B780946026004E8C00AEB870 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
22 | B780946226004E8D00AEB870 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
23 | B780946526004E8D00AEB870 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
24 | B780946726004E8D00AEB870 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
25 | B780946826004E8D00AEB870 /* Patchman.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Patchman.entitlements; sourceTree = ""; };
26 | B780946F26004EA200AEB870 /* Patchman.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patchman.swift; sourceTree = ""; };
27 | /* End PBXFileReference section */
28 |
29 | /* Begin PBXFrameworksBuildPhase section */
30 | B780945826004E8C00AEB870 /* Frameworks */ = {
31 | isa = PBXFrameworksBuildPhase;
32 | buildActionMask = 2147483647;
33 | files = (
34 | B7BBE58A2609B4160087E461 /* CodeViewer in Frameworks */,
35 | );
36 | runOnlyForDeploymentPostprocessing = 0;
37 | };
38 | /* End PBXFrameworksBuildPhase section */
39 |
40 | /* Begin PBXGroup section */
41 | B780945226004E8C00AEB870 = {
42 | isa = PBXGroup;
43 | children = (
44 | B780945D26004E8C00AEB870 /* Patchman */,
45 | B780945C26004E8C00AEB870 /* Products */,
46 | );
47 | sourceTree = "";
48 | };
49 | B780945C26004E8C00AEB870 /* Products */ = {
50 | isa = PBXGroup;
51 | children = (
52 | B780945B26004E8C00AEB870 /* Patchman.app */,
53 | );
54 | name = Products;
55 | sourceTree = "";
56 | };
57 | B780945D26004E8C00AEB870 /* Patchman */ = {
58 | isa = PBXGroup;
59 | children = (
60 | B780945E26004E8C00AEB870 /* PatchmanApp.swift */,
61 | B780946026004E8C00AEB870 /* ContentView.swift */,
62 | B780946226004E8D00AEB870 /* Assets.xcassets */,
63 | B780946726004E8D00AEB870 /* Info.plist */,
64 | B780946826004E8D00AEB870 /* Patchman.entitlements */,
65 | B780946426004E8D00AEB870 /* Preview Content */,
66 | B780946F26004EA200AEB870 /* Patchman.swift */,
67 | );
68 | path = Patchman;
69 | sourceTree = "";
70 | };
71 | B780946426004E8D00AEB870 /* Preview Content */ = {
72 | isa = PBXGroup;
73 | children = (
74 | B780946526004E8D00AEB870 /* Preview Assets.xcassets */,
75 | );
76 | path = "Preview Content";
77 | sourceTree = "";
78 | };
79 | /* End PBXGroup section */
80 |
81 | /* Begin PBXNativeTarget section */
82 | B780945A26004E8C00AEB870 /* Patchman */ = {
83 | isa = PBXNativeTarget;
84 | buildConfigurationList = B780946B26004E8D00AEB870 /* Build configuration list for PBXNativeTarget "Patchman" */;
85 | buildPhases = (
86 | B780945726004E8C00AEB870 /* Sources */,
87 | B780945826004E8C00AEB870 /* Frameworks */,
88 | B780945926004E8C00AEB870 /* Resources */,
89 | );
90 | buildRules = (
91 | );
92 | dependencies = (
93 | );
94 | name = Patchman;
95 | packageProductDependencies = (
96 | B7BBE5892609B4160087E461 /* CodeViewer */,
97 | );
98 | productName = Patchman;
99 | productReference = B780945B26004E8C00AEB870 /* Patchman.app */;
100 | productType = "com.apple.product-type.application";
101 | };
102 | /* End PBXNativeTarget section */
103 |
104 | /* Begin PBXProject section */
105 | B780945326004E8C00AEB870 /* Project object */ = {
106 | isa = PBXProject;
107 | attributes = {
108 | LastSwiftUpdateCheck = 1230;
109 | LastUpgradeCheck = 1230;
110 | TargetAttributes = {
111 | B780945A26004E8C00AEB870 = {
112 | CreatedOnToolsVersion = 12.3;
113 | };
114 | };
115 | };
116 | buildConfigurationList = B780945626004E8C00AEB870 /* Build configuration list for PBXProject "Patchman" */;
117 | compatibilityVersion = "Xcode 9.3";
118 | developmentRegion = en;
119 | hasScannedForEncodings = 0;
120 | knownRegions = (
121 | en,
122 | Base,
123 | );
124 | mainGroup = B780945226004E8C00AEB870;
125 | packageReferences = (
126 | B7BBE5882609B4160087E461 /* XCRemoteSwiftPackageReference "CodeViewer" */,
127 | );
128 | productRefGroup = B780945C26004E8C00AEB870 /* Products */;
129 | projectDirPath = "";
130 | projectRoot = "";
131 | targets = (
132 | B780945A26004E8C00AEB870 /* Patchman */,
133 | );
134 | };
135 | /* End PBXProject section */
136 |
137 | /* Begin PBXResourcesBuildPhase section */
138 | B780945926004E8C00AEB870 /* Resources */ = {
139 | isa = PBXResourcesBuildPhase;
140 | buildActionMask = 2147483647;
141 | files = (
142 | B780946626004E8D00AEB870 /* Preview Assets.xcassets in Resources */,
143 | B780946326004E8D00AEB870 /* Assets.xcassets in Resources */,
144 | );
145 | runOnlyForDeploymentPostprocessing = 0;
146 | };
147 | /* End PBXResourcesBuildPhase section */
148 |
149 | /* Begin PBXSourcesBuildPhase section */
150 | B780945726004E8C00AEB870 /* Sources */ = {
151 | isa = PBXSourcesBuildPhase;
152 | buildActionMask = 2147483647;
153 | files = (
154 | B780947026004EA200AEB870 /* Patchman.swift in Sources */,
155 | B780946126004E8C00AEB870 /* ContentView.swift in Sources */,
156 | B780945F26004E8C00AEB870 /* PatchmanApp.swift in Sources */,
157 | );
158 | runOnlyForDeploymentPostprocessing = 0;
159 | };
160 | /* End PBXSourcesBuildPhase section */
161 |
162 | /* Begin XCBuildConfiguration section */
163 | B780946926004E8D00AEB870 /* Debug */ = {
164 | isa = XCBuildConfiguration;
165 | buildSettings = {
166 | ALWAYS_SEARCH_USER_PATHS = NO;
167 | CLANG_ANALYZER_NONNULL = YES;
168 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
169 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
170 | CLANG_CXX_LIBRARY = "libc++";
171 | CLANG_ENABLE_MODULES = YES;
172 | CLANG_ENABLE_OBJC_ARC = YES;
173 | CLANG_ENABLE_OBJC_WEAK = YES;
174 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
175 | CLANG_WARN_BOOL_CONVERSION = YES;
176 | CLANG_WARN_COMMA = YES;
177 | CLANG_WARN_CONSTANT_CONVERSION = YES;
178 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
179 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
180 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
181 | CLANG_WARN_EMPTY_BODY = YES;
182 | CLANG_WARN_ENUM_CONVERSION = YES;
183 | CLANG_WARN_INFINITE_RECURSION = YES;
184 | CLANG_WARN_INT_CONVERSION = YES;
185 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
186 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
187 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
188 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
189 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
190 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
191 | CLANG_WARN_STRICT_PROTOTYPES = YES;
192 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
193 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
194 | CLANG_WARN_UNREACHABLE_CODE = YES;
195 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
196 | COPY_PHASE_STRIP = NO;
197 | DEBUG_INFORMATION_FORMAT = dwarf;
198 | ENABLE_STRICT_OBJC_MSGSEND = YES;
199 | ENABLE_TESTABILITY = YES;
200 | GCC_C_LANGUAGE_STANDARD = gnu11;
201 | GCC_DYNAMIC_NO_PIC = NO;
202 | GCC_NO_COMMON_BLOCKS = YES;
203 | GCC_OPTIMIZATION_LEVEL = 0;
204 | GCC_PREPROCESSOR_DEFINITIONS = (
205 | "DEBUG=1",
206 | "$(inherited)",
207 | );
208 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
209 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
210 | GCC_WARN_UNDECLARED_SELECTOR = YES;
211 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
212 | GCC_WARN_UNUSED_FUNCTION = YES;
213 | GCC_WARN_UNUSED_VARIABLE = YES;
214 | MACOSX_DEPLOYMENT_TARGET = 11.1;
215 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
216 | MTL_FAST_MATH = YES;
217 | ONLY_ACTIVE_ARCH = YES;
218 | SDKROOT = macosx;
219 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
220 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
221 | };
222 | name = Debug;
223 | };
224 | B780946A26004E8D00AEB870 /* Release */ = {
225 | isa = XCBuildConfiguration;
226 | buildSettings = {
227 | ALWAYS_SEARCH_USER_PATHS = NO;
228 | CLANG_ANALYZER_NONNULL = YES;
229 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
230 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
231 | CLANG_CXX_LIBRARY = "libc++";
232 | CLANG_ENABLE_MODULES = YES;
233 | CLANG_ENABLE_OBJC_ARC = YES;
234 | CLANG_ENABLE_OBJC_WEAK = YES;
235 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
236 | CLANG_WARN_BOOL_CONVERSION = YES;
237 | CLANG_WARN_COMMA = YES;
238 | CLANG_WARN_CONSTANT_CONVERSION = YES;
239 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
240 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
241 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
242 | CLANG_WARN_EMPTY_BODY = YES;
243 | CLANG_WARN_ENUM_CONVERSION = YES;
244 | CLANG_WARN_INFINITE_RECURSION = YES;
245 | CLANG_WARN_INT_CONVERSION = YES;
246 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
247 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
250 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
251 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
252 | CLANG_WARN_STRICT_PROTOTYPES = YES;
253 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
254 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
255 | CLANG_WARN_UNREACHABLE_CODE = YES;
256 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
257 | COPY_PHASE_STRIP = NO;
258 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
259 | ENABLE_NS_ASSERTIONS = NO;
260 | ENABLE_STRICT_OBJC_MSGSEND = YES;
261 | GCC_C_LANGUAGE_STANDARD = gnu11;
262 | GCC_NO_COMMON_BLOCKS = YES;
263 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
264 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
265 | GCC_WARN_UNDECLARED_SELECTOR = YES;
266 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
267 | GCC_WARN_UNUSED_FUNCTION = YES;
268 | GCC_WARN_UNUSED_VARIABLE = YES;
269 | MACOSX_DEPLOYMENT_TARGET = 11.1;
270 | MTL_ENABLE_DEBUG_INFO = NO;
271 | MTL_FAST_MATH = YES;
272 | SDKROOT = macosx;
273 | SWIFT_COMPILATION_MODE = wholemodule;
274 | SWIFT_OPTIMIZATION_LEVEL = "-O";
275 | };
276 | name = Release;
277 | };
278 | B780946C26004E8D00AEB870 /* Debug */ = {
279 | isa = XCBuildConfiguration;
280 | buildSettings = {
281 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
282 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
283 | CODE_SIGN_ENTITLEMENTS = Patchman/Patchman.entitlements;
284 | CODE_SIGN_STYLE = Automatic;
285 | COMBINE_HIDPI_IMAGES = YES;
286 | DEVELOPMENT_ASSET_PATHS = "\"Patchman/Preview Content\"";
287 | DEVELOPMENT_TEAM = TR3K2EP48P;
288 | ENABLE_HARDENED_RUNTIME = YES;
289 | ENABLE_PREVIEWS = YES;
290 | INFOPLIST_FILE = Patchman/Info.plist;
291 | LD_RUNPATH_SEARCH_PATHS = (
292 | "$(inherited)",
293 | "@executable_path/../Frameworks",
294 | );
295 | MACOSX_DEPLOYMENT_TARGET = 11.0;
296 | PRODUCT_BUNDLE_IDENTIFIER = com.luby.Patchman;
297 | PRODUCT_NAME = "$(TARGET_NAME)";
298 | SWIFT_VERSION = 5.0;
299 | };
300 | name = Debug;
301 | };
302 | B780946D26004E8D00AEB870 /* Release */ = {
303 | isa = XCBuildConfiguration;
304 | buildSettings = {
305 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
306 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
307 | CODE_SIGN_ENTITLEMENTS = Patchman/Patchman.entitlements;
308 | CODE_SIGN_STYLE = Automatic;
309 | COMBINE_HIDPI_IMAGES = YES;
310 | DEVELOPMENT_ASSET_PATHS = "\"Patchman/Preview Content\"";
311 | DEVELOPMENT_TEAM = TR3K2EP48P;
312 | ENABLE_HARDENED_RUNTIME = YES;
313 | ENABLE_PREVIEWS = YES;
314 | INFOPLIST_FILE = Patchman/Info.plist;
315 | LD_RUNPATH_SEARCH_PATHS = (
316 | "$(inherited)",
317 | "@executable_path/../Frameworks",
318 | );
319 | MACOSX_DEPLOYMENT_TARGET = 11.0;
320 | PRODUCT_BUNDLE_IDENTIFIER = com.luby.Patchman;
321 | PRODUCT_NAME = "$(TARGET_NAME)";
322 | SWIFT_VERSION = 5.0;
323 | };
324 | name = Release;
325 | };
326 | /* End XCBuildConfiguration section */
327 |
328 | /* Begin XCConfigurationList section */
329 | B780945626004E8C00AEB870 /* Build configuration list for PBXProject "Patchman" */ = {
330 | isa = XCConfigurationList;
331 | buildConfigurations = (
332 | B780946926004E8D00AEB870 /* Debug */,
333 | B780946A26004E8D00AEB870 /* Release */,
334 | );
335 | defaultConfigurationIsVisible = 0;
336 | defaultConfigurationName = Release;
337 | };
338 | B780946B26004E8D00AEB870 /* Build configuration list for PBXNativeTarget "Patchman" */ = {
339 | isa = XCConfigurationList;
340 | buildConfigurations = (
341 | B780946C26004E8D00AEB870 /* Debug */,
342 | B780946D26004E8D00AEB870 /* Release */,
343 | );
344 | defaultConfigurationIsVisible = 0;
345 | defaultConfigurationName = Release;
346 | };
347 | /* End XCConfigurationList section */
348 |
349 | /* Begin XCRemoteSwiftPackageReference section */
350 | B7BBE5882609B4160087E461 /* XCRemoteSwiftPackageReference "CodeViewer" */ = {
351 | isa = XCRemoteSwiftPackageReference;
352 | repositoryURL = "https://github.com/dwarvesf/CodeViewer";
353 | requirement = {
354 | kind = upToNextMajorVersion;
355 | minimumVersion = 1.2.4;
356 | };
357 | };
358 | /* End XCRemoteSwiftPackageReference section */
359 |
360 | /* Begin XCSwiftPackageProductDependency section */
361 | B7BBE5892609B4160087E461 /* CodeViewer */ = {
362 | isa = XCSwiftPackageProductDependency;
363 | package = B7BBE5882609B4160087E461 /* XCRemoteSwiftPackageReference "CodeViewer" */;
364 | productName = CodeViewer;
365 | };
366 | /* End XCSwiftPackageProductDependency section */
367 | };
368 | rootObject = B780945326004E8C00AEB870 /* Project object */;
369 | }
370 |
--------------------------------------------------------------------------------
/Patchman/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Patchman
4 | //
5 | // Created by Praneet S on 16/03/21.
6 | //
7 |
8 | import SwiftUI
9 | import CodeViewer
10 |
11 | struct LogoView: View {
12 | var body: some View {
13 | HStack {
14 | Label("Patchman", systemImage: "envelope.open.fill")
15 | .font(.title)
16 | .padding(.top)
17 | Spacer()
18 | }.padding(.horizontal)
19 | }
20 | }
21 |
22 | struct JSONEditorOptionsView: View {
23 | @Binding var isHeaderAsJson: Bool
24 | @Binding var isHeaderFieldsEnabled: Bool
25 | @Binding var headerKey: String
26 | @Binding var headerValue: String
27 | @Binding var headers: [String : String]
28 | @Binding var isBulkRequest: Bool
29 | @Binding var isBodyAsJson: Bool
30 | @Binding var method: Int
31 | @Binding var bodyKey: String
32 | @Binding var bodyValue: String
33 | @Binding var requestBody: [String : Any]
34 | @Binding var editorFor: Int
35 | @Binding var headersJson: String
36 | @Binding var bodyJson: String
37 | var body: some View {
38 | if !isHeaderAsJson {
39 | HeadersEnabledView(isHeaderFieldsEnabled: $isHeaderFieldsEnabled, headerKey: $headerKey, headerValue: $headerValue, headers: $headers, headersJson: $headersJson)
40 | }
41 | if !isBulkRequest && !isBodyAsJson {
42 | BodyFillerView(method: $method, bodyKey: $bodyKey, bodyValue: $bodyValue, requestBody: $requestBody, requestBodyJson: $bodyJson)
43 | }
44 |
45 | if isHeaderFieldsEnabled || !isBulkRequest {
46 | VStack(alignment: .leading){
47 | Text("JSON Editor")
48 | .bold()
49 | .font(.subheadline)
50 | HStack {
51 | if isHeaderFieldsEnabled {
52 | Toggle("Headers", isOn: $isHeaderAsJson)
53 | .onChange(of: isHeaderAsJson) { value in
54 | if value {
55 | editorFor = 1
56 | }
57 | }
58 | }
59 | if !isBulkRequest {
60 | Toggle("Request Body", isOn: $isBodyAsJson)
61 | .onChange(of: isBodyAsJson) { value in
62 | if value {
63 | editorFor = 2
64 | }
65 | }
66 | }
67 | Spacer()
68 | }.padding(.leading)
69 | }.padding(.top)
70 | }
71 | }
72 | }
73 |
74 | struct ActionBarView: View {
75 |
76 | @Binding var url: String
77 | @Binding var methods: [String]
78 | @Binding var method: Int
79 | @Binding var isProcessing: Bool
80 | @Binding var isBulkRequest: Bool
81 | @Binding var response: String
82 | @Binding var bulkResponsesStatusCodes: [Int]
83 | @Binding var bulkRequestBody: [[String : Any]]
84 | @Binding var headers: [String : String]
85 | @Binding var requestBody: [String : Any]
86 | @Binding var responseStatus: HTTPURLResponse?
87 | @Binding var cachePolicy: String
88 |
89 | func execute(method: RequestMethod) {
90 | isProcessing = true
91 | var cachePolicySelected: URLRequest.CachePolicy {
92 | switch cachePolicy {
93 | case cachePolicies.reloadIgnoringCacheData.rawValue:
94 | return .reloadIgnoringCacheData
95 | case cachePolicies.reloadIgnoringLocalAndRemoteCacheData.rawValue:
96 | return .reloadIgnoringLocalAndRemoteCacheData
97 | case cachePolicies.reloadIgnoringLocalCacheData.rawValue:
98 | return .reloadIgnoringLocalCacheData
99 | case cachePolicies.reloadRevalidatingCacheData.rawValue:
100 | return .reloadRevalidatingCacheData
101 | case cachePolicies.returnCacheDataDontLoad.rawValue:
102 | return .returnCacheDataDontLoad
103 | case cachePolicies.returnCacheDataElseLoad.rawValue:
104 | return .returnCacheDataElseLoad
105 | default:
106 | return .useProtocolCachePolicy
107 | }
108 | }
109 | if isBulkRequest {
110 | DispatchQueue.global().async {
111 | response = ""
112 | bulkResponsesStatusCodes = []
113 | for reqBody in bulkRequestBody {
114 | let req: Request = Request(url: url, method: method, cachingPolicy: cachePolicySelected, requestBody: reqBody, requestHeaders: headers)
115 | let res = req.executeRequest()
116 | responseStatus = res.responseStatus
117 | response += "\t\t---START OF RESPONSE---\n" + (res.response.prettyPrintedJSONString ?? "") + "\n\t\t---END OF RESPONSE---\n\n"
118 | bulkResponsesStatusCodes.append(res.responseStatus?.statusCode ?? -1)
119 | }
120 | }
121 | } else {
122 | let req: Request = Request(url: url, method: method, requestBody: requestBody, requestHeaders: headers)
123 | let res = req.executeRequest()
124 | responseStatus = res.responseStatus
125 | response = res.response.prettyPrintedJSONString ?? ""
126 | isProcessing = false
127 | }
128 | }
129 |
130 | var body: some View {
131 | HStack {
132 | TextField("URL", text: $url)
133 | .textFieldStyle(RoundedBorderTextFieldStyle())
134 | Picker("Method", selection: $method) {
135 | ForEach(0.. Bool {
176 | return statusCode >= 200 && statusCode < 300
177 | }
178 |
179 | var body: some View {
180 | HStack {
181 | ScrollView {
182 | VStack(alignment: .leading) {
183 | ForEach(bulkResponses, id: \.self) { response in
184 | Text("Status: \(response)")
185 | .bold()
186 | .background(
187 | Rectangle()
188 | .frame(width: 90, height: 30)
189 | .foregroundColor(isStatusOkay(statusCode: response) ? green : red )
190 | .border(isStatusOkay(statusCode: response) ? Color.green : Color.red, width: 3)
191 | .cornerRadius(6))
192 | .padding()
193 | }
194 | }.padding()
195 | }.onChange(of: bulkResponses, perform: { _ in
196 | isProcessing = bulkResponses.count != bulkRequestBody.count
197 | print(isProcessing)
198 | })
199 | Spacer()
200 | }.padding()
201 | }
202 | }
203 |
204 | struct FieldsToggleView: View {
205 | @Binding var isParamsEnabled: Bool
206 | @Binding var isHeaderFieldsEnabled: Bool
207 | @Binding var isBulkRequest: Bool
208 | @Binding var bulkRequestBody: [[String : Any]]
209 | @Binding var cachePolicy: String
210 | @State var cachePoliciesList: [String] = cachePolicies.allCases.map({ $0.rawValue })
211 |
212 | func pickFile() -> String? {
213 | let dialog = NSOpenPanel();
214 |
215 | dialog.title = "Choose a CSV file";
216 | dialog.showsResizeIndicator = true;
217 | dialog.showsHiddenFiles = false;
218 | dialog.allowsMultipleSelection = false;
219 | dialog.canChooseDirectories = false;
220 | dialog.allowedFileTypes = ["csv"];
221 |
222 | if (dialog.runModal() == NSApplication.ModalResponse.OK) {
223 | let result = dialog.url
224 | if (result != nil) {
225 | let path: String = result!.path
226 | return path
227 | }
228 | } else {
229 | return nil
230 | }
231 | return nil
232 | }
233 |
234 | var body: some View {
235 | HStack {
236 | Toggle("Query Params", isOn: $isParamsEnabled)
237 | Toggle("Header Fields", isOn: $isHeaderFieldsEnabled)
238 | Toggle("Bulk requests", isOn: $isBulkRequest)
239 | Picker("Cache policy", selection: $cachePolicy) {
240 | ForEach(cachePoliciesList, id: \.self){ policy in
241 | Text(policy)
242 | }
243 | }
244 | if isBulkRequest {
245 | Button(action: {
246 | guard let bulkRequestBodyFilePath: String = pickFile() else { return }
247 | do {
248 | let bulkRequestBodyContents = try String(contentsOf: URL(fileURLWithPath: bulkRequestBodyFilePath))
249 | var bulkRequestBodyCSVs = bulkRequestBodyContents.split(separator: "\n").map({ String($0) })
250 | let keys = bulkRequestBodyCSVs.first!.split(separator: ",")
251 | bulkRequestBodyCSVs = bulkRequestBodyCSVs.dropFirst().map({ String($0) })
252 | for field in bulkRequestBodyCSVs {
253 |
254 | let fieldDecoded = field.split(separator: ",")
255 | if fieldDecoded.count == keys.count {
256 | var bodyField: [String : Any] = [:]
257 | for index in 0.. 0 {
341 | HStack{
342 | Label("Response", systemImage: "icloud.and.arrow.down")
343 | .font(.title2)
344 | Toggle("View response headers", isOn: $isResponseHeadersShown)
345 | if isBulkRequest {
346 | Toggle("View bulk response statuses", isOn: $isBulkResponseStatusesShown)
347 | }
348 | Spacer()
349 | if !isBulkResponseStatusesShown {
350 | Text("Status: \(responseStatus?.statusCode ?? -1)")
351 | .foregroundColor(responseStatus?.statusCode ?? -1 >= 200 && responseStatus?.statusCode ?? -1 < 300 ? .green : .orange)
352 | }
353 | }.padding()
354 | }
355 | }
356 | }
357 |
358 | struct BodyFillerView: View {
359 | @Binding var method: Int
360 | @Binding var bodyKey: String
361 | @Binding var bodyValue: String
362 | @Binding var requestBody: [String : Any]
363 | @Binding var requestBodyJson: String
364 | var body: some View {
365 | HStack {
366 | TextField("Request body key", text: $bodyKey)
367 | .frame(width: 130)
368 | .textFieldStyle(RoundedBorderTextFieldStyle())
369 | TextField("Value", text: $bodyValue)
370 | .frame(width: 130)
371 | .textFieldStyle(RoundedBorderTextFieldStyle())
372 | Button("Add", action: {
373 | requestBody[bodyKey] = bodyValue
374 | do {
375 | let jsonData = try JSONSerialization.data(withJSONObject: requestBody, options: .prettyPrinted)
376 | requestBodyJson = String(data: jsonData, encoding: .utf8) ?? ""
377 | } catch {}
378 | })
379 | Spacer()
380 | }.padding([.horizontal, .top])
381 | }
382 | }
383 |
384 | struct ProfileView: View {
385 | var profile: Profile
386 | var body: some View {
387 | VStack(alignment: .leading) {
388 | HStack {
389 | Text(profile.profileName)
390 | .bold()
391 | .lineLimit(2)
392 | .padding(.leading, 4)
393 | Spacer()
394 | }
395 | }
396 | .frame(width: 165, height: 35)
397 | .background(Rectangle().frame(width: 165, height: 35).foregroundColor(.gray).opacity(0.45).cornerRadius(6))
398 | }
399 | }
400 |
401 | struct PresetView: View {
402 | var preset: Preset
403 | var body: some View {
404 | VStack(alignment: .leading) {
405 | HStack {
406 | Text(preset.presetType == 0 ? "Header Field" : "Body Field")
407 | .font(.subheadline)
408 | .bold()
409 | .padding(.leading, 4)
410 | Spacer()
411 | }
412 | HStack {
413 | Text(preset.presetName)
414 | .bold()
415 | .lineLimit(2)
416 | .padding(.leading, 4)
417 | Spacer()
418 | }
419 | }
420 | .frame(width: 165, height: 55)
421 | .background(Rectangle().frame(width: 165, height: 55).foregroundColor(.gray).opacity(0.45).cornerRadius(6))
422 | }
423 | }
424 |
425 | struct ContentView: View {
426 |
427 | @State var dragOver: Bool = false
428 |
429 | @ObservedObject var defaults: Defaults = Defaults.shared
430 |
431 | @State var url:String = ""
432 | @State var response: String = ""
433 | @State var methods: [String] = RequestMethod.allCases.map({ $0.rawValue })
434 | @State var requestBody: [String : Any] = [:]
435 | @State var headers: [String : String] = [:]
436 | @State var method: Int = 0
437 | @State var isParamsEnabled: Bool = false
438 | @State var isHeaderFieldsEnabled: Bool = false
439 | @State var headerKey: String = ""
440 | @State var headerValue: String = ""
441 | @State var key: String = ""
442 | @State var value: String = ""
443 | @State var bodyKey: String = ""
444 | @State var bodyValue: String = ""
445 | @State var view: Int = 0
446 | @State var responseStatus: HTTPURLResponse?
447 | @State var isResponseHeadersShown: Bool = false
448 | @State var isBulkResponseStatusesShown: Bool = false
449 | @State var bulkResponsesStatusCodes: [Int] = []
450 | @State var isProcessing: Bool = false
451 | @State var presetType: Int = 0
452 | @State var presetKey: String = ""
453 | @State var presetValue: String = ""
454 | @State var presetName: String = ""
455 | @State var isPresetAddShown: Bool = false
456 | @State var bulkRequestBody: [[String : Any]] = []
457 | @State var isBulkRequest: Bool = false
458 | @State var profileName: String = ""
459 | @State var cachePolicy: String = cachePolicies.useProtocolCachePolicy.rawValue
460 | @State var isHeaderAsJson: Bool = false
461 | @State var isBodyAsJson: Bool = false
462 | @State var headerJson: String = ""
463 | @State var bodyJson: String = ""
464 | @State var editorFor: Int = -1
465 |
466 | func getResponseHeaders(response: String) -> String {
467 | let regex = try! NSRegularExpression(pattern: "Optional\\(", options: NSRegularExpression.Options.caseInsensitive)
468 | let range = NSMakeRange(0, response.count)
469 | return regex.stringByReplacingMatches(in: response, options: [], range: range, withTemplate: "")
470 | }
471 |
472 | func saveProfile() {
473 | let p = Profile(profileName: profileName, method: method, url: url, headers: headers, requestBody: requestBody as? [String : JSONValue] ?? [:], isHeadersEnabled: isHeaderFieldsEnabled, isBulkRequest: isBulkRequest, bulkRequestBody: bulkRequestBody as? [[String : JSONValue]] ?? [[:]])
474 | p.save()
475 | defaults.profiles.append(p)
476 | }
477 |
478 | var body: some View {
479 | return HStack {
480 | VStack {
481 | List {
482 | Text("Presets")
483 | .bold()
484 | .font(.title2)
485 | Toggle("Add preset", isOn: $isPresetAddShown)
486 | if isPresetAddShown {
487 | Picker("", selection: $presetType){
488 | Text("Header Field").tag(0)
489 | Text("Body").tag(1)
490 | }.pickerStyle(SegmentedPickerStyle())
491 | Text("Preset Name")
492 | TextEditor(text: $presetName)
493 | .cornerRadius(6)
494 | Text("Key")
495 | TextEditor(text: $presetKey)
496 | .cornerRadius(6)
497 | Text("Value")
498 | TextEditor(text: $presetValue)
499 | .cornerRadius(6)
500 | Button("Add preset"){
501 | if presetKey.count > 0 && presetValue.count > 0 && presetName.count > 0 {
502 | let preset = Preset(presetType: presetType, key: presetKey, value: presetValue, presetName: presetName)
503 | preset.save()
504 | defaults.presets.append(preset)
505 | }
506 | }.padding(.bottom)
507 | }
508 | ForEach(defaults.presets, id: \.key){ preset in
509 | PresetView(preset: preset)
510 | .onTapGesture {
511 | switch preset.presetType {
512 | case 0:
513 | headers[preset.key] = preset.value
514 | break
515 | case 1:
516 | requestBody[preset.key] = preset.value
517 | break
518 | default:
519 | break
520 | }
521 | }
522 | }
523 | Text(defaults.profiles.count > 0 ? "Profiles" : "")
524 | .bold()
525 | .font(.title2)
526 | .padding(.top)
527 | ForEach(defaults.profiles, id: \.profileName){ profile in
528 | ProfileView(profile: profile)
529 | .onTapGesture {
530 | url = profile.url
531 | method = profile.method
532 | headers = profile.headers
533 | requestBody = profile.requestBody
534 | isHeaderFieldsEnabled = profile.isHeadersEnabled
535 | isBulkRequest = profile.isBulkRequest
536 | bulkRequestBody = profile.bulkRequestBody
537 | do {
538 | let jsonHeaderData = try JSONSerialization.data(withJSONObject: headers, options: .prettyPrinted)
539 | headerJson = String(data: jsonHeaderData, encoding: .utf8) ?? ""
540 | let jsonBodyData = try JSONSerialization.data(withJSONObject: requestBody, options: .prettyPrinted)
541 | bodyJson = String(data: jsonBodyData, encoding: .utf8) ?? ""
542 | } catch {}
543 | }
544 | .contextMenu(menuItems: {
545 | Button("Save on disk", action: {
546 | profile.saveOnDisk()
547 | })
548 | })
549 | }
550 | }.frame(minWidth: 200, maxWidth: 200)
551 | }.listStyle(SidebarListStyle())
552 |
553 | VStack {
554 | HStack{
555 | LogoView()
556 | Spacer()
557 | TextField("Profile name", text: $profileName)
558 | .textFieldStyle(RoundedBorderTextFieldStyle())
559 | .frame(width: 145)
560 | Button(action: {
561 | if profileName.count > 0 {
562 | saveProfile()
563 | }
564 | }, label: {
565 | Image(systemName: "square.and.arrow.down")
566 | }).padding()
567 | }
568 |
569 | ActionBarView(url: $url, methods: $methods, method: $method, isProcessing: $isProcessing, isBulkRequest: $isBulkRequest, response: $response, bulkResponsesStatusCodes: $bulkResponsesStatusCodes, bulkRequestBody: $bulkRequestBody, headers: $headers, requestBody: $requestBody, responseStatus: $responseStatus, cachePolicy: $cachePolicy)
570 |
571 | FieldsToggleView(isParamsEnabled: $isParamsEnabled, isHeaderFieldsEnabled: $isHeaderFieldsEnabled, isBulkRequest: $isBulkRequest, bulkRequestBody: $bulkRequestBody, cachePolicy: $cachePolicy)
572 |
573 | ParamsEnabledView(isParamsEnabled: $isParamsEnabled, key: $key, value: $value, url: $url)
574 |
575 | JSONEditorOptionsView(isHeaderAsJson: $isHeaderAsJson, isHeaderFieldsEnabled: $isHeaderFieldsEnabled, headerKey: $headerKey, headerValue: $headerValue, headers: $headers, isBulkRequest: $isBulkRequest, isBodyAsJson: $isBodyAsJson, method: $method, bodyKey: $bodyKey, bodyValue: $bodyValue, requestBody: $requestBody, editorFor: $editorFor, headersJson: $headerJson, bodyJson: $bodyJson)
576 |
577 | if isHeaderAsJson && isBodyAsJson {
578 | Picker(selection: $editorFor, label: Text("")) {
579 | Text("Headers").tag(1)
580 | Text("Request Body").tag(2)
581 | }.pickerStyle(SegmentedPickerStyle())
582 | .padding(.horizontal)
583 | }
584 |
585 | if editorFor == 1 && isHeaderAsJson {
586 | CodeViewer(
587 | content: $headerJson,
588 | textDidChanged: { json in
589 | if let data = json.data(using: .utf8) {
590 | do {
591 | if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:String] {
592 | headers = json
593 | }
594 | } catch {
595 | print("Something went wrong")
596 | }
597 | }
598 | }
599 | )
600 | .cornerRadius(6)
601 | .padding(.horizontal)
602 | } else if editorFor == 2 && isBodyAsJson {
603 | CodeViewer(
604 | content: $bodyJson,
605 | textDidChanged: { json in
606 | if let data = json.data(using: .utf8) {
607 | do {
608 | if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:Any]{
609 | requestBody = json
610 | }
611 | } catch {
612 | print("Something went wrong")
613 | }
614 | }
615 | }
616 | )
617 | .cornerRadius(6)
618 | .padding(.horizontal)
619 | }
620 |
621 | ResponseHeaderAndOptionsView(response: $response, responseStatus: $responseStatus, isResponseHeadersShown: $isResponseHeadersShown, isBulkResponseStatusesShown: $isBulkResponseStatusesShown, isBulkRequest: $isBulkRequest)
622 |
623 | if isBulkResponseStatusesShown {
624 | BulkStatusListView(bulkResponses: $bulkResponsesStatusCodes, isProcessing: $isProcessing, bulkRequestBody: $bulkRequestBody)
625 | Text("\(bulkResponsesStatusCodes.filter({ $0 >= 200 && $0 < 300 }).count) / \(bulkRequestBody.count) requests successful")
626 | .bold()
627 | .padding()
628 | } else {
629 | ScrollView {
630 | HStack {
631 | Text(isResponseHeadersShown ? getResponseHeaders(response: "\(responseStatus)") : response)
632 | .lineLimit(nil)
633 | .onChange(of: bulkResponsesStatusCodes, perform: { _ in
634 | if bulkResponsesStatusCodes.count == bulkRequestBody.count {
635 | isProcessing = false
636 | }
637 | })
638 | Spacer()
639 | }
640 | }.padding()
641 | }
642 | }.frame(height: 550)
643 | .frame(minWidth: 600)
644 | VStack{
645 |
646 | List {
647 | Text(headers.count > 0 ? "Header fields" : "")
648 | .font(.title2)
649 | .bold()
650 | ForEach(headers.keys.map({ String($0) }), id: \.self) { key in
651 | HStack {
652 | Button(action: {
653 | headers.removeValue(forKey: key)
654 | }, label: {
655 | Image(systemName: "trash")
656 | })
657 | Text("\(key) : \(headers[key]!)")
658 | Spacer()
659 | }
660 | }
661 | .frame(width: 200)
662 |
663 |
664 | Text(requestBody.count > 0 ? "Request Body fields" : "")
665 | .font(.title2)
666 | .bold()
667 | ForEach(requestBody.keys.map({ String($0) }), id: \.self) { key in
668 | HStack {
669 | Button(action: {
670 | requestBody.removeValue(forKey: key)
671 | }, label: {
672 | Image(systemName: "trash")
673 | })
674 | Text("\(key) : \(requestBody[key]! as? String ?? "Not representable")")
675 | Spacer()
676 | }
677 | }
678 | .frame(width: 200)
679 | }
680 | .listStyle(SidebarListStyle())
681 | }.frame(minWidth: 200, maxWidth: 200)
682 | }.onDrop(of: ["public.file-url"], isTargeted: $dragOver) { providers -> Bool in
683 | providers.first?.loadDataRepresentation(forTypeIdentifier: "public.file-url", completionHandler: { (data, error) in
684 | if let data = data, let path = NSString(data: data, encoding: 4), let url = URL(string: path as String) {
685 | guard let data: Data = try? Data(contentsOf: url) else { return }
686 | guard let profile: Profile = try? JSONDecoder().decode(Profile.self, from: data) else { return }
687 | DispatchQueue.main.async {
688 | self.profileName = profile.profileName
689 | self.method = profile.method
690 | self.url = profile.url
691 | self.headers = profile.headers
692 | self.requestBody = profile.requestBody
693 | self.isHeaderFieldsEnabled = profile.isHeadersEnabled
694 | self.isBulkRequest = profile.isBulkRequest
695 | self.bulkRequestBody = profile.bulkRequestBody
696 | }
697 | }
698 | })
699 | return true
700 | }
701 | }
702 | }
703 |
704 | extension Data {
705 | var prettyPrintedJSONString: String? {
706 | guard let object = try? JSONSerialization.jsonObject(with: self, options: []),
707 | let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]),
708 | let prettyPrintedString = String(data: data, encoding: .utf8) else { return nil }
709 |
710 | return prettyPrintedString
711 | }
712 | }
713 |
--------------------------------------------------------------------------------