Skip to content
This repository has been archived by the owner on Sep 6, 2018. It is now read-only.

Split colors parser into sub-parsers & support multiple palettes (files) #40

Merged
merged 6 commits into from
Jun 4, 2017

Conversation

djbe
Copy link
Member

@djbe djbe commented May 22, 2017

Currently there is a bit of logic in swiftgen that selects the right parser depending on the extension. This belongs in SwiftGenKit.

Another thing is that after this refactor, we can easily call parseFile(at:) multiple times on the generic colors parser, thus adding multiple files support. Needed for an eventual #39 implementation.

@djbe djbe added this to the 2.0.0 milestone May 22, 2017
@AliSoftware
Copy link
Contributor

What about a registration mechanism, to avoid having a hard-coded enum and switch-ing on each?

Like ColorsFileParser.register(ColorsJSONFileParser.self, forExtension: "json") etc
Would feel more like a plugin-like API and make it easy to extend in the future I think?

@djbe
Copy link
Member Author

djbe commented May 23, 2017

Good idea. For the plugin api we could have stuff like ... register(ColorsFileParser, for subcommand: "colors").


let result = parser.stencilContext()
XCTDiffContexts(result, expected: "empty.plist", sub: .colors)
}

func testFileWithDefaults() throws {
let parser = ColorsCLRFileParser()
let parser = ColorsFileParser()
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we keep ColorsCLRFileParser here and in other Colors*FileTests?
Because if we do this, I have the feeling we are testing two things:

  1. the ColorsCLRFileParser part behind ColorsFileParser
  2. and the "parser routing" in ColorsFileParser which select the correct parser to use

It would be nice to split the specific parsing tests with the tests to select the correct file parser in ColorsFileParser.
In fact, the registration mechanism suggested by @AliSoftware would allow to test this last part quite easily IMO :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, right. But then how do you test the parser routing, namely that it invokes the right bits?
(I have the same question for the SwiftGen CLI, where we currently don't have any tests...)

Copy link
Contributor

@AliSoftware AliSoftware May 23, 2017

Choose a reason for hiding this comment

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

@djbe You can test the routing like this, can't you?

var parserCalled: Int = 0

class FakeParser1: ColorsFileTypeParser {
  func parseFile(at path: Path) throws -> [String: UInt32] {
    parserCalled = 1
  }
}

class FakeParser2: ColorsFileTypeParser {
  func parseFile(at path: Path) throws -> [String: UInt32] {
    parserCalled = 2
  }
}

ColorsFileParser.register(FakeParser1.self, forExtension: "ext1")
ColorsFileParser.register(FakeParser2.self, forExtension: "ext2")
ColorsFileParser.parseFile(at: Path("/foo/colors.ext1"))
XCTAssertEqual(parserCalled, 1)

Copy link
Member Author

Choose a reason for hiding this comment

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

right, obvious in hindsight 😩

}
}
}

// MARK: - Private Helpers

