Skip to content

Commit b4b54e2

Browse files
phimagee-marchand
authored andcommitted
Support plist & pbxproj in json format
1 parent 680db38 commit b4b54e2

File tree

8 files changed

+3058
-79
lines changed

8 files changed

+3058
-79
lines changed

Sources/XcodeProj+FilePath.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010

1111
extension XcodeProj {
1212

13-
func paths(_ current: PBXGroup, prefix: String) -> [String: PathType] {
13+
static func paths(_ current: PBXGroup, prefix: String) -> [String: PathType] {
1414
var paths: [String: PathType] = [:]
1515

1616
for file in current.fileRefs {

Sources/XcodeProj+Serialization.swift

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010

1111
extension XcodeProj {
1212

13-
public func write(to url: URL, format: PropertyListSerialization.PropertyListFormat? = nil,
13+
public func write(to url: URL, format: Format? = nil,
1414
projectName: String? = nil, lineEnding: String? = nil, atomic: Bool = true) throws {
1515
let pbxprojURL: URL
1616
let name: String
@@ -36,13 +36,22 @@ extension XcodeProj {
3636
if format == .openStep {
3737
let serializer = OpenStepSerializer(projectName: name, lineEnding: lineEnding, projectFile: self)
3838
try serializer.serialize().write(to: pbxprojURL, atomically: atomic, encoding: .utf8)
39-
} else {
40-
let data = try PropertyListSerialization.data(fromPropertyList: dict, format: format, options: 0)
41-
#if os(Linux)
39+
}
40+
else if let propertyListformat = format.toPropertyListformat() {
41+
let data = try PropertyListSerialization.data(fromPropertyList: dict, format: propertyListformat, options: 0)
42+
#if os(Linux)
4243
try data.write(to: pbxprojURL, options: []) // error no attomic on linux
43-
#else
44+
#else
4445
try data.write(to: pbxprojURL, options: atomic ? [.atomicWrite] : [])
45-
#endif
46+
#endif
47+
}
48+
else if format == .json {
49+
let data = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
50+
#if os(Linux)
51+
try data.write(to: pbxprojURL, options: []) // error no attomic on linux
52+
#else
53+
try data.write(to: pbxprojURL, options: atomic ? [.atomicWrite] : [])
54+
#endif
4655
}
4756
}
4857

@@ -51,9 +60,13 @@ extension XcodeProj {
5160
let projectName = projectName ?? self.projectName
5261
let serializer = OpenStepSerializer(projectName: projectName, projectFile: self)
5362
return try serializer.serialize().data(using: .utf8) ?? Data()
54-
} else {
55-
return try PropertyListSerialization.data(fromPropertyList: dict, format: format, options: 0)
63+
} else if let propertyListformat = format.toPropertyListformat() {
64+
return try PropertyListSerialization.data(fromPropertyList: dict, format: propertyListformat, options: 0)
65+
}
66+
else if format == .json {
67+
return try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
5668
}
69+
return Data() // must not occurs
5770
}
5871

5972
}

Sources/XcodeProj.swift

Lines changed: 91 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,97 @@
88

99
import Foundation
1010

11-
public class XcodeProj {
11+
open class PropertyList {
12+
13+
public enum Format {
14+
case binary
15+
case xml
16+
case json
17+
case openStep
18+
19+
public func toPropertyListformat() -> PropertyListSerialization.PropertyListFormat? {
20+
switch self {
21+
case .binary:
22+
return .binary
23+
case .xml:
24+
return .xml
25+
case .openStep:
26+
return .openStep
27+
case .json:
28+
return nil
29+
}
30+
}
31+
32+
public init(_ format: PropertyListSerialization.PropertyListFormat) {
33+
switch format {
34+
case .binary:
35+
self = .binary
36+
case .xml:
37+
self = .xml
38+
case .openStep:
39+
self = .openStep
40+
}
41+
}
42+
43+
public init?(_ format: PropertyListSerialization.PropertyListFormat?) {
44+
if let format = format {
45+
self.init(format)
46+
} else {
47+
return nil
48+
}
49+
}
50+
}
51+
52+
public let dict: PBXObject.Fields
53+
public let format: Format
54+
55+
public init(dict: PBXObject.Fields, format: Format) throws {
56+
self.dict = dict
57+
self.format = format
58+
}
59+
60+
public convenience init(propertyListData data: Data) throws {
61+
let format: Format
62+
let obj: Any
63+
if data.first == 123 { // start with {
64+
obj = try JSONSerialization.jsonObject(with: data)
65+
format = .json
66+
} else {
67+
var propertyListFormat: PropertyListSerialization.PropertyListFormat = .binary
68+
obj = try PropertyListSerialization.propertyList(from: data, options: [], format: &propertyListFormat)
69+
format = .init(propertyListFormat)
70+
}
71+
72+
guard let dict = obj as? PBXObject.Fields else {
73+
throw XcodeProjError.invalidData(object: obj)
74+
}
75+
76+
try self.init(dict: dict, format: format)
77+
}
78+
79+
public convenience init(url: URL) throws {
80+
assert(url.isFileURL)
81+
do {
82+
let data = try Data(contentsOf: url)
83+
try self.init(propertyListData: data)
84+
} catch let error as XcodeProjError {
85+
throw error
86+
} catch {
87+
throw XcodeProjError.failedToReadFile(error: error)
88+
}
89+
}
90+
91+
}
92+
93+
public class XcodeProj: PropertyList {
1294

1395
public static let pbxprojFileExtension = "pbxproj"
1496
public static let pbxprojFileName = "project.pbxproj"
1597

16-
public let dict: PBXObject.Fields
17-
public let format: PropertyListSerialization.PropertyListFormat
1898
public var projectName: String = "PRODUCT_NAME"
1999
public var lineEnding: String = "\r\n"
20100

101+
public let objects: Objects
21102
public let project: PBXProject
22103

23104
public class Objects: PBXObjectFactory {
@@ -69,7 +150,6 @@ public class XcodeProj {
69150

70151
}
71152

72-
public let objects: Objects
73153

74154
// MARK: init
75155
public convenience init(url: URL) throws {
@@ -112,35 +192,25 @@ public class XcodeProj {
112192
}
113193
}
114194

115-
public convenience init(propertyListData data: Data) throws {
116-
var format: PropertyListSerialization.PropertyListFormat = .binary
117-
let obj = try PropertyListSerialization.propertyList(from: data, options: [], format: &format)
118-
119-
guard let dict = obj as? PBXObject.Fields else {
120-
throw XcodeProjError.invalidData(object: obj)
121-
}
122-
123-
try self.init(dict: dict, format: format)
195+
public convenience init(dict: PBXObject.Fields, format: PropertyListSerialization.PropertyListFormat) throws {
196+
try self.init(dict: dict, format: .init(format))
124197
}
125198

126-
init(dict: PBXObject.Fields, format: PropertyListSerialization.PropertyListFormat) throws {
127-
self.dict = dict
128-
self.format = format
129-
199+
public override init(dict: PBXObject.Fields, format: Format) throws {
130200
self.objects = Objects()
131201

132-
if let objs = self.dict[FieldKey.objects.rawValue] as? [String: PBXObject.Fields] {
202+
if let objs = dict[FieldKey.objects.rawValue] as? [String: PBXObject.Fields] {
133203
// Create all objects
134204
for (ref, obj) in objs {
135205
self.objects.dict[ref] = try XcodeProj.createObject(ref: ref, fields: obj, objects: self.objects)
136206
}
137207

138208
// parsing project
139-
if let rootObjectRef = self.dict[FieldKey.rootObject.rawValue] as? String {
209+
if let rootObjectRef = dict[FieldKey.rootObject.rawValue] as? String {
140210
if let projDict = objs[rootObjectRef] {
141211
self.project = PBXProject(ref: rootObjectRef, fields: projDict, objects: self.objects)
142212
if let mainGroup = self.project.mainGroup {
143-
objects.fullFilePaths = paths(mainGroup, prefix: "")
213+
objects.fullFilePaths = XcodeProj.paths(mainGroup, prefix: "")
144214
} else {
145215
if let mainGroupref = self.project.string(PBXProject.PBXKeys.mainGroup) {
146216
throw XcodeProjError.objectMissing(key: mainGroupref, expectedType: .group)
@@ -157,6 +227,7 @@ public class XcodeProj {
157227
} else {
158228
throw XcodeProjError.fieldKeyMissing(.objects)
159229
}
230+
try super.init(dict: dict, format: format)
160231
}
161232

162233
static func createObject(ref: String, fields: PBXObject.Fields, objects: XcodeProj.Objects) throws -> PBXObject {

Tests/Utils.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,50 @@ extension XCTestCase {
4747
func url(forResource resource: String, withExtension ext: String) -> URL? {
4848
return Utils.url(forResource: resource, withExtension: ext)
4949
}
50+
51+
func assertContentsEqual(_ url: URL, _ testURL: URL ) {
52+
do {
53+
let contents = try String(contentsOf: url)
54+
let testContents = try String(contentsOf: testURL)
55+
#if os(Linux)
56+
XCTAssertEqual(contents.replacingOccurrences(matchingPattern: "classes = \\{\\s*\\};", by: "classes = [:];"), testContents, "diff \(url.path) \(testURL.path)")
57+
#else
58+
XCTAssertEqual(contents, testContents)
59+
#endif
60+
61+
} catch {
62+
XCTFail("\(error)")
63+
}
64+
}
65+
66+
func assertContentsNotEqual(_ url: URL, _ testURL: URL ) {
67+
do {
68+
let contents = try String(contentsOf: url)
69+
let testContents = try String(contentsOf: testURL)
70+
#if os(Linux)
71+
XCTAssertNotEqual(contents.replacingOccurrences(matchingPattern: "classes = \\{\\s*\\};", by: "classes = [:];"), testContents, "diff \(url.path) \(testURL.path)")
72+
#else
73+
XCTAssertNotEqual(contents, testContents)
74+
#endif
75+
76+
} catch {
77+
XCTFail("\(error)")
78+
}
79+
}
80+
81+
}
82+
83+
extension String {
84+
func replacingOccurrences(matchingPattern pattern: String, by replacement: String) -> String {
85+
do {
86+
let expression = try NSRegularExpression(pattern: pattern, options: [])
87+
let matches = expression.matches(in: self, options: [], range: NSRange(startIndex..<endIndex, in: self))
88+
return matches.reversed().reduce(into: self) { (current, result) in
89+
let range = Range(result.range, in: current)!
90+
current.replaceSubrange(range, with: replacement)
91+
}
92+
} catch {
93+
return self
94+
}
95+
}
5096
}

Tests/XcodeProjKitEditionTests.swift

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class XcodeProjKitEditionTests: XCTestCase {
5353
XCTAssertEqual(PBXProject.Version(major: 12, minor: 0), testproj.project.lastUpgradeCheck)
5454

5555

56-
compare(url, testURL)
56+
assertContentsNotEqual(url, testURL)
5757

5858
} catch {
5959
XCTFail("\(error)")
@@ -63,19 +63,4 @@ class XcodeProjKitEditionTests: XCTestCase {
6363
}
6464
}
6565

66-
func compare(_ url: URL, _ testURL: URL ) {
67-
do {
68-
let contents = try String(contentsOf: url)
69-
let testContents = try String(contentsOf: testURL)
70-
#if os(Linux)
71-
XCTAssertNotEqual(contents.replacingOccurrences(matchingPattern: "classes = \\{\\s*\\};", by: "classes = [:];"), testContents, "diff \(url.path) \(testURL.path)")
72-
#else
73-
XCTAssertNotEqual(contents, testContents)
74-
#endif
75-
76-
} catch {
77-
XCTFail("\(error)")
78-
}
79-
80-
}
8166
}

Tests/XcodeProjKitParseTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ class XcodeProjKitParseTests: XCTestCase {
8888
testParse("ok/plist")
8989
}
9090

91+
func testjson() {
92+
testParse("ok/json")
93+
}
94+
9195
func testswiftpm() {
9296
let proj = testParse("ok/swiftpm.project")
9397
XCTAssertNotNil(proj?.objects.object("48A408192662576D0068A35B"))

Tests/XcodeProjKitWriteTests.swift

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,22 @@ class XcodeProjKitWriteTests: XCTestCase {
102102
XCTAssertNotNil(proj.project.buildConfigurationList)
103103
let testURL = URL(fileURLWithPath: XcodeProjKitWriteTests.directory + resource.replacingOccurrences(of: "ok/", with: "") + "." + XcodeProj.pbxprojFileExtension)
104104
try proj.write(to: testURL)
105-
105+
106+
assertContentsEqual(url, testURL)
107+
108+
// test passing by xml before recoding to openstep
106109
let testURLPlist = URL(fileURLWithPath: XcodeProjKitWriteTests.directory + resource.replacingOccurrences(of: "ok/", with: "") + ".plist")
107110
try proj.write(to: testURLPlist, format: .xml)
108-
109-
compare(url, testURL)
110-
111+
try XcodeProj(url: testURLPlist).write(to: testURLPlist.appendingPathExtension("pbxproj"), format: .openStep, projectName: proj.projectName, lineEnding: proj.lineEnding)
112+
print("\(url.path)")
113+
print("\(testURLPlist.appendingPathExtension("pbxproj").path)")
114+
assertContentsEqual(url, testURLPlist.appendingPathExtension("pbxproj"))
115+
116+
// test passing by json before recoding to openstep
117+
let testURLJSON = URL(fileURLWithPath: XcodeProjKitWriteTests.directory + resource.replacingOccurrences(of: "ok/", with: "") + ".json")
118+
try proj.write(to: testURLJSON, format: .json)
119+
try XcodeProj(url: testURLJSON).write(to: testURLJSON.appendingPathExtension(".pbxproj"), format: .openStep, projectName: proj.projectName, lineEnding: proj.lineEnding)
120+
assertContentsEqual(url, testURLJSON.appendingPathExtension(".pbxproj"))
111121
} catch {
112122
XCTFail("\(error)")
113123
}
@@ -116,35 +126,8 @@ class XcodeProjKitWriteTests: XCTestCase {
116126
}
117127
}
118128

119-
func compare(_ url: URL, _ testURL: URL ) {
120-
do {
121-
let contents = try String(contentsOf: url)
122-
let testContents = try String(contentsOf: testURL)
123-
#if os(Linux)
124-
XCTAssertEqual(contents.replacingOccurrences(matchingPattern: "classes = \\{\\s*\\};", by: "classes = [:];"), testContents, "diff \(url.path) \(testURL.path)")
125-
#else
126-
XCTAssertEqual(contents, testContents)
127-
#endif
128-
129-
} catch {
130-
XCTFail("\(error)")
131-
}
132-
133-
}
134-
135129
}
136130

137-
fileprivate extension String {
138-
func replacingOccurrences(matchingPattern pattern: String, by replacement: String) -> String {
139-
do {
140-
let expression = try NSRegularExpression(pattern: pattern, options: [])
141-
let matches = expression.matches(in: self, options: [], range: NSRange(startIndex..<endIndex, in: self))
142-
return matches.reversed().reduce(into: self) { (current, result) in
143-
let range = Range(result.range, in: current)!
144-
current.replaceSubrange(range, with: replacement)
145-
}
146-
} catch {
147-
return self
148-
}
149-
}
150-
}
131+
132+
133+

0 commit comments

Comments
 (0)