Skip to content

[Feature] Create JSONMapper framework #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking β€œSign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
https://github.com/nimblehq/git-template/issues/??
https://github.com/nimblehq/JSONMapper/issues/??

## What happened πŸ‘€

Expand Down
83 changes: 47 additions & 36 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,36 +1,47 @@
*.gem
*.rbc
/.config
/.idea
/coverage/
/InstalledFiles
/node-modules
/pkg/
/spec/reports/
/spec/examples.txt
/test/tmp/
/test/version_tmp/
/tmp/

# Used by dotenv library to load environment variables.
# .env

## Documentation cache and generated files:
/.yardoc/
/_yardoc/
/doc/
/rdoc/

## Environment normalization:
/.bundle/
/vendor/bundle
/lib/bundler/man/

# for a library or gem, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# Gemfile.lock
# .ruby-version
# .ruby-gemset

# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
.rvmrc
# Created by https://www.gitignore.io/api/swift
# Edit at https://www.gitignore.io/?templates=swift

### Swift ###
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## Build generated
build/
DerivedData/

## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/

## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint

## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM

## Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
.build/

# End of https://www.gitignore.io/api/swift
.DS_Store
28 changes: 28 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "JSONMapper",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "JSONMapper",
targets: ["JSONMapper"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "JSONMapper",
dependencies: []),
.testTarget(
name: "JSONMapperTests",
dependencies: ["JSONMapper"]),
]
)
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
# Git Repository Template
# JSONMapper

Project repository template to set up all public projects at [Nimble](https://nimblehq.co/)
An iOS library supports mapping json data

## Usage
## Installation

Clone the repository
### Swift Package Manager

The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift compiler. It is in early development, but Alamofire does support its use on supported platforms.

Once you have your Swift package set up, adding Alamofire as a dependency is as easy as adding it to the dependencies value of your Package.swift.

dependencies: [
.package(url: "https://github.com/nimblehq/JSONMapper.git", .upToNextMajor(from: "1.0.0"))
]

`git clone git@github.com:nimblehq/git-template.git`

## License

Expand Down
21 changes: 21 additions & 0 deletions Sources/JSONMapper/Extensions/Encodable+Dictionary.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Encodable+Dictionary.swift
// NimbleExtension
//
// Created by Tam Nguyen on 11/17/20.
// Copyright Β© 2020 Nimble. All rights reserved.
//

import Foundation

public extension Encodable {

func toDictionary(_ encoder: JSONEncoder = JSONEncoder()) -> [String: Any] {
guard let dictionary = try? JSONSerialization.jsonObject(
with: encoder.encode(self), options: .allowFragments
) as? [String: Any] else {
return [:]
}
return dictionary
}
}
35 changes: 35 additions & 0 deletions Sources/JSONMapper/Extensions/Optional+Extras.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Optional+Or.swift
//
// Created by Edgars Simanovskis on 27/08/2019.
// Copyright Β© 2018 Nimble. All rights reserved.
//

public extension Optional {

var isNil: Bool {
return self == nil
}

var hasValue: Bool { !isNil }

func or(_ otherOptional: @autoclosure () throws -> Wrapped?) rethrows -> Wrapped? {
switch self {
case .some(let value): return value
case .none: return try otherOptional()
}
}

func or(_ otherWrapped: @autoclosure () throws -> Wrapped) rethrows -> Wrapped {
switch self {
case .some(let value): return value
case .none: return try otherWrapped()
}
}

func resolve(with error: @autoclosure () -> Error) throws -> Wrapped {
switch self {
case .none: throw error()
case .some(let wrapped): return wrapped
}
}
}
135 changes: 135 additions & 0 deletions Sources/JSONMapper/JSONAPIDecoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// JSONAPIDecoder.swift
//
// Created by Pirush Prechathavanich on 4/4/18.
// Copyright Β© 2018 Nimble. All rights reserved.
//

import Foundation

public class JSONAPIDecoder: JSONDecoder {

private typealias ResourceDictionary = [ResourceIdentifier: Resource]

private let decoder: JSONEncoder

init(decoder: JSONEncoder = JSONEncoder()) {
self.decoder = decoder
}

public override func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
let jsonAPIObject = try super.decode(JSONAPIObject.self, from: data)

let includedData = jsonAPIObject.included ?? []
let dictionary = includedDictionary(from: includedData)

switch jsonAPIObject.type {
case .data(let data):
return try decode(data, including: dictionary, into: type)
case .meta(let meta):
return try decode(meta, into: type)
case .errors(let errors):
throw errors
}
}

public func decodeWithMeta<Value: Decodable, Meta: Decodable>(
value valueType: Value.Type,
meta metaType: Meta.Type,
from data: Data
) throws -> (value: Value, meta: Meta) {
let jsonAPIObject = try super.decode(JSONAPIObject.self, from: data)

let includedData = jsonAPIObject.included ?? []
let dictionary = includedDictionary(from: includedData)

switch jsonAPIObject.type {
case .data(let data):
return (
value: try decode(data, including: dictionary, into: valueType),
meta: try decode(jsonAPIObject.meta ?? .nil, into: metaType)
)
case .meta(let meta):
throw Errors.JSONAPIDecodingError.unableToDecode(
reason: "No data field. Only contains meta: \(meta)"
)
case .errors(let errors):
throw errors
}
}

}

// MARK: - Private

extension JSONAPIDecoder {

private func decode<T: Decodable>(_ meta: JSON, into type: T.Type) throws -> T {
let data = try decoder.encode(meta)
return try super.decode(type, from: data)
}

private func decode<T: Decodable>(_ dataType: DataType<Resource>,
including includedDictionary: ResourceDictionary,
into type: T.Type) throws -> T {
switch dataType {
case .single(let resource):
return try decode(resource, including: includedDictionary, into: type)
case .collection(let resources):
return try decodeCollection(of: resources, including: includedDictionary, into: type)
}
}

private func decode<T: Decodable>(_ resource: Resource,
including includedDictionary: ResourceDictionary,
into type: T.Type) throws -> T {
let dictionary = try resolvedAttributes(of: resource, including: includedDictionary)
let data = try decoder.encode(dictionary)
return try super.decode(type, from: data)
}

private func decodeCollection<T: Decodable>(of resources: [Resource],
including includedDictionary: ResourceDictionary,
into type: T.Type) throws -> T {
let collection = try resources.compactMap { try resolvedAttributes(of: $0, including: includedDictionary) }
let data = try decoder.encode(collection)
return try super.decode(type, from: data)
}

private func includedDictionary(from includedData: [Resource]) -> ResourceDictionary {
return includedData.reduce(into: [:]) { dictionary, resource in
let identifier = ResourceIdentifier(id: resource.id, type: resource.type)
dictionary[identifier] = resource
}
}

private func resolvedAttributes(of resource: Resource,
including includedDictionary: ResourceDictionary) throws -> JSON? {
var attributes = resource.attributes?.nested ?? [:]
attributes[Resource.CodingKeys.id.rawValue] = .string(resource.id)
attributes[Resource.CodingKeys.type.rawValue] = .string(resource.type)

try resource.relationships?.forEach { key, relationship in
guard let type = relationship.data else { return }
switch type {
case .single(let identifier):
let includedResource = try getResource(from: includedDictionary, for: identifier)
attributes[key] = try resolvedAttributes(of: includedResource, including: includedDictionary)

case .collection(let identifiers):
let includedAttributes = try identifiers
.map { try getResource(from: includedDictionary, for: $0) }
.compactMap { try resolvedAttributes(of: $0, including: includedDictionary) }
attributes[key] = .array(includedAttributes)
}
}
return .nested(attributes)
}

private func getResource(from includedDictionary: ResourceDictionary,
for identifier: ResourceIdentifier) throws -> Resource {
guard let resource = includedDictionary[identifier] else {
throw Errors.JSONAPIDecodingError.resourceNotFound(identifier: identifier)
}
return resource
}
}
30 changes: 30 additions & 0 deletions Sources/JSONMapper/Models/DataType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// DataType.swift
//
// Created by Edgars Simanovskis on 27/08/2019.
// Copyright Β© 2018 Nimble. All rights reserved.
//

import Foundation

public enum DataType<T: Codable>: Codable {

case single(T)
case collection([T])

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try .single(container.decode(T.self))
} catch DecodingError.typeMismatch {
self = try .collection(container.decode([T].self))
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .single(let object): try container.encode(object)
case .collection(let array): try container.encode(array)
}
}
}
Loading