internal func parse(hex hexString: String, key: String? = nil) throws -> UInt32 {
func parse(hex hexString: String, key: String? = nil) throws -> UInt32 {
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be moved where it is used in ColorsTextFileParser?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's also used in the XML and JSON parsers.

extension NSColor {

fileprivate var rgbColor: NSColor? {
var rgbColor: NSColor? {
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be moved to ColorsCLRFileParser instead of changing its scope?

Copy link
Member Author

@djbe djbe May 24, 2017

Choose a reason for hiding this comment

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

Currently it may only be used in the CLR parser, but that doesn't mean it won't be used in some other future parser. I think this method is general enough to stay here, or maybe move it to some general Helper file.

return "error: Unable to parse file. \(reason)"
case .unsupportedFileType(let supported):
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe in addition to the list of supported file types we should also print the file type and/or file name which triggered the error (especially if in the future we accept multiple files for a single invocation, would be nice to know which one is wrong)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure.

let colors = try parser.parseFile(at: path)

for (name, value) in colors {
self.colors[name] = value
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we warn about duplicates / colors overriding old already-parsed colors?

Copy link
Member Author

Choose a reason for hiding this comment

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

We'd need a way to output to stderr then.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I'm not sure that's a problem?

Copy link
Member Author

Choose a reason for hiding this comment

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

Do we currently have one (for showing warnings)? Currently we never show any warnings.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we have, but it's about time we introduce one.
Probably a dedicated class on which we can call a method to log a message (warning or error) and it'll be responsible of formatting and printing it to stderr

We can then adapt the printing logic depending on some flags or auto-detection. E.g. if a specific env var only used by Xcode is set, and -o provides a destination for the generated output code (making nothing else is supposed to be printed to stdout) we can prefer print the messages as warning: inputfile:line: message on stdout. But if we're not in Xcode or there's no -o and the generated code is already printed in stdout then print the error in a more human-friendly format and on stderr.

Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 yeah indeed probably not SwiftGenKit's job.

SwiftGenKit should only either throw (in case of fatal error) or fill an array of warning messages that consumers of the library could then inspect. Or call a callback/closure that the consumer can provide, like a delegate pattern.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmmm, sounds more like something of a notification pattern?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah except the term notification feels like you shout to whoever wants to listen (NotificationCenter) and that's not what I want.

I'd find it more pragmatic to have something like: parser.warningHandler = { message, file, line in … } for warnings (but still throw for fatal errors)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, that could work.

To clarify, I didn't mean macOS visual notifications.

Copy link
Contributor

Choose a reason for hiding this comment

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

Errr me neither 😂


func register(parser: ColorsFileTypeParser.Type) {
for ext in parser.extensions {
parsers[ext] = parser
Copy link
Contributor

@AliSoftware AliSoftware May 27, 2017

Choose a reason for hiding this comment

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

Should we warn / error in case multiple parsers declare the same extension, or should it be standard and expected that the last registered one is the one with the higher priority on that duplicate extension?

I'm also wondering if we'll always distinguish parsers only according to the file extensions (we could instead imagine a static func canParseFile(file: Path) -> Bool or something — so that the parser can also analyse the file structure to tell if it can parse the file, would allow multiple parsers to support JSON or XML extensions with different internal structure/format), but it's probably better to keep that feature for later, and only add it if we really need it one day, right? (YAGNI)

Copy link
Member Author

Choose a reason for hiding this comment

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

We can throw an error for now, eventually we could have multiple parsers per extension (or mime-type) and should then define some priority rules, especially in a plugin scenario.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah let's do that for now. We can still improve that later and make it more flexible in the future, step by step

Copy link
Member Author

Choose a reason for hiding this comment

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

I think that instead of throwing an error, when we have the warning system, this should trigger a warning. But leave the error in for now until we have it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure. The warning closure could be part of the PR creating the unifying parser protocol (#39) as the closure will likely be part of this protocol anyway

guard colorSpace.colorSpaceModel != .RGB else { return self }

return usingColorSpaceName(NSCalibratedRGBColorSpace)
}

internal var hexValue: UInt32 {
var hexValue: UInt32 {
let hexRed = UInt32(round(redComponent * 0xFF)) << 24
let hexGreen = UInt32(round(greenComponent * 0xFF)) << 16
let hexBlue = UInt32(round(blueComponent * 0xFF)) << 8
let hexAlpha = UInt32(round(alphaComponent * 0xFF))
return hexRed | hexGreen | hexBlue | hexAlpha
Copy link
Contributor

Choose a reason for hiding this comment

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

Totally not part of the PR but reviewing this, I realise that we should first call rgbColor on self before extracting components from redComponent etc. Probably worth a future PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

We only use it in one place, and call rgbColor before it. I've changed this for now, seems overkill for a PR 😄

@testable import SwiftGenKit

final class TestFileParser1: ColorsFileTypeParser {
static var extensions = ["test1"]
Copy link
Contributor

Choose a reason for hiding this comment

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

static let 😉

let parser = ColorsTextFileParser()
try parser.parseFile(at: Fixtures.path(for: "colors.txt", sub: .colors))
let parser = ColorsFileParser()
parser.colors = try ColorsTextFileParser().parseFile(at: Fixtures.path(for: "colors.txt", sub: .colors))

let result = parser.stencilContext()
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we really test both the parsing AND the transformation into a StencilContext?

Shouldn't we instead just put an expectation directly on the dictionary returned by parseFile without transforming it into a stencilContext — and test that conversion of a dictionary into a stencilContext in a separate test suite?

Copy link
Member Author

Choose a reason for hiding this comment

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

That... might make tests much more complicated, we'd have to maintain the list of colours in code whereas right now we just diff with a plist file.

For the parsing part, I'd agree with you, but how do we ensure that we've fully parsed a file? Without having a list of colours in code, and we wouldn't be able to use the context plist files as they're for the stencilContext part. Just test if one or 2 colours are present, and a count of the number of colours?

For the stencil context part, I'd suggest using ColorsFileParser as we'd normally use it:

let parser = ColorsFileParser()
parser.parseFile(at: ...)
let result = parser.stencilContext()

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah might not be worth the hassle of maintaining all of that just for making tests more unitary. Maybe we can re discuss that in a separate PR.

import PathKit

final class ColorsCLRFileParser: ColorsFileTypeParser {
static var extensions = ["clr"]
Copy link
Contributor

Choose a reason for hiding this comment

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

static let

@djbe djbe force-pushed the feature/split-color-parser branch from 5aebc67 to 7ae20f5 Compare May 28, 2017 20:52
@AliSoftware AliSoftware mentioned this pull request May 30, 2017
@djbe djbe force-pushed the feature/split-color-parser branch 2 times, most recently from 0c0e9b1 to fb9dd25 Compare May 30, 2017 21:42
@djbe djbe changed the title Split colors parser into multiple sub-parsers Split colors parser into sub-parsers & support multiple palettes (files) May 30, 2017
@djbe
Copy link
Member Author

djbe commented May 30, 2017

Needs SwiftGen/templates#55 to align submodule

@djbe djbe force-pushed the feature/split-color-parser branch from ced4247 to 2ebc5d4 Compare May 31, 2017 10:01
public enum ColorsParserError: Error, CustomStringConvertible {
case duplicateExtensionParser(ext: String, existing: String, new: String)
case invalidHexColor(string: String, key: String?)
Copy link
Contributor

Choose a reason for hiding this comment

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

Mmmmh maybe we should add a path: Path associated value to this one too to make it homogeneous?

let whitespace = CharacterSet.whitespaces
let skippedCharacters = NSMutableCharacterSet()
skippedCharacters.formUnion(with: whitespace)
skippedCharacters.formUnion(with: skippedCharacters as CharacterSet)
Copy link
Contributor

Choose a reason for hiding this comment

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

I know this code was already there before, but re-reading this, I can't understand what this does… a union with itself?!

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, it was added in this commit:
SwiftGen/SwiftGen@ea9c97f
I can't see why though.

Seems to work fine without, so I'll remove it

@djbe djbe force-pushed the feature/split-color-parser branch from 3938c3e to 8b6e987 Compare June 4, 2017 18:02
@djbe djbe merged commit df6219d into master Jun 4, 2017
@djbe djbe deleted the feature/split-color-parser branch June 4, 2017 20:59
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants