Skip to content

Commit

Permalink
feat: added options for custom key case style (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
soumyamahunt committed Sep 20, 2023
1 parent 6cd519e commit 5cc1a93
Show file tree
Hide file tree
Showing 18 changed files with 1,277 additions and 68 deletions.
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
"**/.docc-build": true,
"**/node_modules": true,
"Package.resolved": true
}
},
"editor.unicodeHighlight.allowedCharacters": {
"-": true
},
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Supercharge `Swift`'s `Codable` implementations with macros.
- Generates member-wise initializer(s) considering the above default value syntax as well.
- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` etc.
- Allows to ignore specific properties from decoding/encoding with ``IgnoreCoding()``, ``IgnoreDecoding()`` and ``@IgnoreEncoding()``.
- Allows to use camel-case names for variables according to [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/#general-conventions), while enabling a type to work with different case style keys with ``CodingKeys(_:)``.
- Allows to ignore all initialized properties of a type from decoding/encoding with ``IgnoreCodingInitialized()`` unless explicitly asked to decode/encode by attaching any coding attributes, i.e. ``CodedIn(_:)``, ``CodedAt(_:)``,
``CodedBy(_:)``, ``Default(_:)`` etc.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ extension Codable: ConformanceMacro, MemberMacro {
else { return }

// builder
let builder = IgnoreCodingInitialized(from: declaration)
let builder = CodingKeys(from: declaration)
|> IgnoreCodingInitialized(from: declaration)
|> KeyPathRegistrationBuilder(
provider: CodedAt(from: decl)
?? CodedIn(from: decl)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import Foundation

/// A type performing transformation on provided `CodingKey`.
///
/// Performs transformation on provided `CodingKey`
/// based on the strategy passed during initialization.
/// The separation letter and separated words capitalization
/// style is adjusted according to the provided case style.
struct CodingKeyTransformer {
/// The key transformation strategy provided.
let strategy: CodingKeys.Strategy

/// Transform provided `CodingKey` string according
/// to current strategy.
///
/// Adjusts elements in provided `CodingKey` to match
/// the current casing strategy.
///
/// - Parameter key: The `CodingKey` to transform.
/// - Returns: The transformed `CodingKey`.
func transform(key: String) -> String {
guard !key.isEmpty else { return key }

let interimKey: String
if #available(
macOS 13, iOS 16, macCatalyst 16,
tvOS 16, watchOS 9, *
) {
let regex = #/([a-z0-9])([A-Z])/#
interimKey = key.replacing(regex) { match in
let (_, first, second) = match.output
return "\(first)@\(second)"
}.lowercased()
} else {
let regex = try! NSRegularExpression(pattern: "([a-z0-9])([A-Z])")
let range = NSRange(location: 0, length: key.count)
interimKey = regex.stringByReplacingMatches(
in: key,
range: range,
withTemplate: "$1@$2"
).lowercased()
}

let parts = interimKey.components(separatedBy: .alphanumerics.inverted)
return strategy.capitalization
.transform(parts: parts)
.joined(separator: strategy.separator)
}
}

fileprivate extension CodingKeys.Strategy {
/// The separator being used by current case style.
///
/// There might not be any separator for current case style,
/// in such case empty string is returned. Otherwise the separator
/// character corresponding to current case is returned.
var separator: String {
switch self {
case .camelCase, .PascalCase:
return ""
case .snake_case, .camel_Snake_Case, .SCREAMING_SNAKE_CASE:
return "_"
case .kebab-case, .Train-Case, .SCREAMING-KEBAB-CASE:
return "-"
}
}
}

fileprivate extension CodingKeys.Strategy {
/// Represents capitalization style
/// of each token in a casing style.
///
/// Indicates capitalization style preferred
/// by each separated word in a casing style,
/// i.e. upper, lower, only first letter is capitalized etc.
enum Capitalization {
/// Represents all the separated
/// words are in upper case.
///
/// Typically used for screaming
/// style cases with separators.
case upper
/// Represents all the separated words
/// have only first letter capitalized.
///
/// Typically used for default
/// style cases with separators.
case lower
/// Represents all the separated
/// words are in lower case.
///
/// Typically used for default
/// style cases with separators.
case all
/// Represents first word is in lower case
/// and subsequent words have only
/// first letter capitalized.
///
/// Typically used for styles that are variation
/// on top of default styles.
case exceptFirst

/// Converts provided string tokens according
/// to current casing style.
///
/// Adjusts capitalization style of provided string tokens
/// according to current casing style.
///
/// - Parameter parts: The string tokens to transform.
/// - Returns: The transformed string tokens.
func transform(parts: [String]) -> [String] {
guard !parts.isEmpty else { return parts }
switch self {
case .upper:
return parts.map { $0.uppercased() }
case .lower:
return parts.map { $0.lowercased() }
case .all:
return parts.map { $0.uppercasingFirst }
case .exceptFirst:
let first = parts.first!.lowercasingFirst
let rest = parts.dropFirst().map { $0.uppercasingFirst }
return [first] + rest
}
}
}

/// The capitalization casing style of each pattern
/// corresponding to current strategy.
///
/// Depending on the current style it might be upper,
/// lower or capitalizing first word etc.
var capitalization: Capitalization {
switch self {
case .camelCase, .camel_Snake_Case:
return .exceptFirst
case .snake_case, .kebab-case:
return .lower
case .SCREAMING_SNAKE_CASE, .SCREAMING-KEBAB-CASE:
return .upper
case .PascalCase, .Train-Case:
return .all
}
}
}

/// Helps converting any string to camel case
///
/// Picked up from:
/// https://gist.github.com/reitzig/67b41e75176ddfd432cb09392a270218
extension String {
/// Makes the first letter lowercase.
var lowercasingFirst: String { prefix(1).lowercased() + dropFirst() }
/// Makes the first letter uppercase.
var uppercasingFirst: String { prefix(1).uppercased() + dropFirst() }

/// Convert any string to camel case
///
/// Removes non-alphanumeric characters
/// and makes the letters just after these
/// characters uppercase.
///
/// First letter is made lowercase.
var camelCased: String {
return CodingKeyTransformer(strategy: .camelCase).transform(key: self)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import SwiftSyntax

/// Attribute type for `CodingKeys` macro-attribute.
///
/// This type can validate`CodingKeys` macro-attribute
/// usage and extract data for `Codable` macro to
/// generate implementation.
///
/// Attaching this macro to type declaration indicates all the
/// property names will be converted to `CodingKey` value
/// using the strategy provided.
struct CodingKeys: PeerAttribute {
/// The node syntax provided
/// during initialization.
let node: AttributeSyntax

/// The key transformation strategy provided.
var strategy: Strategy {
let expr = node.argument!
.as(TupleExprElementListSyntax.self)!.first!.expression
return .init(with: expr)
}

/// Creates a new instance with the provided node
///
/// The initializer fails to create new instance if the name
/// of the provided node is different than this attribute.
///
/// - Parameter node: The attribute syntax to create with.
/// - Returns: Newly created attribute instance.
init?(from node: AttributeSyntax) {
guard
node.attributeName.as(SimpleTypeIdentifierSyntax.self)!
.description == Self.name
else { return nil }
self.node = node
}

/// Builds diagnoser that can validate this macro
/// attached declaration.
///
/// Builds diagnoser that validates attached declaration
/// has `Codable` macro attached and macro usage
/// is not duplicated for the same declaration.
///
/// - Returns: The built diagnoser instance.
func diagnoser() -> DiagnosticProducer {
return AggregatedDiagnosticProducer {
mustBeCombined(with: Codable.self)
cantDuplicate()
}
}
}

0 comments on commit 5cc1a93

Please sign in to comment.