├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Tagging │ └── Tagging.swift └── Tests ├── LinuxMain.swift └── TaggingTests ├── CollectionTests.swift ├── LinuxTestable.swift ├── TaggableTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alex Rupérez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | /** 4 | * Tagging 5 | * Copyright (c) alexruperez 2019 6 | * Licensed under the MIT license (see LICENSE file) 7 | */ 8 | 9 | import PackageDescription 10 | 11 | let package = Package( 12 | name: "Tagging", 13 | products: [ 14 | .library( 15 | name: "Tagging", 16 | targets: ["Tagging"] 17 | ) 18 | ], 19 | targets: [ 20 | .target( 21 | name: "Tagging"), 22 | .testTarget( 23 | name: "TaggingTests", 24 | dependencies: ["Tagging"] 25 | ) 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🏷 Tagging 2 | [![Twitter](https://img.shields.io/badge/contact-@alexruperez-0FABFF.svg?style=flat)](http://twitter.com/alexruperez) 3 | [![Swift](https://img.shields.io/badge/Swift-5-orange.svg?style=flat)](https://swift.org) 4 | [![Swift Package Manager Compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-4BC51D.svg?style=flat)](https://github.com/apple/swift-package-manager) 5 | 6 | Welcome to **Tagging**, a small library that makes it easy to create *type-safe tags* in Swift. 7 | Categorization are often very useful for our models, so leveraging the compiler to ensure that they're used in a correct manner can go a long way to making the model layer of an app or system more robust. 8 | 9 | This library is **strongly** inspired by [JohnSundell](https://github.com/JohnSundell)/[🆔entity](https://github.com/JohnSundell/Identity) and [mbleigh](https://github.com/mbleigh)/[acts-as-taggable-on](https://github.com/mbleigh/acts-as-taggable-on), for theoretical information, check out *["Type-safe identifiers in Swift"](https://www.swiftbysundell.com/posts/type-safe-identifiers-in-swift)* on [Swift by Sundell](https://www.swiftbysundell.com). 10 | 11 | ## Making types taggable 12 | 13 | All you have to do to use Tagging is to make a model conform to `Taggable`, and give it an `tags` property, like this: 14 | 15 | ```swift 16 | struct Article: Taggable { 17 | let tags: [Tag
] 18 | let title: String 19 | } 20 | ``` 21 | 22 | And just like that, the above `Article` tags are now type-safe! Thanks to Swift’s type inference capabilities, it’s also possible to implement an `Taggable` type’s `tags` simply by using `Tags` as its type: 23 | 24 | ```swift 25 | struct Article: Taggable { 26 | let tags: Tags 27 | let title: String 28 | } 29 | ``` 30 | 31 | The `Tags` type alias is automatically added for all `Taggable` types, which also makes it possible to refer to `[Tag
]` as `Article.Tags`. 32 | 33 | ## Customizing the raw type 34 | 35 | `Tag` values are backed by strings by default, but that can easily be customized by giving an `Taggable` type a `RawTag`, but must be at least `Hashable`: 36 | 37 | ```swift 38 | struct Article: Taggable { 39 | typealias RawTag = UUID 40 | 41 | let tags: Tags 42 | let title: String 43 | } 44 | ``` 45 | 46 | The above `Article` tags are now backed by a `UUID` instead of a `String`. 47 | 48 | ## Conveniences built-in 49 | 50 | Even though Tagging is focused on type safety, it still offers several conveniences to help reduce verbosity. For example, if a `Tag` is backed by a raw value type that can be expressed by a `String` literal, so can the tags: 51 | 52 | ```swift 53 | let article = Article(tags: ["foo", "bar"], title: "Example") 54 | ``` 55 | 56 | The same is also true for tags that are backend by a raw value type that can be expressed by `Int` literals: 57 | 58 | ```swift 59 | let article = Article(tags: [7, 9], title: "Example") 60 | ``` 61 | 62 | `Tag` also becomes `Codable`, `Hashable` and `Equatable` whenever its raw value type conforms to one of those protocols. 63 | 64 | ## Type safety 65 | 66 | So how exactly does Tagging make tags more type-safe? First, when using Tagging, it no longer becomes possible to accidentally pass a tag for one type to an API that accepts an tag for another type. For example, this code won't compile when using Tagging: 67 | 68 | ```swift 69 | articleManager.articles(withTags: user.tags) 70 | ``` 71 | 72 | The compiler will give us an error above, since we're trying to pass an `[Tag]` value to a method that accepts an `[Tag
]` - giving us much stronger type safety than when using plain values, like `String` or `Int`, as tag types. 73 | 74 | Tagging also makes it impossible to accidentally declare `tags` properties of the wrong type. So the following won't compile either: 75 | 76 | ```swift 77 | struct User: Tagging { 78 | let tags: [Tag
] 79 | } 80 | ``` 81 | 82 | The reason the above code will fail to compile is because `Taggable` requires types conforming to it to declare tags that are bound to the same type as the conformer, again providing an extra level of type safety. 83 | 84 | ### Finding most or least used tags 85 | 86 | You can find the most or least used tags by using: 87 | 88 | ```swift 89 | taggableCollection.mostUsedTags() 90 | taggableCollection.leastUsedTags() 91 | ``` 92 | 93 | You can also filter the results by passing the method a limit, however the default limit is 20. 94 | 95 | ```swift 96 | taggableCollection.mostUsedTags(10) 97 | taggableCollection.leastUsedTags(10) 98 | ``` 99 | 100 | Or directly get the raw values. 101 | 102 | ```swift 103 | taggableCollection.mostUsedRawTags() 104 | taggableCollection.leastUsedRawTags() 105 | ``` 106 | 107 | ### Finding tagged objects 108 | ##### NOTE: By default, find objects tagged with any of the specified tags. 109 | 110 | ```swift 111 | taggableCollection.tagged(with: "foo") 112 | taggableCollection.tagged(with: ["foo"]) 113 | taggableCollection.tagged(with: ["foo", "bar"]) 114 | taggableCollection.tagged(with: taggable.tags) 115 | ``` 116 | 117 | ### Find tagged objects that matches all given tags 118 | ##### NOTE: This only matches tagged objects that have the exact set of specified tags. If a tagged object has additional tags, they are not returned. 119 | 120 | ```swift 121 | taggableCollection.tagged(with: ["foo", "bar"], match: .all) 122 | taggableCollection.tagged(with: taggable.tags, match: .all) 123 | ``` 124 | 125 | ### Find tagged objects that have not been tagged with given tags 126 | ```swift 127 | taggableCollection.tagged(with: ["foo", "bar"], match: .none) 128 | taggableCollection.tagged(with: taggable.tags, match: .none) 129 | ``` 130 | 131 | ## Installation 132 | 133 | Since Tagging is implemented within a [single file](https://github.com/alexruperez/Tagging/blob/master/Sources/Tagging/Tagging.swift)!, the easiest way to use it is to simply drag and drop it into your Xcode project. 134 | 135 | But if you wish to use a dependency manager, you can use the [Swift Package Manager](https://github.com/apple/swift-package-manager) by declaring Tagging as a dependency in your `Package.swift` file: 136 | 137 | ```swift 138 | .package(url: "https://github.com/alexruperez/Tagging", from: "0.1.0") 139 | ``` 140 | 141 | *For more information, see [the Swift Package Manager documentation](https://github.com/apple/swift-package-manager/tree/master/Documentation).* 142 | 143 | ## Contributions & support 144 | 145 | Tagging is developed completely in the open, and your contributions are more than welcome. 146 | 147 | Before you start using Tagging in any of your projects, it’s highly recommended that you spend a few minutes familiarizing yourself with its documentation and internal implementation (it all fits [in a single file](https://github.com/alexruperez/Tagging/blob/master/Sources/Tagging/Tagging.swift)!), so that you’ll be ready to tackle any issues or edge cases that you might encounter. 148 | 149 | To learn more about the principles used to implement Tagging, check out *["Type-safe identifiers in Swift"](https://www.swiftbysundell.com/posts/type-safe-identifiers-in-swift)* on [Swift by Sundell](https://www.swiftbysundell.com). 150 | 151 | This project does not come with GitHub Issues-based support, and users are instead encouraged to become active participants in its continued development — by fixing any bugs that they encounter, or improving the documentation wherever it’s found to be lacking. 152 | 153 | If you wish to make a change, [open a Pull Request](https://github.com/alexruperez/Tagging/pull/new) — even if it just contains a draft of the changes you’re planning, or a test that reproduces an issue — and we can discuss it further from there. 154 | 155 | Hope you’ll enjoy using **Tagging**! 😀 156 | -------------------------------------------------------------------------------- /Sources/Tagging/Tagging.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Tagging 3 | * Copyright (c) alexruperez 2019 4 | * Licensed under the MIT license (see LICENSE file) 5 | */ 6 | 7 | import Foundation 8 | 9 | // MARK: - Core API 10 | 11 | /// Protocol used to mark a given type as being taggable, meaning 12 | /// that it has type-safe tags, backed by a raw value, which 13 | /// defaults to String. 14 | public protocol Taggable { 15 | /// The backing raw type of this type's tags. 16 | associatedtype RawTag: Hashable = String 17 | /// Shorthand type alias for this type's tags. 18 | typealias Tags = [Tag] 19 | /// The tags of this instance. 20 | var tags: Tags { get } 21 | } 22 | 23 | /// A type-safe tag for a given `Value`, backed by a raw value. 24 | /// When backed by a `Codable` type, `Tag` also becomes codable, 25 | /// and will be encoded into a single value according to its raw value. 26 | public struct Tag { 27 | /// The raw value that is backing this tag. 28 | public let rawValue: Value.RawTag 29 | 30 | /// Initialize an instance with a raw value. 31 | public init(rawValue: Value.RawTag) { 32 | self.rawValue = rawValue 33 | } 34 | } 35 | 36 | // MARK: - Integer literal support 37 | 38 | extension Tag: ExpressibleByIntegerLiteral 39 | where Value.RawTag: ExpressibleByIntegerLiteral { 40 | public init(integerLiteral value: Value.RawTag.IntegerLiteralType) { 41 | rawValue = .init(integerLiteral: value) 42 | } 43 | } 44 | 45 | // MARK: - String literal support 46 | 47 | extension Tag: ExpressibleByUnicodeScalarLiteral 48 | where Value.RawTag: ExpressibleByUnicodeScalarLiteral { 49 | public init(unicodeScalarLiteral value: Value.RawTag.UnicodeScalarLiteralType) { 50 | rawValue = .init(unicodeScalarLiteral: value) 51 | } 52 | } 53 | 54 | extension Tag: ExpressibleByExtendedGraphemeClusterLiteral 55 | where Value.RawTag: ExpressibleByExtendedGraphemeClusterLiteral { 56 | public init(extendedGraphemeClusterLiteral value: Value.RawTag.ExtendedGraphemeClusterLiteralType) { 57 | rawValue = .init(extendedGraphemeClusterLiteral: value) 58 | } 59 | } 60 | 61 | extension Tag: ExpressibleByStringLiteral 62 | where Value.RawTag: ExpressibleByStringLiteral { 63 | public init(stringLiteral value: Value.RawTag.StringLiteralType) { 64 | rawValue = .init(stringLiteral: value) 65 | } 66 | } 67 | 68 | // MARK: - Compiler-generated protocol support 69 | 70 | extension Tag: Equatable where Value.RawTag: Equatable {} 71 | extension Tag: Hashable where Value.RawTag: Hashable {} 72 | 73 | // MARK: - Codable support 74 | 75 | extension Tag: Codable where Value.RawTag: Codable { 76 | public init(from decoder: Decoder) throws { 77 | let container = try decoder.singleValueContainer() 78 | rawValue = try container.decode(Value.RawTag.self) 79 | } 80 | 81 | public func encode(to encoder: Encoder) throws { 82 | var container = encoder.singleValueContainer() 83 | try container.encode(rawValue) 84 | } 85 | } 86 | 87 | // MARK: - Collection extensions 88 | 89 | /// Enum used to filter the search for multiple tags in the collection. 90 | public enum Match { 91 | case all, any, none 92 | } 93 | 94 | extension Collection where Element: Taggable { 95 | /// An ordered, random-access collection of all tags. 96 | public var allTags: [Tag] { 97 | return flatMap { $0.tags } 98 | } 99 | 100 | /// An ordered, random-access collection of all tags raw values. 101 | public var allRawTags: [Self.Element.RawTag] { 102 | return allTags.map { $0.rawValue } 103 | } 104 | 105 | /// An unordered collection of unique tags. 106 | public var uniqueTags: Set> { 107 | return Set(allTags) 108 | } 109 | 110 | /// An unordered collection of unique tags raw values. 111 | public var uniqueRawTags: Set { 112 | return Set(allRawTags) 113 | } 114 | 115 | /// A collection whose elements are tag as key and occurrence frequency as value. 116 | public var tagsFrequency: [Tag: Int] { 117 | return Dictionary(allTags.map { ($0, 1) }, uniquingKeysWith: +) 118 | } 119 | 120 | /// A collection whose elements are tag raw value as key and occurrence frequency as value. 121 | public var rawTagsFrequency: [Self.Element.RawTag: Int] { 122 | return Dictionary(allRawTags.map { ($0, 1) }, uniquingKeysWith: +) 123 | } 124 | 125 | /// An ordered, random-access collection of most popular tags, 126 | /// limited by a limit which defaults to 20. 127 | public func mostUsedTags(_ limit: Int = 20) -> [Tag] { 128 | return tagsFrequency.sorted { $0.value > $1.value }.prefix(limit).map { $0.key } 129 | } 130 | 131 | /// An ordered, random-access collection of most popular tags raw values, 132 | /// limited by a limit which defaults to 20. 133 | public func mostUsedRawTags(_ limit: Int = 20) -> [Self.Element.RawTag] { 134 | return rawTagsFrequency.sorted { $0.value > $1.value }.prefix(limit).map { $0.key } 135 | } 136 | 137 | /// An ordered, random-access collection of least popular tags, 138 | /// limited by a limit which defaults to 20. 139 | public func leastUsedTags(_ limit: Int = 20) -> [Tag] { 140 | return tagsFrequency.sorted { $0.value < $1.value }.prefix(limit).map { $0.key } 141 | } 142 | 143 | /// An ordered, random-access collection of least popular tags raw values, 144 | /// limited by a limit which defaults to 20. 145 | public func leastUsedRawTags(_ limit: Int = 20) -> [Self.Element.RawTag] { 146 | return rawTagsFrequency.sorted { $0.value < $1.value }.prefix(limit).map { $0.key } 147 | } 148 | 149 | /// An ordered, random-access collection of Taggable elements containing that tag raw value. 150 | public func tagged(with tag: Self.Element.RawTag) -> [Self.Element] { 151 | return tagged(with: Tag(rawValue: tag)) 152 | } 153 | 154 | /// An ordered, random-access collection of Taggable elements containing that tag. 155 | public func tagged(with tag: Tag) -> [Self.Element] { 156 | return filter { $0.tags.contains(tag) } 157 | } 158 | 159 | /// An ordered, random-access collection of Taggable elements containing 160 | /// all, any or none of that tags raw values. 161 | public func tagged(with tags: [Self.Element.RawTag], match: Match = .any) -> [Self.Element] { 162 | return tagged(with: tags.map { Tag(rawValue: $0) }, match: match) 163 | } 164 | 165 | /// An ordered, random-access collection of Taggable elements containing 166 | /// all, any or none of that tags. 167 | public func tagged(with tags: [Tag], match: Match = .any) -> [Self.Element] { 168 | return filter { 169 | for tag in tags { 170 | if match == .all, !$0.tags.contains(tag) { return false } 171 | if match == .any, $0.tags.contains(tag) { return true } 172 | if match == .none, $0.tags.contains(tag) { return false } 173 | } 174 | return match != .any 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Tagging 3 | * Copyright (c) alexruperez 2019 4 | * Licensed under the MIT license (see LICENSE file) 5 | */ 6 | 7 | import XCTest 8 | @testable import TaggingTests 9 | 10 | var tests = [XCTestCaseEntry]() 11 | tests += TaggableTests.allTests() 12 | tests += CollectionTests.allTests() 13 | XCTMain(tests) 14 | -------------------------------------------------------------------------------- /Tests/TaggingTests/CollectionTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Tagging 3 | * Copyright (c) alexruperez 2019 4 | * Licensed under the MIT license (see LICENSE file) 5 | */ 6 | 7 | import XCTest 8 | @testable import Tagging 9 | 10 | final class CollectionTests: XCTestCase { 11 | 12 | struct Model: Taggable, Equatable { 13 | let tags: Tags 14 | } 15 | 16 | func testAllTags() { 17 | let modelA = Model(tags: ["foo", "bar"]) 18 | let modelB = Model(tags: ["1", "2", "3"]) 19 | let allTags = [modelA, modelB].allTags 20 | XCTAssertEqual(allTags.first, modelA.tags.first) 21 | XCTAssertEqual(allTags.last, modelB.tags.last) 22 | } 23 | 24 | func testAllRawTags() { 25 | let modelA = Model(tags: ["foo", "bar"]) 26 | let modelB = Model(tags: ["1", "2", "3"]) 27 | let allRawTags = [modelA, modelB].allRawTags 28 | XCTAssertEqual(allRawTags.first, "foo") 29 | XCTAssertEqual(allRawTags.last, "3") 30 | } 31 | 32 | func testUniqueTags() { 33 | let modelA = Model(tags: ["3", "foo", "bar"]) 34 | let modelB = Model(tags: ["1", "2", "3"]) 35 | let uniqueTags = [modelA, modelB].uniqueTags 36 | XCTAssert(uniqueTags.contains(modelA.tags.first!)) 37 | XCTAssert(uniqueTags.contains(modelB.tags.last!)) 38 | } 39 | 40 | func testUniqueRawTags() { 41 | let modelA = Model(tags: ["3", "foo", "bar"]) 42 | let modelB = Model(tags: ["1", "2", "3"]) 43 | let uniqueTags = [modelA, modelB].uniqueRawTags 44 | XCTAssert(uniqueTags.contains("2")) 45 | } 46 | 47 | func testTagsFrequency() { 48 | let modelA = Model(tags: ["3", "foo", "bar"]) 49 | let modelB = Model(tags: ["1", "2", "3"]) 50 | let tagsFrequency = [modelA, modelB].tagsFrequency 51 | XCTAssertEqual(tagsFrequency[modelA.tags.first!], 2) 52 | } 53 | 54 | func testRawTagsFrequency() { 55 | let modelA = Model(tags: ["3", "foo", "bar"]) 56 | let modelB = Model(tags: ["1", "2", "3"]) 57 | let rawTagsFrequency = [modelA, modelB].rawTagsFrequency 58 | XCTAssertEqual(rawTagsFrequency["3"], 2) 59 | } 60 | 61 | func testMostUsedTags() { 62 | let modelA = Model(tags: ["3", "foo", "bar"]) 63 | let modelB = Model(tags: ["1", "2", "3", "4", "4", "4"]) 64 | let mostUsedTags = [modelA, modelB].mostUsedTags() 65 | XCTAssertEqual(mostUsedTags.first, modelB.tags.last) 66 | } 67 | 68 | func testMostUsedRawTags() { 69 | let modelA = Model(tags: ["3", "foo", "bar"]) 70 | let modelB = Model(tags: ["1", "2", "4", "4", "4", "3"]) 71 | let mostUsedRawTags = [modelA, modelB].mostUsedRawTags() 72 | XCTAssertEqual(mostUsedRawTags.first, "4") 73 | } 74 | 75 | func testMostUsedTagsLimit() { 76 | let modelA = Model(tags: ["3", "foo", "bar"]) 77 | let modelB = Model(tags: ["1", "2", "3", "4", "4", "4"]) 78 | let mostUsedTags = [modelA, modelB].mostUsedTags(6) 79 | XCTAssertEqual(mostUsedTags.count, 6) 80 | } 81 | 82 | func testMostUsedRawTagsLimit() { 83 | let modelA = Model(tags: ["3", "foo", "bar"]) 84 | let modelB = Model(tags: ["1", "2", "3", "4", "4", "4"]) 85 | let mostUsedRawTags = [modelA, modelB].mostUsedRawTags(6) 86 | XCTAssertEqual(mostUsedRawTags.count, 6) 87 | } 88 | 89 | func testLeastUsedTags() { 90 | let modelA = Model(tags: ["3", "foo", "foo", "bar", "bar"]) 91 | let modelB = Model(tags: ["1", "2", "2", "3", "4", "4", "4"]) 92 | let leastUsedTags = [modelA, modelB].leastUsedTags() 93 | XCTAssertEqual(leastUsedTags.first, modelB.tags.first) 94 | } 95 | 96 | func testLeastUsedRawTags() { 97 | let modelA = Model(tags: ["3", "foo", "foo", "bar", "bar"]) 98 | let modelB = Model(tags: ["1", "2", "2", "3", "4", "4", "4"]) 99 | let leastUsedRawTags = [modelA, modelB].leastUsedRawTags() 100 | XCTAssertEqual(leastUsedRawTags.first, "1") 101 | } 102 | 103 | func testLeastUsedTagsLimit() { 104 | let modelA = Model(tags: ["3", "foo", "foo", "bar", "bar"]) 105 | let modelB = Model(tags: ["1", "2", "2", "3", "4", "4", "4"]) 106 | let mostUsedTags = [modelA, modelB].leastUsedTags(6) 107 | XCTAssertEqual(mostUsedTags.count, 6) 108 | } 109 | 110 | func testLeastUsedRawTagsLimit() { 111 | let modelA = Model(tags: ["3", "foo", "foo", "bar", "bar"]) 112 | let modelB = Model(tags: ["1", "2", "2", "3", "4", "4", "4"]) 113 | let mostUsedRawTags = [modelA, modelB].leastUsedRawTags(6) 114 | XCTAssertEqual(mostUsedRawTags.count, 6) 115 | } 116 | 117 | func testTaggedWithRawTag() { 118 | let modelA = Model(tags: ["3", "foo", "foo", "bar", "bar"]) 119 | let modelB = Model(tags: ["1", "2", "2", "3", "4", "4", "4"]) 120 | let tagged = [modelA, modelB].tagged(with: "foo") 121 | XCTAssert(tagged.contains(modelA)) 122 | XCTAssertFalse(tagged.contains(modelB)) 123 | } 124 | 125 | func testTaggedWithTag() { 126 | let modelA = Model(tags: ["3", "foo", "foo", "bar", "bar"]) 127 | let modelB = Model(tags: ["1", "2", "2", "3", "4", "4", "4"]) 128 | let tagged = [modelA, modelB].tagged(with: modelB.tags.first!) 129 | XCTAssert(tagged.contains(modelB)) 130 | XCTAssertFalse(tagged.contains(modelA)) 131 | } 132 | 133 | func testTaggedWithRawTags() { 134 | let modelA = Model(tags: ["3", "foo", "foo", "bar", "bar"]) 135 | let modelB = Model(tags: ["1", "2", "2", "3", "4", "4", "4"]) 136 | let tagged = [modelA, modelB].tagged(with: ["foo"]) 137 | XCTAssert(tagged.contains(modelA)) 138 | XCTAssertFalse(tagged.contains(modelB)) 139 | } 140 | 141 | func testTaggedWithTags() { 142 | let modelA = Model(tags: ["3", "foo", "foo", "bar", "bar"]) 143 | let modelB = Model(tags: ["1", "2", "2", "3", "4", "4", "4"]) 144 | let tagged = [modelA, modelB].tagged(with: modelA.tags.first!) 145 | XCTAssert(tagged.contains(modelB)) 146 | XCTAssert(tagged.contains(modelA)) 147 | } 148 | 149 | func testTaggedAllWithRawTags() { 150 | let modelA = Model(tags: ["3", "foo", "foo", "bar", "bar"]) 151 | let modelB = Model(tags: ["1", "2", "2", "3", "4", "4", "4"]) 152 | let tagged = [modelA, modelB].tagged(with: ["foo", "bar"], match: .all) 153 | XCTAssert(tagged.contains(modelA)) 154 | XCTAssertFalse(tagged.contains(modelB)) 155 | } 156 | 157 | func testTaggedAllWithRawTagsNotFound() { 158 | let modelA = Model(tags: ["3", "foo", "foo", "bar", "bar"]) 159 | let modelB = Model(tags: ["1", "2", "2", "3", "4", "4", "4"]) 160 | let tagged = [modelA, modelB].tagged(with: ["foo", "bar", "unknown"], match: .all) 161 | XCTAssertFalse(tagged.contains(modelA)) 162 | XCTAssertFalse(tagged.contains(modelB)) 163 | } 164 | 165 | func testTaggedNoneWithTags() { 166 | let modelA = Model(tags: ["3", "foo", "foo", "bar", "bar"]) 167 | let modelB = Model(tags: ["1", "2", "2", "3", "4", "4", "4"]) 168 | let tagged = [modelA, modelB].tagged(with: [modelA.tags.first!], match: .none) 169 | XCTAssertFalse(tagged.contains(modelB)) 170 | XCTAssertFalse(tagged.contains(modelA)) 171 | } 172 | 173 | func testTaggedWithEmptyRawTags() { 174 | let modelA = Model(tags: ["3", "foo", "foo", "bar", "bar"]) 175 | let modelB = Model(tags: ["1", "2", "2", "3", "4", "4", "4"]) 176 | let tagged = [modelA, modelB].tagged(with: ["unknown"]) 177 | XCTAssertFalse(tagged.contains(modelA)) 178 | XCTAssertFalse(tagged.contains(modelB)) 179 | } 180 | 181 | func testAllTestsRunOnLinux() { 182 | verifyAllTestsRunOnLinux() 183 | } 184 | 185 | } 186 | 187 | extension CollectionTests: LinuxTestable { 188 | static var allTests: [(String, (CollectionTests) -> () throws -> Void)] = [ 189 | ("testAllTags", testAllTags), 190 | ("testAllRawTags", testAllRawTags), 191 | ("testUniqueTags", testUniqueTags), 192 | ("testUniqueRawTags", testUniqueRawTags), 193 | ("testTagsFrequency", testTagsFrequency), 194 | ("testRawTagsFrequency", testRawTagsFrequency), 195 | ("testMostUsedTags", testMostUsedTags), 196 | ("testMostUsedRawTags", testMostUsedRawTags), 197 | ("testMostUsedTagsLimit", testMostUsedTagsLimit), 198 | ("testMostUsedRawTagsLimit", testMostUsedRawTagsLimit), 199 | ("testLeastUsedTags", testLeastUsedTags), 200 | ("testLeastUsedRawTags", testLeastUsedRawTags), 201 | ("testLeastUsedTagsLimit", testLeastUsedTagsLimit), 202 | ("testLeastUsedRawTagsLimit", testLeastUsedRawTagsLimit), 203 | ("testTaggedWithRawTag", testTaggedWithRawTag), 204 | ("testTaggedWithTag", testTaggedWithTag), 205 | ("testTaggedWithRawTags", testTaggedWithRawTags), 206 | ("testTaggedWithTags", testTaggedWithTags), 207 | ("testTaggedAllWithRawTags", testTaggedAllWithRawTags), 208 | ("testTaggedAllWithRawTagsNotFound", testTaggedAllWithRawTagsNotFound), 209 | ("testTaggedNoneWithTags", testTaggedNoneWithTags), 210 | ("testTaggedWithEmptyRawTags", testTaggedWithEmptyRawTags) 211 | ] 212 | } 213 | -------------------------------------------------------------------------------- /Tests/TaggingTests/LinuxTestable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Tagging 3 | * Copyright (c) alexruperez 2019 4 | * Licensed under the MIT license (see LICENSE file) 5 | */ 6 | 7 | import XCTest 8 | 9 | protocol LinuxTestable: XCTestCase { 10 | static var allTests: [(String, (Self) -> () throws -> Void)] { get } 11 | } 12 | 13 | extension LinuxTestable { 14 | func verifyAllTestsRunOnLinux(excluding excludedTestNames: Set = []) { 15 | #if os(macOS) 16 | let testNames = Set(Self.allTests.map { $0.0 }) 17 | 18 | for name in Self.testNames { 19 | guard name != "testAllTestsRunOnLinux" else { 20 | continue 21 | } 22 | 23 | guard !excludedTestNames.contains(name) else { 24 | continue 25 | } 26 | 27 | if !testNames.contains(name) { 28 | XCTFail(""" 29 | Test case \(Self.self) does not include test \(name) on Linux. 30 | Please add it to the test case's 'allTests' array. 31 | """) 32 | } 33 | } 34 | #endif 35 | } 36 | } 37 | 38 | #if os(macOS) 39 | private extension LinuxTestable { 40 | static var testNames: [String] { 41 | return defaultTestSuite.tests.map { test in 42 | let components = test.name.components(separatedBy: .whitespaces) 43 | return components[1].replacingOccurrences(of: "]", with: "") 44 | } 45 | } 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /Tests/TaggingTests/TaggableTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Tagging 3 | * Copyright (c) alexruperez 2019 4 | * Licensed under the MIT license (see LICENSE file) 5 | */ 6 | 7 | import XCTest 8 | @testable import Tagging 9 | 10 | final class TaggableTests: XCTestCase { 11 | 12 | func testStringBasedTags() { 13 | struct Model: Taggable { 14 | let tags: Tags 15 | } 16 | 17 | let model = Model(tags: ["foo", "bar"]) 18 | XCTAssertEqual(model.tags.first?.rawValue, "foo") 19 | XCTAssertEqual(model.tags.last?.rawValue, "bar") 20 | } 21 | 22 | func testIntBasedTags() { 23 | struct Model: Taggable { 24 | typealias RawTag = Int 25 | let tags: Tags 26 | } 27 | 28 | let model = Model(tags: [7, 9]) 29 | XCTAssertEqual(model.tags.first?.rawValue, 7) 30 | XCTAssertEqual(model.tags.last?.rawValue, 9) 31 | } 32 | 33 | func testCodableTags() throws { 34 | struct Model: Taggable, Codable { 35 | typealias RawTag = UUID 36 | let tags: Tags 37 | } 38 | 39 | let model = Model(tags: [Tag(rawValue: UUID()), Tag(rawValue: UUID())]) 40 | let data = try JSONEncoder().encode(model) 41 | let decoded = try JSONDecoder().decode(Model.self, from: data) 42 | XCTAssertEqual(model.tags.first, decoded.tags.first) 43 | XCTAssertEqual(model.tags.last, decoded.tags.last) 44 | } 45 | 46 | func testTagsEncodedAsSingleValue() throws { 47 | struct Model: Taggable, Codable { 48 | let tags: Tags 49 | } 50 | 51 | let model = Model(tags: ["foo", "bar"]) 52 | let data = try JSONEncoder().encode(model) 53 | let json = try JSONSerialization.jsonObject(with: data) as? [String: [String]] 54 | XCTAssertEqual(json?["tags"]?.first, "foo") 55 | } 56 | 57 | func testAllTestsRunOnLinux() { 58 | verifyAllTestsRunOnLinux() 59 | } 60 | 61 | } 62 | 63 | extension TaggableTests: LinuxTestable { 64 | static var allTests: [(String, (TaggableTests) -> () throws -> Void)] = [ 65 | ("testStringBasedTags", testStringBasedTags), 66 | ("testIntBasedTags", testIntBasedTags), 67 | ("testCodableTags", testCodableTags), 68 | ("testTagsEncodedAsSingleValue", testTagsEncodedAsSingleValue) 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /Tests/TaggingTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Tagging 3 | * Copyright (c) alexruperez 2019 4 | * Licensed under the MIT license (see LICENSE file) 5 | */ 6 | 7 | import XCTest 8 | 9 | #if !canImport(ObjectiveC) 10 | public func allTests() -> [XCTestCaseEntry] { 11 | return [ 12 | testCase(TaggableTests.allTests), 13 | testCase(CollectionTests.allTests) 14 | ] 15 | } 16 | #endif 17 | --------------------------------------------------------------------------------