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

JSON parsing and factories #7

merged 30 commits into from Jan 19, 2017

Conversation

joanromano
Copy link
Contributor

@joanromano joanromano commented Jan 13, 2017

Closes #3

First approach to JSON parsing and factories.

  • JSONFactory wraps registered factory builders (closures which provide Components) and uses them in order to parse JSONObjects and create the main Component

Still to do:

  • Update README documentation, after agreement on the approach. We should also add here a proper documentation on the JSON schema we follow.

@MP0w @mathiasAichinger @AndreasThenn let's do this!

return children
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe we should add this as a public method on Component?

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 it shouldn't be needed (so shouldn't be exposed) but maybe I'm missing the point to use it?

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 will keep it there for now

@codecov-io
Copy link

codecov-io commented Jan 13, 2017

Current coverage is 99.16% (diff: 100%)

Merging #7 into master will increase coverage by 0.10%

@@             master         #7   diff @@
==========================================
  Files             7          8     +1   
  Lines           214        240    +26   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits            212        238    +26   
  Misses            2          2          
  Partials          0          0          

Sunburst

Powered by Codecov. Last update 660b435...6a9d36a

@AndreasThenn
Copy link
Contributor

I think we should update the code (JSONReaderTests, app-structure.json, ..) first, so that it fits to our decision to get rid of the id for now.

@joanromano
Copy link
Contributor Author

@AndreasThenn tests are already updated to check if we assert when no type. I am going to update the app-structure.json to reflect our current schema though, yes.

@mathiasAichinger
Copy link
Contributor

@joanromano JSONFactory, StackClusterFactory, TabBarClusterFactory cannot be intialized from outside the module because they have no public initializer.

@mathiasAichinger
Copy link
Contributor

App Example can be found in feature/jsonParsingExample. Will create a PR after this is finished.

@joanromano
Copy link
Contributor Author

@mathiasAichinger good catch, fixing and updating

let faultyProduceNoKeys = { _ = try! jsonFactory.produce(from: ["foo": "bar"]) }
let faultyProduceNoType = { _ = try! jsonFactory.produce(from: ["id": "bar"]) }

expect(faultyProduceNoKeys()).to(throwAssertion())
Copy link
Contributor

Choose a reason for hiding this comment

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

should not throw an assertion. throws an error. However it's now asserting because you are try!ing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

true, forgot to update after updating error handling

@@ -15,7 +15,7 @@ class ComponentTests: QuickSpec {

override func spec() {

typealias DictMeta = Dictionary<String, String>
typealias DictMeta = [String:String]
Copy link
Contributor

Choose a reason for hiding this comment

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

good but missing a space after :

@@ -112,7 +112,7 @@ class StackClusterTests: QuickSpec {
labelComponent(title: "1", color: .red, labelSize: size),
labelComponent(title: "2", color: .red, labelSize: size),
labelComponent(title: "3", color: .red, labelSize: size),
labelComponent(title: "4", color: .red, labelSize: size),
labelComponent(title: "4", color: .red, labelSize: size)
Copy link
Contributor

Choose a reason for hiding this comment

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

I like trailing comma in collections because you can easily add or move lines... I know swiftlint complains with recent versions but we might disable the rule eventually

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 actually agree with swiftlint here :D

Copy link
Contributor

Choose a reason for hiding this comment

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

I am fine with both solutions, but I would choose the one that differs not from the default ruleset, so we can avoid custom rules wherever it is possible.

/// - children: an array of children component
/// - meta: a ComponentMeta object
/// - Returns: An optional Component
func produce(children: [Component],
Copy link
Contributor

Choose a reason for hiding this comment

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

not a fan of this naming... produce...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggestions?


/// A concrete ComponentFactory which produces a stack cluster component
public final class StackClusterFactory: ComponentFactory {
public func produce(children: [Component],
Copy link
Contributor

Choose a reason for hiding this comment

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

it seems to me that these factories have something wrong.
To comform to ClusterFactory you need a function that given children and meta creates a Component
then ClusterLayout.stack... ClusterLayout.tab... are already factories so we are creating all this code to basically wrap those functions.

Other than this this factory implementation seems wrong to me because it's only working for .cluster
How do you create wrapper and view from json? Am I missing anything?

Copy link
Contributor

Choose a reason for hiding this comment

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

in the proof of concept implementation I had 3 factories (one for cluster one for wrapper one for view) because each type has different inputs.
Also all this XXXFactory seems not need to me since all we want is a function that given the correct input returns a Component

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Factories are meant to provide the type to register. Maybe we can skip that and just have JSONFactory registering the functions for a given string, but I liked the idea of representing a factory also.

In the tests I create factories for clusters and views, and I guess would also work for wrappers right?

So if I am not wrong the difference between this and the proof of concept is that here we are more explicit on the factories (which also needs more code, true) and in the proof of concept the factory only registers closures for given types.

let children = json[childrenKey] as? [JSONObject] ?? []
let componentChildren = try children.flatMap { try produce(from: $0) }

return factories[type]?.produce(children: componentChildren, meta: meta)
Copy link
Contributor

Choose a reason for hiding this comment

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

yes looks like only cluster can be created from json

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the factory tests we create clusters or plain views, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Well you can by ignoring the children (empty array) , the view shouldn't have children.
And what about wrappers? They don't have children but one child

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You would get the first child of the children. So for you what's wrong here is having same factory API for the three kind of Component's

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes and actually you don't need three factory (will write some pseudo code when I arrive at home)
Having to ignore children or get the first seems bad API design.
Other than this, defining types that wraps functions (e.g. StackFactory) makes registration way more verbose than needed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Trying out that approach too, let's see what we each get

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got something out of what you said, I will update the tests now and we can discuss the differences

Copy link
Contributor

@MP0w MP0w Jan 13, 2017

Choose a reason for hiding this comment

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

I wrote some pseudocode:

typealias JSONRepr = [String: Any]

fileprivate protocol FactoryBuilder {
    var factory: (JSONRepr) -> Component { get }
}

fileprivate class ClusterFactoryBuilder: FactoryBuilder {
    let factory: (JSONRepr) -> Component

    init(builder: @escaping Component.ClusterBuilder) {
        self.factory = { (json) in
            return Component.cluster(
                builder: builder,
                children: json["children"] as! [Component],
                meta: json["meta"] as! [String: Any]
            )
            // pseudo.....
        }
    }
}

// same for....
// fileprivate class WrapperFactoryBuilder: FactoryBuilder
// fileprivate class ViewFactoryBuilder: FactoryBuilder

class ComponentFactory {
    private static var factories: [String: FactoryBuilder] = [:]

    class func registerCluster(for id: String, builder: @escaping Component.ClusterBuilder) {
        factories[id] = ClusterFactoryBuilder(builder: builder)
    }

    class func registerWrapper(for id: String, builder: Component.WrapperBuilder) {
        // factories[id] = WrapperFactoryBuilder(builder: builder)
    }

    class func registerView(for id: String, builder: Component.ViewBuilder) {
        // factories[id] = ViewFactoryBuilder(builder: builder)
    }

    class func component(for json: JSONRepr) -> Component? {
        let id = json["id"] as! String // pseudo
        return factories[id]?.factory(json)
    }
}

// USAGE


ComponentFactory.registerView(for: "myView") { (meta) -> UIViewController? in
    return UIViewController()
}

ComponentFactory.registerCluster(for: "stack") { (children, meta) -> UIViewController? in
    return StackViewController()
}

ComponentFactory.registerCluster(for: "tabvar", builder: tabBarBuilder(Bundle.main))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got something similar with minor differences, i will push and we can check

Copy link
Contributor

Choose a reason for hiding this comment

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

I like this approach now, because it's using closures everywhere. The first approach was a little bit a mixture of the closure and class approach. I still prefer the class approach, but probably because I'm thinking in old concepts ;) (I would have used classes for the clusters and protocols for the factories.)

@joanromano
Copy link
Contributor Author

@MP0w refactored following a similar approach, doing the json handling within the json factory.

Also using the factory as an instance with instance methods instead of static methods with a single static builder registration: still not sure which is the best solution there.

/// - 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
public class func jsonObject(from jsonFilename: String, bundle: Bundle = .main) throws -> JSONObject? {
Copy link
Contributor

Choose a reason for hiding this comment

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

this shouldn't be part of Matrioska, I think the whole JSONReadershouldn't be part of Matrioska since it's just convenience functions for JSONSerialization and retrieving a file

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's move this class to the test target for now. If we need it later on we can move it again to the project.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is still in Source, should be in Test/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Didn't apply any changes yet, was waiting for the full review


opt_in_rules:
- empty_count
- 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!

@joanromano
Copy link
Contributor Author

Overall my feelings:

  • This second approach is a bit more handling on our side but better and easier usage for clients: just registering closures into the json factory and having always three kind of factory builders.
  • In the other hand the first approach let's you express better what a factory is for and having different factories in code, but requires more work on the client side and has a single api for factory building which one may argue that is not 100% correct.

I would go for the ease of use of the api then and keep the second approach.

Copy link
Contributor

@MP0w MP0w left a comment

Choose a reason for hiding this comment

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

Didn't properly check the test because I think we still need to define the json schema properly

@@ -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.


static let typeKey = "type"
static let metaKey = "meta"
static let childrenKey = "children"
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 we need to son specs and documentation (schema would also be nice)... because I think we are missing child for wrapperfor example

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 agreed on having the same children key for both wrapper and cluster afaik

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, as said at the beginning of the PR the documentation is still to do.

Copy link
Contributor

Choose a reason for hiding this comment

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

Don't agree with using children instead of child. If we want to have wrappers (and I think we want) they shouldn't be an hacky cluster (a cluster that ignore children other than the first.
Please couldn't you guys reconsider this decision or try to explain why you think it should be like this?

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 think this is a discussion for #3 probably. Anyway nobody said we don't want to have wrappers, it's just the agreement on the json schema.

/// A factory that wraps ComponentFactory objects and uses them to produce Components
public final class JSONFactory {

static let typeKey = "type"
Copy link
Contributor

Choose a reason for hiding this comment

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

all these properties should be private...
Also consider a nested enum for namespacing:

enum Keys { // not a struct because an enum with no cases can't be initialized
   static let type = "type"
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, but what would the nested enum namespacing bring if we make it private?

Copy link
Contributor

Choose a reason for hiding this comment

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

Usage would be Keys.type instead of JSONFactory.type .
More explicit imho

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 also thought it might be good for these keys to be public so that clients now which keys to expect. That could also be reflected on the documentation though.

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

/// - 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
public class func jsonObject(from jsonFilename: String, bundle: Bundle = .main) throws -> JSONObject? {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is still in Source, should be in Test/


opt_in_rules:
- empty_count
- 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.

true!

return children
}
}
}
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 it shouldn't be needed (so shouldn't be exposed) but maybe I'm missing the point to use it?

@joanromano joanromano mentioned this pull request Jan 15, 2017
3 tasks
/// - 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.

@joanromano
Copy link
Contributor Author

joanromano commented Jan 18, 2017

1 Warning
⚠️ Big PR

SwiftLint found issues

Warnings

File Line Reason
ComponentMeta.swift 78 Line should be 100 characters or less: currently 103 characters

Generated by 🚫 danger

@joanromano joanromano merged commit 792a2e4 into master Jan 19, 2017
@joanromano joanromano deleted the feature/jsonParsing branch January 19, 2017 21:06
joanromano added a commit that referenced this pull request Jan 19, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants