Skip to content
This repository has been archived by the owner on Aug 16, 2019. It is now read-only.

JSON parsing and factories #7

Merged
merged 30 commits into from
Jan 19, 2017
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
823cb16
ADD first draft for component factories. Not happy yet; WIP
Jan 11, 2017
6eb292f
ADD JSONReader to read a json file into a dictionary (JSONObject)
AndreasThenn Jan 12, 2017
8c6bc88
ADD unit tests for JSONReader
Jan 12, 2017
ba91bc7
REM line_length setting for SwiftLint again, because new SwiftLint ve…
AndreasThenn Jan 12, 2017
6ecf8d0
Add JSONFactory wrapper
joanromano Jan 12, 2017
77b4626
Update swiftlint tests file
joanromano Jan 12, 2017
df43352
Add tests to JSONReader
joanromano Jan 12, 2017
d6c16f6
Add JSONFactoryError for JSONFactory error handling
joanromano Jan 12, 2017
2a4fd05
Minor header fixes
joanromano Jan 12, 2017
be460f6
Fix swift lint rules with new version
joanromano Jan 13, 2017
f524260
Update project set up
joanromano Jan 13, 2017
26acb59
Update app_structure.json and tests to reflect current schema
joanromano Jan 13, 2017
62ca01d
Refactor using a single JSONFactory which holds builder closures
joanromano Jan 13, 2017
06b4bcd
Remove swiftlint line length since we use the default
joanromano Jan 13, 2017
d8ff008
Add public init in JSONFactory
joanromano Jan 13, 2017
98a5b1d
Merge branch 'master' into feature/jsonParsing
joanromano Jan 13, 2017
d218084
Fix code in example
joanromano Jan 13, 2017
81a720f
Fix line length violation on ExpressibleByComponentMeta documentation
joanromano Jan 13, 2017
b3cc35b
CRC
joanromano Jan 16, 2017
e030bf4
Use JSONObject type when possible
joanromano Jan 16, 2017
dadde5d
Re build example
joanromano Jan 18, 2017
ae229e1
Added gemfile to fix buddybuild
Jan 18, 2017
452a56b
Added another gemfile
Jan 18, 2017
50dcde2
ADD danger to the gemfile
Jan 18, 2017
abd0a79
Add danger-swiftlint
Jan 18, 2017
c80a885
Update documentation
joanromano Jan 19, 2017
57b6d0b
Update code and documentation with snake case keys
joanromano Jan 19, 2017
8119b1b
Remove fast test
joanromano Jan 19, 2017
6a9d36a
Update json example with snake case
joanromano Jan 19, 2017
63a84b9
Fix link on Readme
joanromano Jan 19, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Example/MatrioskaExample/TileViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import UIKit
import Matrioska

