Skip to content
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

Added conformance to Hashable #19

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
11 changes: 9 additions & 2 deletions MyPlayground.playground/Contents.swift
Original file line number Diff line number Diff line change
@@ -11,7 +11,14 @@ UTI(filenameExtension: "jpeg") == UTI(mimeType: "image/jpeg")

switch UTI(kUTTypeJPEG as String) {
case UTI(kUTTypeImage as String):
print("JPEG is a kind of images")
print("JPEG is a kind of image")
default:
fatalError("JPEG must be a image")
fatalError("JPEG must be a kind of image")
}

let utiPublicData = UTI("public.data")
let utiPublicImage = UTI("public.image")

let utiSet: Set = [utiPublicData]
utiSet.contains(utiPublicData) // => true
utiSet.contains(utiPublicImage) // => false
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -6,38 +6,38 @@ UTIKit is an UTI (Uniform Type Identifier) wrapper for Swift.

## Features

UTIKit is a full featured library including entire UTI functions.
UTIKit is a fully featured library containing the entire set of UTI functions.

- Convertibility
- Filename extension
- MIME type
- OSType (OS X only)
- Pasteboard type (OS X only)
- Equality
- Conformance
- UTI Conformance checking
- and others…

## Usage

### Making from an UTI string
### Making a UTI from a UTI string

```swift
let jpeg = UTI("public.jpeg")
```

### Making from a filename extension
### Making a UTI from a filename extension

```swift
let jpeg = UTI(filenameExtension: "jpeg")
```

### Making from a MIME type
### Making a UTI from a MIME type

```swift
let jpeg = UTI(mimeType: "image/jpeg")
```

### Getting filename extensions or MIME types
### Listing filename extensions and MIME types for a UTI

```swift
UTI(mimeType: "image/jpeg").filenameExtensions // => ["jpeg", "jpg", "jpe"]
@@ -51,14 +51,25 @@ UTI(filenameExtension: "jpeg").mimeTypes // => ["image/jpeg"]
UTI(mimeType: "image/jpeg") == UTI(filenameExtension: "jpeg") // => true
```

### Conformance
### Hashable

```swift
let utiPublicData = UTI("public.data")
let utiPublicImage = UTI("public.image")

let utiSet: Set = [utiPublicData]
utiSet.contains(utiPublicData) // => true
utiSet.contains(utiPublicImage) // => false
```

### UTI Conformance checking

```swift
switch UTI(kUTTypeJPEG) {
case UTI(kUTTypeImage):
print("JPEG is a kind of images")
print("JPEG is a kind of image")
default:
fatalError("JPEG must be a image")
fatalError("JPEG must be a kind of image")
}
```

