Skip to content

Commit 16108d2

Browse files
committed
✅ Add real-world use cases
1 parent dc22d4c commit 16108d2

File tree

5 files changed

+228
-3
lines changed

5 files changed

+228
-3
lines changed

Sources/GeoJSON/GeoJSON+Codable.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,38 @@ extension Feature {
409409
}
410410

411411
}
412+
413+
fileprivate enum FeatureCollectionCodingKeys: String, CodingKey {
414+
case geoJSONType = "type"
415+
case features, bbox
416+
}
417+
418+
extension FeatureCollection {
419+
420+
public init(from decoder: Decoder) throws {
421+
let container = try decoder.container(keyedBy: FeatureCollectionCodingKeys.self)
422+
423+
let type = try container.decode(GeoJSON.`Type`.self, forKey: .geoJSONType)
424+
guard type == Self.geoJSONType else {
425+
throw DecodingError.typeMismatch(Self.self, DecodingError.Context(
426+
codingPath: container.codingPath,
427+
debugDescription: "Found GeoJSON type '\(type.rawValue)'"
428+
))
429+
}
430+
431+
let features = try container.decodeIfPresent([Feature<Properties>].self, forKey: .features) ?? []
432+
433+
self.init(features: features)
434+
}
435+
436+
public func encode(to encoder: Encoder) throws {
437+
var container = encoder.container(keyedBy: FeatureCollectionCodingKeys.self)
438+
439+
try container.encode(Self.geoJSONType, forKey: .geoJSONType)
440+
try container.encodeIfPresent(self.features, forKey: .features)
441+
// TODO: Create GeoJSONEncoder that allows setting "export bboxes" to a boolean value
442+
// TODO: Memoize bboxes not to recompute them all the time (bboxes tree)
443+
try container.encodeIfPresent(self.bbox, forKey: .bbox)
444+
}
445+
446+
}

Sources/GeoJSON/Objects/Feature.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public struct Feature<
1010
// Geometry: GeoJSON.Geometry,
1111
// BoundingBox: GeoJSON.BoundingBox,
1212
Properties: GeoJSON.Properties
13-
>: GeoJSON.Object, Hashable, Codable {
13+
>: GeoJSON.Object {
1414

1515
public static var geoJSONType: GeoJSON.`Type` { .feature }
1616

Sources/GeoJSON/Objects/Geometry.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import Turf
1010

11-
public protocol Geometry: GeoJSON.Object, Hashable, Codable {
11+
public protocol Geometry: GeoJSON.Object {
1212

1313
static var geometryType: GeoJSON.`Type`.Geometry { get }
1414

Sources/GeoJSON/Objects/Object.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// Copyright © 2022 Rémi Bardon. All rights reserved.
77
//
88

9-
public protocol Object {
9+
public protocol Object: Hashable, Codable {
1010

1111
associatedtype BoundingBox: GeoJSON.BoundingBox
1212

Tests/GeoJSONTests/GeoJSON+DecodableTests.swift

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import XCTest
1111

1212
final class GeoJSONDecodableTests: XCTestCase {
1313

14+
// MARK: Specification tests
15+
1416
func testPosition2DDecode() throws {
1517
let string: String = "[-1.55366,47.21881]"
1618
let data: Data = try XCTUnwrap(string.data(using: .utf8))
@@ -177,4 +179,192 @@ final class GeoJSONDecodableTests: XCTestCase {
177179
XCTAssertEqual(feature, expected)
178180
}
179181

182+
// MARK: Real-world use cases
183+
184+
func testDecodeFeatureProperties() throws {
185+
struct RealWorldProperties: Hashable, Codable {
186+
let prop0: String
187+
}
188+
189+
let string = """
190+
{
191+
"type": "Feature",
192+
"geometry": {
193+
"type": "Point",
194+
"coordinates": [102.0, 0.5]
195+
},
196+
"properties": {
197+
"prop0": "value0"
198+
}
199+
}
200+
"""
201+
202+
let data: Data = try XCTUnwrap(string.data(using: .utf8))
203+
let feature = try JSONDecoder().decode(Feature<RealWorldProperties>.self, from: data)
204+
205+
let expected: Feature = Feature(
206+
geometry: .point2D(Point2D(coordinates: .init(latitude: 0.5, longitude: 102))),
207+
properties: RealWorldProperties(prop0: "value0")
208+
)
209+
XCTAssertEqual(feature, expected)
210+
}
211+
212+
/// Example from [RFC 7946, section 1.5](https://datatracker.ietf.org/doc/html/rfc7946#section-1.5).
213+
func testDecodeHeterogeneousFeatureCollection() throws {
214+
enum HeterogeneousProperties: Hashable, Codable, CustomStringConvertible {
215+
struct Properties1: Hashable, Codable, CustomStringConvertible {
216+
let prop0: String
217+
218+
var description: String {
219+
"{prop0:\"\(prop0)\"}"
220+
}
221+
}
222+
struct Properties2: Hashable, Codable, CustomStringConvertible {
223+
let prop0: String
224+
let prop1: Double
225+
226+
var description: String {
227+
"{prop0:\"\(prop0)\",prop1:\(prop1)}"
228+
}
229+
}
230+
struct Properties3: Hashable, Codable, CustomStringConvertible {
231+
struct Prop1: Hashable, Codable, CustomStringConvertible {
232+
let this: String
233+
234+
var description: String {
235+
"{this:\"\(this)\"}"
236+
}
237+
}
238+
239+
let prop0: String
240+
let prop1: Prop1
241+
242+
var description: String {
243+
"{prop0:\"\(prop0)\",prop1:\(prop1)}"
244+
}
245+
}
246+
247+
enum CodingKeys: String, CodingKey {
248+
case prop0, prop1
249+
}
250+
251+
case type1(Properties1)
252+
case type2(Properties2)
253+
case type3(Properties3)
254+
255+
var description: String {
256+
switch self {
257+
case .type1(let type1):
258+
return type1.description
259+
case .type2(let type2):
260+
return type2.description
261+
case .type3(let type3):
262+
return type3.description
263+
}
264+
}
265+
266+
init(from decoder: Decoder) throws {
267+
let container = try decoder.container(keyedBy: CodingKeys.self)
268+
269+
let prop0 = try container.decode(String.self, forKey: .prop0)
270+
do {
271+
do {
272+
let prop1 = try container.decode(Double.self, forKey: .prop1)
273+
self = .type2(.init(prop0: prop0, prop1: prop1))
274+
} catch {
275+
let prop1 = try container.decode(Properties3.Prop1.self, forKey: .prop1)
276+
self = .type3(.init(prop0: prop0, prop1: prop1))
277+
}
278+
} catch {
279+
self = .type1(.init(prop0: prop0))
280+
}
281+
}
282+
283+
func encode(to encoder: Encoder) throws {
284+
fatalError("Useless")
285+
}
286+
}
287+
288+
let string = """
289+
{
290+
"type": "FeatureCollection",
291+
"features": [{
292+
"type": "Feature",
293+
"geometry": {
294+
"type": "Point",
295+
"coordinates": [102.0, 0.5]
296+
},
297+
"properties": {
298+
"prop0": "value0"
299+
}
300+
}, {
301+
"type": "Feature",
302+
"geometry": {
303+
"type": "LineString",
304+
"coordinates": [
305+
[102.0, 0.0],
306+
[103.0, 1.0],
307+
[104.0, 0.0],
308+
[105.0, 1.0]
309+
]
310+
},
311+
"properties": {
312+
"prop0": "value0",
313+
"prop1": 0.0
314+
}
315+
}, {
316+
"type": "Feature",
317+
"geometry": {
318+
"type": "Polygon",
319+
"coordinates": [
320+
[
321+
[100.0, 0.0],
322+
[101.0, 0.0],
323+
[101.0, 1.0],
324+
[100.0, 1.0],
325+
[100.0, 0.0]
326+
]
327+
]
328+
},
329+
"properties": {
330+
"prop0": "value0",
331+
"prop1": {
332+
"this": "that"
333+
}
334+
}
335+
}]
336+
}
337+
"""
338+
339+
let data: Data = try XCTUnwrap(string.data(using: .utf8))
340+
let feature = try JSONDecoder().decode(FeatureCollection<HeterogeneousProperties>.self, from: data)
341+
342+
let expected: FeatureCollection<HeterogeneousProperties> = FeatureCollection(features: [
343+
Feature(
344+
geometry: .point2D(Point2D(coordinates: .init(latitude: 0.5, longitude: 102))),
345+
properties: .type1(.init(prop0: "value0"))
346+
),
347+
Feature(
348+
geometry: .lineString2D(.init(coordinates: [
349+
.init(latitude: 0.0, longitude: 102.0),
350+
.init(latitude: 1.0, longitude: 103.0),
351+
.init(latitude: 0.0, longitude: 104.0),
352+
.init(latitude: 1.0, longitude: 105.0)
353+
])!),
354+
properties: .type2(.init(prop0: "value0", prop1: 0))
355+
),
356+
Feature(
357+
geometry: .polygon2D(.init(coordinates: .init(arrayLiteral: [
358+
.init(latitude: 0.0, longitude: 100.0),
359+
.init(latitude: 0.0, longitude: 101.0),
360+
.init(latitude: 1.0, longitude: 101.0),
361+
.init(latitude: 1.0, longitude: 100.0),
362+
.init(latitude: 0.0, longitude: 100.0)
363+
]))),
364+
properties: .type3(.init(prop0: "value0", prop1: .init(this: "that")))
365+
),
366+
])
367+
XCTAssertEqual(feature, expected)
368+
}
369+
180370
}

0 commit comments

Comments
 (0)