struct TileConfig: MaterializableComponentMeta {
struct TileConfig: ExpressibleByComponentMeta {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe setup buddybuild quickly so we can make sure to not break the example

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do so

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (wanted to see how it works), I created a runtastic team on buddybuild. Will invite you guys. Btw I think buddybuild also supports libraries now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nevermind that's only for payed plans.

let text: String?
let color: UIColor?

Expand Down
4 changes: 2 additions & 2 deletions Example/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PODS:
- Matrioska (0.1.0):
- Matrioska (0.1.0-alpha1):
- SnapKit (~> 3.0)
- SnapKit (3.0.2)

Expand All @@ -12,7 +12,7 @@ EXTERNAL SOURCES:
:path: "../Matrioska.podspec"

SPEC CHECKSUMS:
Matrioska: 872321eb578deb7af60323aa61126ee13e0d3fbd
Matrioska: b070c1ad36e64bdf7ab9d847033e2a5e4d2b8ec7
SnapKit: 2e456761aa92d4d4067a7a5594c18769d451a8ad

PODFILE CHECKSUM: b63a58336b9471a5dbe1ffe7ea7d0b94f6dbf667
Expand Down
10 changes: 5 additions & 5 deletions Example/Pods/Local Podspecs/Matrioska.podspec.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Example/Pods/Manifest.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

629 changes: 334 additions & 295 deletions Example/Pods/Pods.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

100 changes: 63 additions & 37 deletions Matrioska.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0810"
LastUpgradeVersion = "0820"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ extension ExpressibleByComponentMeta {
/// or an already materialized meta object.
/// - Returns: A materialized `ExpressibleByComponentMeta` object if the input represents correctly
/// the object to be materialized.
/// - Note: This will return nil when `meta` is nil or will return the same `meta` object when `meta` is already a `Self` type.
/// - Note: This will return nil when `meta` is nil or
/// will return the same `meta` object when `meta` is already a `Self` type.
public static func materialize(_ meta: ComponentMeta?) -> Self? {
guard let meta = meta else {
return nil
Expand Down
105 changes: 105 additions & 0 deletions Source/Factories/JSONFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//
// JSONFactory.swift
// Matrioska
//
// Created by Mathias Aichinger on 11/01/2017.
// Copyright © 2017 runtastic. All rights reserved.
//

import Foundation

/// A JSONObject type
public typealias JSONObject = [String: Any]

/// An error type for JSONFactory
///
/// - missing: specifies that a mandatory key is missing
enum JSONFactoryError: Error {
case missing(JSONObject, String)
}

/// A factory that wraps `Component` builder closures (ViewFactoryBuilder,
/// `WrapperFactoryBuilder` & `ClusterFactoryBuilder`) and uses them to produce `Component`s
public final class JSONFactory {

private enum Key {
static let type = "type"
static let meta = "meta"
static let children = "children"
}

/// A factory closure to build a view `Component`
public typealias ViewFactoryBuilder = (ComponentMeta?) -> Component

/// A factory closure to build a wrapper `Component`
public typealias WrapperFactoryBuilder = (Component, ComponentMeta?) -> Component

/// A factory closure to build a cluster `Component`
public typealias ClusterFactoryBuilder = ([Component], ComponentMeta?) -> Component

private var viewFactory: [String: ViewFactoryBuilder] = [:]
private var wrapperFactory: [String: WrapperFactoryBuilder] = [:]
private var clusterFactory: [String: ClusterFactoryBuilder] = [:]

/// Initialize a new JSONFactory
public init() {
// Empty but needed to be initialized from other modules
}

/// Registers a new `ViewFactoryBuilder` which will be used when producing the component
///
/// - Parameters:
/// - type: a string identifying this factory type
/// - factoryBuilder: a `ViewFactoryBuilder` to build a `Component`
public func register(with type: String,
factoryBuilder: @escaping ViewFactoryBuilder) {
viewFactory[type] = factoryBuilder
}

/// Registers a new `WrapperFactoryBuilder` which will be used when producing the component
///
/// - Parameters:
/// - type: a string identifying this factory type
/// - factoryBuilder: a `WrapperFactoryBuilder` to build a `Component`
public func register(with type: String,
factoryBuilder: @escaping WrapperFactoryBuilder) {
wrapperFactory[type] = factoryBuilder
}

/// Registers a new `ClusterFactoryBuilder` which will be used when producing the component
///
/// - Parameters:
/// - type: a string identifying this factory type
/// - factoryBuilder: a `ClusterFactoryBuilder` to build a `Component`
public func register(with type: String,
factoryBuilder: @escaping ClusterFactoryBuilder) {
clusterFactory[type] = factoryBuilder
}

/// Produces a `Component` from a given `JSONObject`, which has one mandatory key: `type`
///
/// - Parameter json: the `JSONObject` to be used
/// - Returns: An optional `Component`
/// - Throws: `JSONFactoryError` when a mandatory key is missing
public func component(from json: JSONObject) throws -> Component? {
guard let type = json[Key.type] as? String else {
throw JSONFactoryError.missing(json, Key.type)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be a possibility to register fallback components. So eg. for clusters always fallback to stacks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned this on last meeting and they said we don't include fallback components for now? We could implement it here though.

}

let meta = json[Key.meta] as? JSONObject
let children = json[Key.children] as? [JSONObject] ?? []
let componentChildren = try children.flatMap { try component(from: $0) }
var componentResult: Component?

if let viewFactory = viewFactory[type] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would do the matching differently:

let id = ....
if let children = ... { // is optional
 // then is a cluster
} else if let child = ... { 
 // then is a wrapper
} else {
  // must be a view
}

But this depends by the json specs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now we would have the same key for children in the json

componentResult = viewFactory(meta)
} else if let wrapperFactory = wrapperFactory[type],
let componentChild = componentChildren.first {
componentResult = wrapperFactory(componentChild, meta)
} else if let clusterFactory = clusterFactory[type] {
componentResult = clusterFactory(componentChildren, meta)
}

return componentResult
}
}
17 changes: 6 additions & 11 deletions Tests/.swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
disabled_rules:
- trailing_whitespace # xcode add it by default, we might enable it and use autocorrect
- function_body_length
- force_cast
- force_try
- force_unwrapping

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why removed all these rules?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the swiftlint for tests so I disabled them only here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true!

line_length: 200 # goal: 110

opt_in_rules:
- empty_count
- force_unwrapping
- private_outlet
- vertical_whitespace
# - missing_docs # broken
- closure_spacing
- conditional_returns_on_newline
- explicit_init
- overridden_super_call
- redundant_nil_coalesing
# - switch_case_on_newline # broken

type_body_length:
warning: 800
Expand All @@ -23,4 +18,4 @@ file_length:
warning: 1000
error: 1500

reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit)
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit)
71 changes: 71 additions & 0 deletions Tests/Assets/app_structure.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"structure": {
"type": "tabbar",
"meta": {
"default_tab_id": "main_tab"
},
"children": [
{
"type": "stack",
"meta": {
"tab_config": {
"title": {
"key": "history_title",
"type": "localized_string"
},
"icon_name": "history_tab_icon"
}
},
"children": [
{
"type": "table_view"
},
{
"type": "navigation"
}
]
},
{
"type": "stack",
"meta": {
"tab_config": {
"title": {
"key": "main_tab_title",
"type": "localized_string"
},
"iconName": "main_tab_icon"
}
},
"children": [
{
"type": "label"
},
{
"type": "stack",
"meta": {
"requires_navigation": "true",
"orientation": "horizontal"
}
},
{
"type": "button",
"meta": {
"title": {
"key": "start_button_title",
"type": "localized_string"
}
}
}
]
},
{
"type": "navigation",
"children": [
{
"type": "label"
}
]
}
]
}
}
4 changes: 4 additions & 0 deletions Tests/Assets/invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"structure": "test"
"test": "test"
}
2 changes: 1 addition & 1 deletion Tests/ComponentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class ComponentTests: QuickSpec {

override func spec() {

typealias DictMeta = Dictionary<String, String>
typealias DictMeta = [String: String]

describe("View component") {
it("should build a viewController") {
Expand Down
42 changes: 42 additions & 0 deletions Tests/Helpers/JSONReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// JSONReader.swift
// Matrioska
//
// Created by Andreas Thenn on 12/01/2017.
// Copyright © 2017 runtastic. All rights reserved.
//

import Foundation
@testable import Matrioska

/// A JSONReader used to convert to JSONObject
final class JSONReader {

/// Serializes from a given JSON Data into JSONObject
///
/// - Parameter data: the Data object used in serialization
/// - Returns: an optional serialized JSONObject
/// - Throws: throws an error in case of failure or invalid JSON data
class func jsonObject(from data: Data) throws -> JSONObject? {
let json = try JSONSerialization.jsonObject(with: data) as? JSONObject

return json
}

/// Serializes from a given JSON file into JSONObject
///
/// - Parameters:
/// - jsonFilename: the file name
/// - bundle: the bundle where the file is located
/// - Returns: an optional serialized JSONObject
/// - Throws: throws an error in case of failure or invalid JSON data
class func jsonObject(from jsonFilename: String, bundle: Bundle = .main) throws -> JSONObject? {
guard let filePath = bundle.path(forResource: jsonFilename, ofType: "json") else {
return nil
}

let url = URL(fileURLWithPath: filePath)

return try jsonObject(from: Data(contentsOf: url, options: .uncached))
}
}