4 changes: 4 additions & 0 deletions UTIKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@
09FD21411A962FC00071BF6B /* UTIKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 09FD21401A962FC00071BF6B /* UTIKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
09FD21471A962FC00071BF6B /* UTIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09FD213B1A962FC00071BF6B /* UTIKit.framework */; };
09FD21581A962FCF0071BF6B /* UTI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FD21571A962FCF0071BF6B /* UTI.swift */; };
FAB88A8C2329581900D3D615 /* UTI Equatable and Hashable Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB88A8B2329581900D3D615 /* UTI Equatable and Hashable Tests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
@@ -35,6 +36,7 @@
09FD21461A962FC00071BF6B /* UTIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UTIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
09FD214C1A962FC00071BF6B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
09FD21571A962FCF0071BF6B /* UTI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UTI.swift; sourceTree = "<group>"; };
FAB88A8B2329581900D3D615 /* UTI Equatable and Hashable Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UTI Equatable and Hashable Tests.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
@@ -105,6 +107,7 @@
isa = PBXGroup;
children = (
09F886461A96B61E00ADF55F /* UTITests.swift */,
FAB88A8B2329581900D3D615 /* UTI Equatable and Hashable Tests.swift */,
09FD214B1A962FC00071BF6B /* Supporting Files */,
);
path = UTIKitTests;
@@ -285,6 +288,7 @@
buildActionMask = 2147483647;
files = (
09F886471A96B61E00ADF55F /* UTITests.swift in Sources */,
FAB88A8C2329581900D3D615 /* UTI Equatable and Hashable Tests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
26 changes: 23 additions & 3 deletions UTIKit/UTI.swift
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ import Foundation
import MobileCoreServices
#endif

public struct UTI: CustomStringConvertible, CustomDebugStringConvertible, Equatable {
public struct UTI: CustomStringConvertible, CustomDebugStringConvertible {

public let utiString: String

@@ -247,8 +247,28 @@ public struct UTI: CustomStringConvertible, CustomDebugStringConvertible, Equata

}

public func ==(lhs: UTI, rhs: UTI) -> Bool {
return UTTypeEqual(lhs.utiString as CFString, rhs.utiString as CFString)
extension UTI: Equatable {
static public func ==(lhs: UTI, rhs: UTI) -> Bool {
// Since there is no UTTypeHash, or anything like that, we're forced to implement hashable
// according to raw hashing of `utiString`. To ensure our implementation of `Hashable` and
// `Equatable` are logically valid, we need to assert that `==` and `UTTypeEqual` behave
// equally
let expected = UTTypeEqual(lhs.utiString as CFString, rhs.utiString as CFString)
let actual = lhs.utiString == rhs.utiString
Copy link
Owner

Choose a reason for hiding this comment

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

A quick check shows that UTTypeEqual is not a simple string match. It seems to be case-insensitive.

UTTypeEqual("public.image" as CFString, "public.image" as CFString) // true
UTTypeEqual("public.image" as CFString, "PUBLIC.IMAGE" as CFString) // true

However, I couldn't find any documentation on how it was implemented.

Copy link
Owner

Choose a reason for hiding this comment

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

Getting a canonical identifier from the declaration may work. You also need to fall back to utiString for dynamic UTI.

extension UTI: Hashable {
    public func hash(into hasher: inout Hasher) {
        hasher.combine(self.declaration.identifier ?? self.utiString.lowercased())
    }
}

Copy link
Author

Choose a reason for hiding this comment

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

Hey, thanks for getting back on this! Really busy right now, but I'll fix these up when I get a chance and repsuh!

assert(expected == actual, """
Found a situation in which the Equatable and Hashable implementations are not compatible!
UTTypeEqual(\(lhs.utiString), \(rhs.utiString)) returned \(expected), but
\(lhs.utiString) == \(rhs.utiString) returned \(actual)
""")

return expected
}
}

extension UTI: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(self.utiString)
}
}

public func ~=(pattern: UTI, value: UTI) -> Bool {
126 changes: 126 additions & 0 deletions UTIKitTests/UTI Equatable and Hashable Tests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// The MIT License (MIT)
//
// Copyright (c) 2019 Alexander Momchilov
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import XCTest
@testable import UTIKit

class UTIHashableTests: XCTestCase {
let testUTIs = [
UTI("public.text"),
UTI("public.plain-text"),
UTI("public.image"),
UTI("public.jpeg"),
]

private func computeHash<T: Hashable>(_ value: T) -> Int {
var hasher = Hasher()
hasher.combine(value)
return hasher.finalize()
}

func testEqualHashImpliesEquality() {
for a in testUTIs {
for b in testUTIs {
if a == b {
XCTAssertEqual(computeHash(a), computeHash(b))
}
// If unequal, hashes don't have to be equal
}
}
}

// Technically redundant, but it's a nice little sanity check
func testHashableUsage() {
let utiSet = Set(testUTIs)

XCTAssertEqual(testUTIs.count, utiSet.count)
XCTAssertTrue(testUTIs.allSatisfy { utiSet.contains($0) })

XCTAssertFalse(utiSet.contains(UTI("not.real.uti")))
}
}

class UTIEquatableTests: XCTestCase {

let gif = UTI("com.compuserve.gif")
let png = UTI("public.png")

func testRawOperators() {
XCTAssertTrue(gif == gif)
XCTAssertTrue(gif != png)

XCTAssertFalse(gif == png)
XCTAssertFalse(gif != gif)
}

func testReflexivity() {
/// Test that every UTI is equal to itself.
func testReflexivity(_ uti: UTI) {
XCTAssertEqual(uti, uti)
}

testReflexivity(gif)
testReflexivity(png)
}

func testSymmetry() {
/// Test the equality is symmetric, i.e. that a == b equivalent to b == a
func testSymmetry(_ a: UTI, _ b: UTI) {
if a == b {
XCTAssertEqual(b, a)
} else {
XCTAssertNotEqual(b, a)
}
}

testSymmetry(gif, gif)
testSymmetry(gif, png)
testSymmetry(png, gif)
testSymmetry(png, png)
}

func testTransitivity() {
/// Test that 2/3 pairs being equal implies the 3rd pair must be equal.
func testTransitivity(_ a: UTI, _ b: UTI, _ c: UTI) {
let pairs = [
(a, b),
(b, c),
(c, a),
]

let pairEquality = pairs.map(==)

if pairEquality[0] && pairEquality[1] { XCTAssertEqual(pairs[2].0, pairs[2].1) }
if pairEquality[1] && pairEquality[2] { XCTAssertEqual(pairs[0].0, pairs[0].1) }
if pairEquality[2] && pairEquality[0] { XCTAssertEqual(pairs[1].0, pairs[1].1) }
}

testTransitivity(gif, gif, gif)
testTransitivity(gif, gif, png)
testTransitivity(gif, png, gif)
testTransitivity(gif, png, png)
testTransitivity(png, gif, gif)
testTransitivity(png, gif, png)
testTransitivity(png, png, gif)
testTransitivity(png, png, png)
}
}
21 changes: 10 additions & 11 deletions UTIKitTests/UTITests.swift
Original file line number Diff line number Diff line change
@@ -76,14 +76,18 @@ class UTITests: XCTestCase {
XCTAssertEqual(xls.mimeTypes, [ "application/vnd.ms-excel", "application/msexcel" ])
}


let declaredUTI = UTI(filenameExtension: "numbers")!
let dynamicUTI = UTI(filenameExtension: "some-all-new-filetype")!

func testIsDeclared() {
XCTAssertTrue(UTI(filenameExtension: "numbers")!.isDeclared)
XCTAssertFalse(UTI(filenameExtension: "meaningless-characters")!.isDeclared)
XCTAssertTrue(declaredUTI.isDeclared)
XCTAssertFalse(dynamicUTI.isDeclared)
}

func testIsDynamic() {
XCTAssertFalse(UTI(filenameExtension: "key")!.isDynamic)
XCTAssertTrue(UTI(filenameExtension: "all-new-filetype")!.isDynamic)
XCTAssertFalse(declaredUTI.isDynamic)
XCTAssertTrue(dynamicUTI.isDynamic)
}

func testDeclaration() {
@@ -103,19 +107,14 @@ class UTITests: XCTestCase {
XCTAssertNotNil(pdf.declaringBundle)
}

func testEquatable() {
XCTAssertEqual(UTI(filenameExtension: "gif"), UTI(mimeType: "image/gif"))
}

func testMatch() {
func testPatternMatching() {
XCTAssertTrue(UTI(kUTTypeImage as String) ~= UTI(mimeType: "image/jpeg")!)

switch UTI(kUTTypeJPEG as String) {
case UTI(kUTTypeImage as String):
XCTAssert(true)
default:
XCTFail("kUTTypeJPEG must be comforms to kUTTypeImage")
XCTFail("kUTTypeJPEG must comform to kUTTypeImage")
}
}

}