diff --git a/Package.swift b/Package.swift
index ccd22ec..e35edd8 100644
--- a/Package.swift
+++ b/Package.swift
@@ -10,7 +10,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://source.skip.tools/skip.git", from: "1.0.10"),
- .package(url: "https://source.skip.tools/skip-lib.git", from: "1.1.0"),
+ .package(url: "https://source.skip.tools/skip-lib.git", from: "1.1.1"),
],
targets: [
.target(name: "SkipFoundation", dependencies: [.product(name: "SkipLib", package: "skip-lib")], plugins: [.plugin(name: "skipstone", package: "skip")]),
diff --git a/README.md b/README.md
index dd04776..e99b8dc 100644
--- a/README.md
+++ b/README.md
@@ -416,26 +416,12 @@ Support levels:
- 🟢 |
-
-
- JSONDecoder
-
-- Snake case key decoding is not supported
-
-
- |
+ ✅ |
+ JSONDecoder |
- 🟢 |
-
-
- JSONEncoder
-
-- Snake case key encoding is not supported
-
-
- |
+ ✅ |
+ JSONEncoder |
✅ |
diff --git a/Sources/SkipFoundation/JSONDecoder.swift b/Sources/SkipFoundation/JSONDecoder.swift
index c733be7..c6ee3f1 100644
--- a/Sources/SkipFoundation/JSONDecoder.swift
+++ b/Sources/SkipFoundation/JSONDecoder.swift
@@ -51,8 +51,49 @@ open class JSONDecoder {
case convertFromSnakeCase
case custom((_ codingPath: [CodingKey]) -> CodingKey)
- fileprivate static func _convertFromSnakeCase(_ key: String) -> String {
- fatalError("SKIP TODO: JSON snakeCase")
+ fileprivate static func _convertFromSnakeCase(_ stringKey: String) -> String {
+ guard !stringKey.isEmpty else { return stringKey }
+
+ // Find the first non-underscore character
+ guard let firstNonUnderscore = stringKey.firstIndex(where: { $0 != "_" }) else {
+ // Reached the end without finding an _
+ return stringKey
+ }
+
+ // Find the last non-underscore character
+ var lastNonUnderscore = stringKey.count - 1
+ while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" {
+ lastNonUnderscore -= 1
+ }
+
+ let keyRange = firstNonUnderscore...lastNonUnderscore
+ let leadingUnderscoreRange = 0..: KeyedDecodingCon
init(keyedBy: Any.Type, impl: JSONDecoderImpl, codingPath: [CodingKey], dictionary: Dictionary) {
self.impl = impl
self.codingPath = codingPath
- let decodeKeys = keyedBy == DictionaryCodingKey.self
+ let decodeKeys = keyedBy != DictionaryCodingKey.self
if decodeKeys {
switch impl.options.keyDecodingStrategy {
case .useDefaultKeys:
diff --git a/Sources/SkipFoundation/JSONEncoder.swift b/Sources/SkipFoundation/JSONEncoder.swift
index 5e17881..cb096e5 100644
--- a/Sources/SkipFoundation/JSONEncoder.swift
+++ b/Sources/SkipFoundation/JSONEncoder.swift
@@ -62,51 +62,61 @@ open class JSONEncoder {
fileprivate static func _convertToSnakeCase(_ stringKey: String) -> String {
guard !stringKey.isEmpty else { return stringKey }
- fatalError("SKIP TODO: JSON snakeCase")
-// var words: [Range] = []
-// // The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase
-// //
-// // myProperty -> my_property
-// // myURLProperty -> my_url_property
-// //
-// // We assume, per Swift naming conventions, that the first character of the key is lowercase.
-// var wordStart = stringKey.startIndex
-// var searchRange = stringKey.index(after: wordStart)..1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
-// let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound)
-// words.append(upperCaseRange.lowerBound..] = []
+ // The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase
+ //
+ // myProperty -> my_property
+ // myURLProperty -> my_url_property
+ //
+ // We assume, per Swift naming conventions, that the first character of the key is lowercase.
+ var wordStart = 0
+ var searchStart = 1
+ var searchEnd = stringKey.count
+
+ func indexOfCharacterCase(upper: Bool, in string: String, searchStart: Int, searchEnd: Int) -> Int? {
+ for i in searchStart..1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
+ let beforeLowerIndex = lowerCaseIndex - 1
+ words.append(upperCaseIndex..: KeyedEncodingContaine
self.impl = impl
self.object = impl.object!
self.codingPath = codingPath
- self.encodeKeys = keyedBy == DictionaryCodingKey.self
+ self.encodeKeys = keyedBy != DictionaryCodingKey.self
}
init(keyedBy: Any.Type, impl: JSONEncoderImpl, object: JSONFuture.RefObject, codingPath: [CodingKey]) {
self.impl = impl
self.object = object
self.codingPath = codingPath
- self.encodeKeys = keyedBy == DictionaryCodingKey.self
+ self.encodeKeys = keyedBy != DictionaryCodingKey.self
}
private func _converted(_ key: JSONEncoderKey) -> CodingKey {
diff --git a/Tests/SkipFoundationTests/Foundation/JSONTests.swift b/Tests/SkipFoundationTests/Foundation/JSONTests.swift
index 38fc8c4..6cb2dd8 100644
--- a/Tests/SkipFoundationTests/Foundation/JSONTests.swift
+++ b/Tests/SkipFoundationTests/Foundation/JSONTests.swift
@@ -225,10 +225,13 @@ class TestJSON : XCTestCase {
}
/// Round-trip a type
- @inline(__always) private func roundtrip(value: T, fmt: JSONEncoder.OutputFormatting? = .sortedKeys, data: JSONEncoder.DataEncodingStrategy? = nil, date: JSONEncoder.DateEncodingStrategy? = nil, floats: JSONEncoder.NonConformingFloatEncodingStrategy? = nil, keys: JSONEncoder.KeyEncodingStrategy? = nil) throws -> String where T : Encodable, T : Decodable, T : Equatable {
+ @inline(__always) private func roundtrip(value: T, fmt: JSONEncoder.OutputFormatting? = .sortedKeys, data: JSONEncoder.DataEncodingStrategy? = nil, date: JSONEncoder.DateEncodingStrategy? = nil, floats: JSONEncoder.NonConformingFloatEncodingStrategy? = nil, keys: JSONEncoder.KeyEncodingStrategy? = nil, dkeys: JSONDecoder.KeyDecodingStrategy? = nil) throws -> String where T : Encodable, T : Decodable, T : Equatable {
let json = try enc(value, fmt: fmt, data: data, date: date, floats: floats, keys: keys)
- let decoder = JSONDecoder()
+ let decoder = JSONDecoder()
+ if let dkeys {
+ decoder.keyDecodingStrategy = dkeys
+ }
let value2 = try decoder.decode(T.self, from: json.data(using: String.Encoding.utf8)!)
XCTAssertEqual(value, value2)
@@ -341,8 +344,37 @@ class TestJSON : XCTestCase {
}
""", try roundtrip(value: testData, fmt: [.prettyPrinted, .sortedKeys] as JSONEncoder.OutputFormatting))
#endif
-
- #if !SKIP
+
+ #if SKIP
+ XCTAssertEqual("""
+ {
+ "this_is_a_bool" : true,
+ "this_is_a_date" : 12345,
+ "this_is_a_dictionary" : {
+ "X" : true,
+ "Y" : false
+ },
+ "this_is_a_double" : 12,
+ "this_is_a_float" : 11,
+ "this_is_a_string" : "ABC",
+ "this_is_a_uint" : 6,
+ "this_is_a_uint16" : 8,
+ "this_is_a_uint32" : 9,
+ "this_is_a_uint64" : 10,
+ "this_is_a_uint8" : 7,
+ "this_is_an_array" : [
+ -1,
+ 0,
+ 1
+ ],
+ "this_is_an_int" : 1,
+ "this_is_an_int16" : 3,
+ "this_is_an_int32" : 4,
+ "this_is_an_int64" : 5,
+ "this_is_an_int8" : 2
+ }
+ """, try enc(testData, fmt: [.prettyPrinted, .sortedKeys] as JSONEncoder.OutputFormatting, keys: .convertToSnakeCase as JSONEncoder.KeyEncodingStrategy))
+ #else
XCTAssertEqual("""
{
"this_is_a_bool" : true,
@@ -387,6 +419,7 @@ class TestJSON : XCTestCase {
"lastName" : "Doe"
}
""", try roundtrip(value: p1, fmt: [.prettyPrinted, .sortedKeys] as JSONEncoder.OutputFormatting))
+ XCTAssertEqual(#"{"first_name":"Jon","height":180.5,"last_name":"Doe"}"#, try roundtrip(value: p1, keys: JSONEncoder.KeyEncodingStrategy.convertToSnakeCase, dkeys: JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase))
let org = Org(head: p2, people: [p1, p3], departmentHeads: ["X":p2, "Y": p3], departmentMembers: ["Y":[p1], "X": [p2, p1]])
XCTAssertEqual("""