├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Configurations ├── UniversalFramework_Base.xcconfig ├── UniversalFramework_Framework.xcconfig └── UniversalFramework_Test.xcconfig ├── LICENSE ├── Package.swift ├── README.md ├── Representor.podspec ├── Representor.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── Representor.xcscheme ├── Sources ├── Blueprint.swift ├── BlueprintTransition.swift ├── HTTPDeserialization.swift ├── HTTPHALAdapter.swift ├── HTTPSirenAdapter.swift ├── HTTPTransition.swift ├── HTTPTransitionBuilder.swift ├── Info.plist ├── Representor.h ├── Representor.swift ├── RepresentorBuilder.swift ├── Transition.swift └── TransitionBuilder.swift └── Tests ├── APIBlueprint ├── BlueprintTests.swift └── BlueprintTransitionTests.swift ├── Adapters ├── HALAdapterTests.swift ├── NSHTTPURLResponseTests.swift └── SirenAdapterTests.swift ├── Builder ├── HTTPTransitionBuilderTests.swift └── RepresentorBuilderTests.swift ├── Fixtures ├── blueprint.json ├── blueprint.md ├── poll.hal.json └── poll.siren.json ├── HTTPTransitionTests.swift ├── Info.plist ├── RepresentorTests.swift └── Utils.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.xccheckout 3 | *.xcscmblueprint 4 | xcuserdata 5 | /.build/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode9 3 | script: 4 | - set -o pipefail && xcodebuild -project Representor.xcodeproj -scheme Representor test -sdk macosx | xcpretty -c 5 | - pod lib lint --allow-warnings 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Representor Changelog 2 | 3 | ## Master 4 | 5 | ## Enhancements 6 | 7 | - Representor now targets Swift 4. 8 | 9 | ## 0.8.0 10 | 11 | ### Breaking 12 | 13 | - `InputProperty` is no longer a generic. 14 | 15 | ### Enhancements 16 | 17 | - Migrated to Swift 3. 18 | -------------------------------------------------------------------------------- /Configurations/UniversalFramework_Base.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Put this file alongside to the other both, as it contains what 3 | // both have in common. Don't rename this file. 4 | // 5 | // Copyright (c) 2014-2015 Marius Rackwitz. All rights reserved. 6 | // 7 | 8 | // Make it universal 9 | SUPPORTED_PLATFORMS = macosx iphonesimulator iphoneos watchos watchsimulator appletvos appletvsimulator 10 | VALID_ARCHS[sdk=macosx*] = i386 x86_64 11 | VALID_ARCHS[sdk=iphoneos*] = arm64 armv7 armv7s 12 | VALID_ARCHS[sdk=iphonesimulator*] = i386 x86_64 13 | VALID_ARCHS[sdk=watchos*] = armv7k 14 | VALID_ARCHS[sdk=watchsimulator*] = i386 15 | VALID_ARCHS[sdk=appletv*] = arm64 16 | VALID_ARCHS[sdk=appletvsimulator*] = x86_64 17 | 18 | // Dynamic linking uses different default copy paths 19 | LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' 20 | LD_RUNPATH_SEARCH_PATHS[sdk=iphoneos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 21 | LD_RUNPATH_SEARCH_PATHS[sdk=iphonesimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 22 | LD_RUNPATH_SEARCH_PATHS[sdk=watchos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 23 | LD_RUNPATH_SEARCH_PATHS[sdk=watchsimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 24 | LD_RUNPATH_SEARCH_PATHS[sdk=appletvos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 25 | LD_RUNPATH_SEARCH_PATHS[sdk=appletvsimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 26 | -------------------------------------------------------------------------------- /Configurations/UniversalFramework_Framework.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Inherit from this config in your framework target. 3 | // 4 | // Copyright (c) 2014-2015 Marius Rackwitz. All rights reserved. 5 | // 6 | 7 | #include "UniversalFramework_Base.xcconfig" 8 | 9 | // OSX-specific default settings 10 | FRAMEWORK_VERSION[sdk=macosx*] = A 11 | COMBINE_HIDPI_IMAGES[sdk=macosx*] = YES 12 | 13 | // iOS-specific default settings 14 | CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer 15 | TARGETED_DEVICE_FAMILY[sdk=iphonesimulator*] = 1,2 16 | TARGETED_DEVICE_FAMILY[sdk=iphone*] = 1,2 17 | 18 | // TV-specific default settings 19 | TARGETED_DEVICE_FAMILY[sdk=appletvsimulator*] = 3 20 | TARGETED_DEVICE_FAMILY[sdk=appletv*] = 3 21 | 22 | // Watch-specific default settings 23 | TARGETED_DEVICE_FAMILY[sdk=watchsimulator*] = 4 24 | TARGETED_DEVICE_FAMILY[sdk=watch*] = 4 25 | 26 | ENABLE_BITCODE[sdk=macosx*] = NO 27 | ENABLE_BITCODE[sdk=watchsimulator*] = YES 28 | ENABLE_BITCODE[sdk=watch*] = YES 29 | ENABLE_BITCODE[sdk=iphonesimulator*] = YES 30 | ENABLE_BITCODE[sdk=iphone*] = YES 31 | ENABLE_BITCODE[sdk=appletvsimulator*] = YES 32 | ENABLE_BITCODE[sdk=appletv*] = YES 33 | -------------------------------------------------------------------------------- /Configurations/UniversalFramework_Test.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Inherit from this config in the test target for your framework. 3 | // 4 | // Copyright (c) 2014-2015 Marius Rackwitz. All rights reserved. 5 | // 6 | 7 | #include "UniversalFramework_Base.xcconfig" 8 | 9 | FRAMEWORK_SEARCH_PATHS = $(inherited) '$(PLATFORM_DIR)/Developer/Library/Frameworks' 10 | 11 | // Yep. 12 | LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks' 13 | LD_RUNPATH_SEARCH_PATHS[sdk=iphoneos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 14 | LD_RUNPATH_SEARCH_PATHS[sdk=iphonesimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 15 | LD_RUNPATH_SEARCH_PATHS[sdk=watchos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 16 | LD_RUNPATH_SEARCH_PATHS[sdk=watchsimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 17 | LD_RUNPATH_SEARCH_PATHS[sdk=appletvos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 18 | LD_RUNPATH_SEARCH_PATHS[sdk=appletvsimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kyle Fuller 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 | import PackageDescription 2 | 3 | 4 | let package = Package( 5 | name: "Representor" 6 | ) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Representor in Swift 2 | 3 | Swift library for building and consuming Hypermedia messages. See [The Hypermedia Project Charter](https://github.com/the-hypermedia-project/charter) for details. 4 | 5 | ## Installation 6 | 7 | ### Swift Package Manager 8 | 9 | Representor can be installed via the Swift Package Manager by adding 10 | Representor as a dependency in your `Package.swift`: 11 | 12 | ```swift 13 | .Package(url: "https://github.com/the-hypermedia-project/representor-swift.git", majorVersion: 0, minor: 7) 14 | ``` 15 | 16 | ### CocoaPods 17 | 18 | Representor can be installed with CocoaPods by adding Representor to your 19 | `Podfile`: 20 | 21 | ```ruby 22 | pod 'Representor' 23 | ``` 24 | 25 | ### Sub-projects 26 | 27 | Alternatively, you can clone Representor via git or as a submodule and 28 | include Representor.xcodeproj inside your project and add 29 | Representor.framework as a target dependency. 30 | 31 | ## Usage 32 | 33 | ### Using the builder pattern to build a representor 34 | 35 | ```swift 36 | import Representor 37 | 38 | let representor = Representor { builder in 39 | builder.addTransition("self", uri:"/notes/2/") 40 | builder.addTransition("previous", uri:"/notes/1/") 41 | builder.addTransition("next", uri:"/notes/3/") 42 | 43 | builder.addMetaData("title", "Customer Details") 44 | 45 | builder.addTransition("create", uri:"/notes/") { transitionBuilder in 46 | transitionBuilder.method = "POST" 47 | transitionBuilder.addAttribute("title") 48 | transitionBuilder.addAttribute("note") 49 | } 50 | } 51 | ``` 52 | 53 | ### Consuming a representor 54 | 55 | ```swift 56 | if let create = representor.transitions["create"] { 57 | print("You can create with the URI: \(create.uri).") 58 | } 59 | 60 | if let next = representor.transitions["next"] { 61 | print("The next representor can be found at: \(next).") 62 | } 63 | 64 | if let prev = representor.transitions["previous"] { 65 | print("The previous representor can be found at: \(prev).") 66 | } 67 | ``` 68 | 69 | ### Adapters 70 | 71 | The representor includes adapters to convert between other hypermedia types. 72 | 73 | #### NSHTTPURLResponse 74 | 75 | You can initialise a representor using a `NSHTTPURLResponse` and the body (`NSData`). It will use the content-type from the response and deserialise the body payload into a format. For unsupported/unknown types, nil will returned. 76 | 77 | ```swift 78 | let representor = HTTPDeserialization.deserialize(response, body: body) 79 | ``` 80 | 81 | You can register your own, or overide an existing HTTP deserializer for a 82 | specific content type. 83 | 84 | ```swift 85 | HTTPDeserialization.deserializers["application/json"] = { response, body in 86 | return Representor(...) 87 | } 88 | ``` 89 | 90 | ##### Supported Media Types 91 | 92 | - [HAL](http://stateless.co/hal_specification.html) JSON (application/hal+json) 93 | - [Siren](https://github.com/kevinswiber/siren) JSON (application/vnd.siren+json) 94 | 95 | #### HAL 96 | 97 | You can explicitly convert to and from a [HAL](http://stateless.co/hal_specification.html) representation using the following. 98 | 99 | ```swift 100 | let representor = deserializeHAL(representation) 101 | ``` 102 | 103 | ```swift 104 | let representation = serializeHAL(representor) 105 | ``` 106 | 107 | #### Siren 108 | 109 | Conversion to and from a [Siren](https://github.com/kevinswiber/siren) representation can also be done using the following. 110 | 111 | ```swift 112 | let representor = deserializeSiren(representation) 113 | ``` 114 | 115 | ```swift 116 | let representation = serializeSiren(representor) 117 | ``` 118 | 119 | ## License 120 | 121 | Representor is released under the MIT license. See [LICENSE](LICENSE). 122 | 123 | -------------------------------------------------------------------------------- /Representor.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'Representor' 3 | spec.version = '0.8.0' 4 | spec.summary = 'A canonical resource object interface in Swift.' 5 | spec.homepage = 'https://github.com/the-hypermedia-project/representor-swift' 6 | spec.license = { :type => 'MIT', :file => 'LICENSE' } 7 | spec.author = { 'Kyle Fuller' => 'kyle@fuller.li' } 8 | spec.social_media_url = 'http://twitter.com/kylefuller' 9 | spec.source = { :git => 'https://github.com/the-hypermedia-project/representor-swift.git', :tag => "#{spec.version}" } 10 | spec.requires_arc = true 11 | spec.ios.deployment_target = '8.0' 12 | spec.osx.deployment_target = '10.9' 13 | spec.watchos.deployment_target = '2.0' 14 | spec.tvos.deployment_target = '9.0' 15 | spec.source_files = 'Sources/*.{swift,h}' 16 | end 17 | -------------------------------------------------------------------------------- /Representor.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 271629B71C4011D40027A90C /* RepresentorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271629B51C4011D40027A90C /* RepresentorBuilder.swift */; }; 11 | 271629B81C4011D40027A90C /* TransitionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271629B61C4011D40027A90C /* TransitionBuilder.swift */; }; 12 | 271629C51C4012C30027A90C /* HTTPDeserialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271629C21C4012C30027A90C /* HTTPDeserialization.swift */; }; 13 | 271629C61C4012C30027A90C /* HTTPTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271629C31C4012C30027A90C /* HTTPTransition.swift */; }; 14 | 271629C71C4012C30027A90C /* HTTPTransitionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271629C41C4012C30027A90C /* HTTPTransitionBuilder.swift */; }; 15 | 276A2C051A7BA4EE004BCC6F /* BlueprintTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276A2C031A7BA4EE004BCC6F /* BlueprintTests.swift */; }; 16 | 276A2C061A7BA4EE004BCC6F /* BlueprintTransitionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276A2C041A7BA4EE004BCC6F /* BlueprintTransitionTests.swift */; }; 17 | 276A2C081A7BA500004BCC6F /* blueprint.json in Resources */ = {isa = PBXBuildFile; fileRef = 276A2C071A7BA500004BCC6F /* blueprint.json */; }; 18 | 2774DFE21A474164008F41CE /* NSHTTPURLResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2774DFE11A474164008F41CE /* NSHTTPURLResponseTests.swift */; }; 19 | 279BCA861EC281E60042D05B /* HTTPHALAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271629B91C40125A0027A90C /* HTTPHALAdapter.swift */; }; 20 | 279BCA871EC2824D0042D05B /* HTTPSirenAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271629BA1C40125A0027A90C /* HTTPSirenAdapter.swift */; }; 21 | 279BCA881EC284050042D05B /* Blueprint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271629BD1C40126B0027A90C /* Blueprint.swift */; }; 22 | 279BCA891EC284120042D05B /* BlueprintTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271629BE1C40126B0027A90C /* BlueprintTransition.swift */; }; 23 | 770834691A0913860008869E /* Representor.h in Headers */ = {isa = PBXBuildFile; fileRef = 770834681A0913860008869E /* Representor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 24 | 7708346F1A0913860008869E /* Representor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 770834631A0913860008869E /* Representor.framework */; }; 25 | 770834761A0913860008869E /* RepresentorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770834751A0913860008869E /* RepresentorTests.swift */; }; 26 | 770834801A09144F0008869E /* Representor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7708347F1A09144F0008869E /* Representor.swift */; }; 27 | 770834821A0914CB0008869E /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770834811A0914CB0008869E /* Transition.swift */; }; 28 | 770834841A0914E40008869E /* HTTPTransitionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770834831A0914E40008869E /* HTTPTransitionTests.swift */; }; 29 | 77B507D01A0EB16100E794BF /* SirenAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77B507CF1A0EB16100E794BF /* SirenAdapterTests.swift */; }; 30 | 77BC15391A0A6A7700DD24EF /* RepresentorBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77BC15381A0A6A7700DD24EF /* RepresentorBuilderTests.swift */; }; 31 | 77BC153E1A0A6E2E00DD24EF /* HTTPTransitionBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77BC153D1A0A6E2E00DD24EF /* HTTPTransitionBuilderTests.swift */; }; 32 | 77D6433A1A0E6DC1004D4BA0 /* HALAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D643391A0E6DC1004D4BA0 /* HALAdapterTests.swift */; }; 33 | 77F592371A1B6E1B0070F839 /* poll.hal.json in Resources */ = {isa = PBXBuildFile; fileRef = 77F592341A1B6E1B0070F839 /* poll.hal.json */; }; 34 | 77F592381A1B6E1B0070F839 /* poll.siren.json in Resources */ = {isa = PBXBuildFile; fileRef = 77F592351A1B6E1B0070F839 /* poll.siren.json */; }; 35 | 77F5923A1A1B6F930070F839 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F592391A1B6F930070F839 /* Utils.swift */; }; 36 | /* End PBXBuildFile section */ 37 | 38 | /* Begin PBXContainerItemProxy section */ 39 | 770834701A0913860008869E /* PBXContainerItemProxy */ = { 40 | isa = PBXContainerItemProxy; 41 | containerPortal = 7708345A1A0913860008869E /* Project object */; 42 | proxyType = 1; 43 | remoteGlobalIDString = 770834621A0913860008869E; 44 | remoteInfo = Representor; 45 | }; 46 | /* End PBXContainerItemProxy section */ 47 | 48 | /* Begin PBXFileReference section */ 49 | 271629B51C4011D40027A90C /* RepresentorBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepresentorBuilder.swift; sourceTree = ""; }; 50 | 271629B61C4011D40027A90C /* TransitionBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionBuilder.swift; sourceTree = ""; }; 51 | 271629B91C40125A0027A90C /* HTTPHALAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPHALAdapter.swift; path = Sources/HTTPHALAdapter.swift; sourceTree = SOURCE_ROOT; }; 52 | 271629BA1C40125A0027A90C /* HTTPSirenAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPSirenAdapter.swift; path = Sources/HTTPSirenAdapter.swift; sourceTree = SOURCE_ROOT; }; 53 | 271629BD1C40126B0027A90C /* Blueprint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Blueprint.swift; sourceTree = ""; }; 54 | 271629BE1C40126B0027A90C /* BlueprintTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlueprintTransition.swift; sourceTree = ""; }; 55 | 271629C21C4012C30027A90C /* HTTPDeserialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPDeserialization.swift; path = Sources/HTTPDeserialization.swift; sourceTree = SOURCE_ROOT; }; 56 | 271629C31C4012C30027A90C /* HTTPTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPTransition.swift; path = Sources/HTTPTransition.swift; sourceTree = SOURCE_ROOT; }; 57 | 271629C41C4012C30027A90C /* HTTPTransitionBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPTransitionBuilder.swift; path = Sources/HTTPTransitionBuilder.swift; sourceTree = SOURCE_ROOT; }; 58 | 276A2C031A7BA4EE004BCC6F /* BlueprintTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlueprintTests.swift; sourceTree = ""; }; 59 | 276A2C041A7BA4EE004BCC6F /* BlueprintTransitionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlueprintTransitionTests.swift; sourceTree = ""; }; 60 | 276A2C071A7BA500004BCC6F /* blueprint.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = blueprint.json; sourceTree = ""; }; 61 | 2774DFE11A474164008F41CE /* NSHTTPURLResponseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NSHTTPURLResponseTests.swift; path = Adapters/NSHTTPURLResponseTests.swift; sourceTree = ""; }; 62 | 279294961A488A24009C52E1 /* UniversalFramework_Base.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Base.xcconfig; sourceTree = ""; }; 63 | 279294971A488A24009C52E1 /* UniversalFramework_Framework.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Framework.xcconfig; sourceTree = ""; }; 64 | 279294981A488A24009C52E1 /* UniversalFramework_Test.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Test.xcconfig; sourceTree = ""; }; 65 | 770834631A0913860008869E /* Representor.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Representor.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 66 | 770834671A0913860008869E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 67 | 770834681A0913860008869E /* Representor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Representor.h; sourceTree = ""; }; 68 | 7708346E1A0913860008869E /* RepresentorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RepresentorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 69 | 770834741A0913860008869E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 70 | 770834751A0913860008869E /* RepresentorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepresentorTests.swift; sourceTree = ""; }; 71 | 7708347F1A09144F0008869E /* Representor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Representor.swift; sourceTree = ""; }; 72 | 770834811A0914CB0008869E /* Transition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Transition.swift; sourceTree = ""; }; 73 | 770834831A0914E40008869E /* HTTPTransitionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPTransitionTests.swift; sourceTree = ""; }; 74 | 77B507CF1A0EB16100E794BF /* SirenAdapterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SirenAdapterTests.swift; path = Adapters/SirenAdapterTests.swift; sourceTree = ""; }; 75 | 77BC15381A0A6A7700DD24EF /* RepresentorBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RepresentorBuilderTests.swift; path = Builder/RepresentorBuilderTests.swift; sourceTree = ""; }; 76 | 77BC153D1A0A6E2E00DD24EF /* HTTPTransitionBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPTransitionBuilderTests.swift; path = Builder/HTTPTransitionBuilderTests.swift; sourceTree = ""; }; 77 | 77D643391A0E6DC1004D4BA0 /* HALAdapterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HALAdapterTests.swift; path = Adapters/HALAdapterTests.swift; sourceTree = ""; }; 78 | 77F592341A1B6E1B0070F839 /* poll.hal.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = poll.hal.json; sourceTree = ""; }; 79 | 77F592351A1B6E1B0070F839 /* poll.siren.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = poll.siren.json; sourceTree = ""; }; 80 | 77F592391A1B6F930070F839 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 81 | /* End PBXFileReference section */ 82 | 83 | /* Begin PBXFrameworksBuildPhase section */ 84 | 7708345F1A0913860008869E /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | 7708346B1A0913860008869E /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | 7708346F1A0913860008869E /* Representor.framework in Frameworks */, 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | /* End PBXFrameworksBuildPhase section */ 100 | 101 | /* Begin PBXGroup section */ 102 | 271629C11C4012710027A90C /* Blueprint */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 271629BD1C40126B0027A90C /* Blueprint.swift */, 106 | 271629BE1C40126B0027A90C /* BlueprintTransition.swift */, 107 | ); 108 | name = Blueprint; 109 | sourceTree = ""; 110 | }; 111 | 276A2C021A7BA4EE004BCC6F /* API Blueprint */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 276A2C031A7BA4EE004BCC6F /* BlueprintTests.swift */, 115 | 276A2C041A7BA4EE004BCC6F /* BlueprintTransitionTests.swift */, 116 | ); 117 | name = "API Blueprint"; 118 | path = APIBlueprint; 119 | sourceTree = ""; 120 | }; 121 | 279294951A488A24009C52E1 /* Configurations */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 279294961A488A24009C52E1 /* UniversalFramework_Base.xcconfig */, 125 | 279294971A488A24009C52E1 /* UniversalFramework_Framework.xcconfig */, 126 | 279294981A488A24009C52E1 /* UniversalFramework_Test.xcconfig */, 127 | ); 128 | path = Configurations; 129 | sourceTree = ""; 130 | }; 131 | 27931A6F1A7271B60084CB47 /* HTTP */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 271629C31C4012C30027A90C /* HTTPTransition.swift */, 135 | 271629C41C4012C30027A90C /* HTTPTransitionBuilder.swift */, 136 | 271629C21C4012C30027A90C /* HTTPDeserialization.swift */, 137 | 27931A741A727A280084CB47 /* Adapters */, 138 | 271629C11C4012710027A90C /* Blueprint */, 139 | ); 140 | name = HTTP; 141 | sourceTree = ""; 142 | }; 143 | 27931A741A727A280084CB47 /* Adapters */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | 271629B91C40125A0027A90C /* HTTPHALAdapter.swift */, 147 | 271629BA1C40125A0027A90C /* HTTPSirenAdapter.swift */, 148 | ); 149 | name = Adapters; 150 | path = HTTP/Adapters; 151 | sourceTree = ""; 152 | }; 153 | 770834591A0913860008869E = { 154 | isa = PBXGroup; 155 | children = ( 156 | 770834651A0913860008869E /* Representor */, 157 | 770834721A0913860008869E /* RepresentorTests */, 158 | 279294951A488A24009C52E1 /* Configurations */, 159 | 770834641A0913860008869E /* Products */, 160 | ); 161 | indentWidth = 2; 162 | sourceTree = ""; 163 | tabWidth = 2; 164 | }; 165 | 770834641A0913860008869E /* Products */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | 770834631A0913860008869E /* Representor.framework */, 169 | 7708346E1A0913860008869E /* RepresentorTests.xctest */, 170 | ); 171 | name = Products; 172 | sourceTree = ""; 173 | }; 174 | 770834651A0913860008869E /* Representor */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | 770834681A0913860008869E /* Representor.h */, 178 | 7708347F1A09144F0008869E /* Representor.swift */, 179 | 770834811A0914CB0008869E /* Transition.swift */, 180 | 27931A6F1A7271B60084CB47 /* HTTP */, 181 | 77BC15371A0A69DF00DD24EF /* Builder */, 182 | 770834661A0913860008869E /* Supporting Files */, 183 | ); 184 | name = Representor; 185 | path = Sources; 186 | sourceTree = ""; 187 | }; 188 | 770834661A0913860008869E /* Supporting Files */ = { 189 | isa = PBXGroup; 190 | children = ( 191 | 770834671A0913860008869E /* Info.plist */, 192 | ); 193 | name = "Supporting Files"; 194 | sourceTree = ""; 195 | }; 196 | 770834721A0913860008869E /* RepresentorTests */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | 77F592321A1B6E1B0070F839 /* Fixtures */, 200 | 77F592391A1B6F930070F839 /* Utils.swift */, 201 | 770834751A0913860008869E /* RepresentorTests.swift */, 202 | 770834831A0914E40008869E /* HTTPTransitionTests.swift */, 203 | 77BC153A1A0A6A7B00DD24EF /* Builder */, 204 | 77D6433B1A0E6DC5004D4BA0 /* Adapters */, 205 | 276A2C021A7BA4EE004BCC6F /* API Blueprint */, 206 | 770834731A0913860008869E /* Supporting Files */, 207 | ); 208 | name = RepresentorTests; 209 | path = Tests; 210 | sourceTree = ""; 211 | }; 212 | 770834731A0913860008869E /* Supporting Files */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | 770834741A0913860008869E /* Info.plist */, 216 | ); 217 | name = "Supporting Files"; 218 | sourceTree = ""; 219 | }; 220 | 77BC15371A0A69DF00DD24EF /* Builder */ = { 221 | isa = PBXGroup; 222 | children = ( 223 | 271629B51C4011D40027A90C /* RepresentorBuilder.swift */, 224 | 271629B61C4011D40027A90C /* TransitionBuilder.swift */, 225 | ); 226 | name = Builder; 227 | sourceTree = ""; 228 | }; 229 | 77BC153A1A0A6A7B00DD24EF /* Builder */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | 77BC15381A0A6A7700DD24EF /* RepresentorBuilderTests.swift */, 233 | 77BC153D1A0A6E2E00DD24EF /* HTTPTransitionBuilderTests.swift */, 234 | ); 235 | name = Builder; 236 | sourceTree = ""; 237 | }; 238 | 77D6433B1A0E6DC5004D4BA0 /* Adapters */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | 2774DFE11A474164008F41CE /* NSHTTPURLResponseTests.swift */, 242 | 77D643391A0E6DC1004D4BA0 /* HALAdapterTests.swift */, 243 | 77B507CF1A0EB16100E794BF /* SirenAdapterTests.swift */, 244 | ); 245 | name = Adapters; 246 | sourceTree = ""; 247 | }; 248 | 77F592321A1B6E1B0070F839 /* Fixtures */ = { 249 | isa = PBXGroup; 250 | children = ( 251 | 276A2C071A7BA500004BCC6F /* blueprint.json */, 252 | 77F592341A1B6E1B0070F839 /* poll.hal.json */, 253 | 77F592351A1B6E1B0070F839 /* poll.siren.json */, 254 | ); 255 | path = Fixtures; 256 | sourceTree = ""; 257 | }; 258 | /* End PBXGroup section */ 259 | 260 | /* Begin PBXHeadersBuildPhase section */ 261 | 770834601A0913860008869E /* Headers */ = { 262 | isa = PBXHeadersBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | 770834691A0913860008869E /* Representor.h in Headers */, 266 | ); 267 | runOnlyForDeploymentPostprocessing = 0; 268 | }; 269 | /* End PBXHeadersBuildPhase section */ 270 | 271 | /* Begin PBXNativeTarget section */ 272 | 770834621A0913860008869E /* Representor */ = { 273 | isa = PBXNativeTarget; 274 | buildConfigurationList = 770834791A0913860008869E /* Build configuration list for PBXNativeTarget "Representor" */; 275 | buildPhases = ( 276 | 7708345E1A0913860008869E /* Sources */, 277 | 7708345F1A0913860008869E /* Frameworks */, 278 | 770834601A0913860008869E /* Headers */, 279 | 770834611A0913860008869E /* Resources */, 280 | ); 281 | buildRules = ( 282 | ); 283 | dependencies = ( 284 | ); 285 | name = Representor; 286 | productName = Representor; 287 | productReference = 770834631A0913860008869E /* Representor.framework */; 288 | productType = "com.apple.product-type.framework"; 289 | }; 290 | 7708346D1A0913860008869E /* RepresentorTests */ = { 291 | isa = PBXNativeTarget; 292 | buildConfigurationList = 7708347C1A0913860008869E /* Build configuration list for PBXNativeTarget "RepresentorTests" */; 293 | buildPhases = ( 294 | 7708346A1A0913860008869E /* Sources */, 295 | 7708346B1A0913860008869E /* Frameworks */, 296 | 7708346C1A0913860008869E /* Resources */, 297 | ); 298 | buildRules = ( 299 | ); 300 | dependencies = ( 301 | 770834711A0913860008869E /* PBXTargetDependency */, 302 | ); 303 | name = RepresentorTests; 304 | productName = RepresentorTests; 305 | productReference = 7708346E1A0913860008869E /* RepresentorTests.xctest */; 306 | productType = "com.apple.product-type.bundle.unit-test"; 307 | }; 308 | /* End PBXNativeTarget section */ 309 | 310 | /* Begin PBXProject section */ 311 | 7708345A1A0913860008869E /* Project object */ = { 312 | isa = PBXProject; 313 | attributes = { 314 | LastSwiftMigration = 0700; 315 | LastSwiftUpdateCheck = 0700; 316 | LastUpgradeCheck = 0900; 317 | ORGANIZATIONNAME = Apiary; 318 | TargetAttributes = { 319 | 770834621A0913860008869E = { 320 | CreatedOnToolsVersion = 6.1; 321 | LastSwiftMigration = 0900; 322 | }; 323 | 7708346D1A0913860008869E = { 324 | CreatedOnToolsVersion = 6.1; 325 | LastSwiftMigration = 0900; 326 | }; 327 | }; 328 | }; 329 | buildConfigurationList = 7708345D1A0913860008869E /* Build configuration list for PBXProject "Representor" */; 330 | compatibilityVersion = "Xcode 3.2"; 331 | developmentRegion = English; 332 | hasScannedForEncodings = 0; 333 | knownRegions = ( 334 | en, 335 | ); 336 | mainGroup = 770834591A0913860008869E; 337 | productRefGroup = 770834641A0913860008869E /* Products */; 338 | projectDirPath = ""; 339 | projectRoot = ""; 340 | targets = ( 341 | 770834621A0913860008869E /* Representor */, 342 | 7708346D1A0913860008869E /* RepresentorTests */, 343 | ); 344 | }; 345 | /* End PBXProject section */ 346 | 347 | /* Begin PBXResourcesBuildPhase section */ 348 | 770834611A0913860008869E /* Resources */ = { 349 | isa = PBXResourcesBuildPhase; 350 | buildActionMask = 2147483647; 351 | files = ( 352 | ); 353 | runOnlyForDeploymentPostprocessing = 0; 354 | }; 355 | 7708346C1A0913860008869E /* Resources */ = { 356 | isa = PBXResourcesBuildPhase; 357 | buildActionMask = 2147483647; 358 | files = ( 359 | 77F592371A1B6E1B0070F839 /* poll.hal.json in Resources */, 360 | 77F592381A1B6E1B0070F839 /* poll.siren.json in Resources */, 361 | 276A2C081A7BA500004BCC6F /* blueprint.json in Resources */, 362 | ); 363 | runOnlyForDeploymentPostprocessing = 0; 364 | }; 365 | /* End PBXResourcesBuildPhase section */ 366 | 367 | /* Begin PBXSourcesBuildPhase section */ 368 | 7708345E1A0913860008869E /* Sources */ = { 369 | isa = PBXSourcesBuildPhase; 370 | buildActionMask = 2147483647; 371 | files = ( 372 | 271629B71C4011D40027A90C /* RepresentorBuilder.swift in Sources */, 373 | 271629C71C4012C30027A90C /* HTTPTransitionBuilder.swift in Sources */, 374 | 279BCA871EC2824D0042D05B /* HTTPSirenAdapter.swift in Sources */, 375 | 770834821A0914CB0008869E /* Transition.swift in Sources */, 376 | 271629C61C4012C30027A90C /* HTTPTransition.swift in Sources */, 377 | 279BCA891EC284120042D05B /* BlueprintTransition.swift in Sources */, 378 | 271629C51C4012C30027A90C /* HTTPDeserialization.swift in Sources */, 379 | 279BCA861EC281E60042D05B /* HTTPHALAdapter.swift in Sources */, 380 | 279BCA881EC284050042D05B /* Blueprint.swift in Sources */, 381 | 271629B81C4011D40027A90C /* TransitionBuilder.swift in Sources */, 382 | 770834801A09144F0008869E /* Representor.swift in Sources */, 383 | ); 384 | runOnlyForDeploymentPostprocessing = 0; 385 | }; 386 | 7708346A1A0913860008869E /* Sources */ = { 387 | isa = PBXSourcesBuildPhase; 388 | buildActionMask = 2147483647; 389 | files = ( 390 | 77BC153E1A0A6E2E00DD24EF /* HTTPTransitionBuilderTests.swift in Sources */, 391 | 2774DFE21A474164008F41CE /* NSHTTPURLResponseTests.swift in Sources */, 392 | 77B507D01A0EB16100E794BF /* SirenAdapterTests.swift in Sources */, 393 | 77D6433A1A0E6DC1004D4BA0 /* HALAdapterTests.swift in Sources */, 394 | 77F5923A1A1B6F930070F839 /* Utils.swift in Sources */, 395 | 770834841A0914E40008869E /* HTTPTransitionTests.swift in Sources */, 396 | 276A2C061A7BA4EE004BCC6F /* BlueprintTransitionTests.swift in Sources */, 397 | 770834761A0913860008869E /* RepresentorTests.swift in Sources */, 398 | 77BC15391A0A6A7700DD24EF /* RepresentorBuilderTests.swift in Sources */, 399 | 276A2C051A7BA4EE004BCC6F /* BlueprintTests.swift in Sources */, 400 | ); 401 | runOnlyForDeploymentPostprocessing = 0; 402 | }; 403 | /* End PBXSourcesBuildPhase section */ 404 | 405 | /* Begin PBXTargetDependency section */ 406 | 770834711A0913860008869E /* PBXTargetDependency */ = { 407 | isa = PBXTargetDependency; 408 | target = 770834621A0913860008869E /* Representor */; 409 | targetProxy = 770834701A0913860008869E /* PBXContainerItemProxy */; 410 | }; 411 | /* End PBXTargetDependency section */ 412 | 413 | /* Begin XCBuildConfiguration section */ 414 | 770834771A0913860008869E /* Debug */ = { 415 | isa = XCBuildConfiguration; 416 | buildSettings = { 417 | ALWAYS_SEARCH_USER_PATHS = NO; 418 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 419 | CLANG_CXX_LIBRARY = "libc++"; 420 | CLANG_ENABLE_MODULES = YES; 421 | CLANG_ENABLE_OBJC_ARC = YES; 422 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 423 | CLANG_WARN_BOOL_CONVERSION = YES; 424 | CLANG_WARN_COMMA = YES; 425 | CLANG_WARN_CONSTANT_CONVERSION = YES; 426 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 427 | CLANG_WARN_EMPTY_BODY = YES; 428 | CLANG_WARN_ENUM_CONVERSION = YES; 429 | CLANG_WARN_INFINITE_RECURSION = YES; 430 | CLANG_WARN_INT_CONVERSION = YES; 431 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 432 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 433 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 434 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 435 | CLANG_WARN_STRICT_PROTOTYPES = YES; 436 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 437 | CLANG_WARN_UNREACHABLE_CODE = YES; 438 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 439 | COPY_PHASE_STRIP = NO; 440 | CURRENT_PROJECT_VERSION = 1; 441 | ENABLE_STRICT_OBJC_MSGSEND = YES; 442 | ENABLE_TESTABILITY = YES; 443 | GCC_C_LANGUAGE_STANDARD = gnu99; 444 | GCC_DYNAMIC_NO_PIC = NO; 445 | GCC_NO_COMMON_BLOCKS = YES; 446 | GCC_OPTIMIZATION_LEVEL = 0; 447 | GCC_PREPROCESSOR_DEFINITIONS = ( 448 | "DEBUG=1", 449 | "$(inherited)", 450 | ); 451 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 452 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 453 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 454 | GCC_WARN_UNDECLARED_SELECTOR = YES; 455 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 456 | GCC_WARN_UNUSED_FUNCTION = YES; 457 | GCC_WARN_UNUSED_VARIABLE = YES; 458 | MACOSX_DEPLOYMENT_TARGET = 10.9; 459 | MTL_ENABLE_DEBUG_INFO = YES; 460 | ONLY_ACTIVE_ARCH = YES; 461 | SDKROOT = macosx; 462 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 463 | VERSIONING_SYSTEM = "apple-generic"; 464 | VERSION_INFO_PREFIX = ""; 465 | }; 466 | name = Debug; 467 | }; 468 | 770834781A0913860008869E /* Release */ = { 469 | isa = XCBuildConfiguration; 470 | buildSettings = { 471 | ALWAYS_SEARCH_USER_PATHS = NO; 472 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 473 | CLANG_CXX_LIBRARY = "libc++"; 474 | CLANG_ENABLE_MODULES = YES; 475 | CLANG_ENABLE_OBJC_ARC = YES; 476 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 477 | CLANG_WARN_BOOL_CONVERSION = YES; 478 | CLANG_WARN_COMMA = YES; 479 | CLANG_WARN_CONSTANT_CONVERSION = YES; 480 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 481 | CLANG_WARN_EMPTY_BODY = YES; 482 | CLANG_WARN_ENUM_CONVERSION = YES; 483 | CLANG_WARN_INFINITE_RECURSION = YES; 484 | CLANG_WARN_INT_CONVERSION = YES; 485 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 486 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 487 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 488 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 489 | CLANG_WARN_STRICT_PROTOTYPES = YES; 490 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 491 | CLANG_WARN_UNREACHABLE_CODE = YES; 492 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 493 | COPY_PHASE_STRIP = YES; 494 | CURRENT_PROJECT_VERSION = 1; 495 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 496 | ENABLE_NS_ASSERTIONS = NO; 497 | ENABLE_STRICT_OBJC_MSGSEND = YES; 498 | GCC_C_LANGUAGE_STANDARD = gnu99; 499 | GCC_NO_COMMON_BLOCKS = YES; 500 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 501 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 502 | GCC_WARN_UNDECLARED_SELECTOR = YES; 503 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 504 | GCC_WARN_UNUSED_FUNCTION = YES; 505 | GCC_WARN_UNUSED_VARIABLE = YES; 506 | MACOSX_DEPLOYMENT_TARGET = 10.9; 507 | MTL_ENABLE_DEBUG_INFO = NO; 508 | SDKROOT = macosx; 509 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 510 | VERSIONING_SYSTEM = "apple-generic"; 511 | VERSION_INFO_PREFIX = ""; 512 | }; 513 | name = Release; 514 | }; 515 | 7708347A1A0913860008869E /* Debug */ = { 516 | isa = XCBuildConfiguration; 517 | baseConfigurationReference = 279294971A488A24009C52E1 /* UniversalFramework_Framework.xcconfig */; 518 | buildSettings = { 519 | CLANG_ENABLE_MODULES = YES; 520 | COMBINE_HIDPI_IMAGES = YES; 521 | DEFINES_MODULE = YES; 522 | DYLIB_COMPATIBILITY_VERSION = 1; 523 | DYLIB_CURRENT_VERSION = 1; 524 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 525 | FRAMEWORK_VERSION = A; 526 | INFOPLIST_FILE = Sources/Info.plist; 527 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 528 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 529 | PRODUCT_BUNDLE_IDENTIFIER = "io.apiary.$(PRODUCT_NAME:rfc1034identifier)"; 530 | PRODUCT_NAME = Representor; 531 | SKIP_INSTALL = YES; 532 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 533 | SWIFT_VERSION = 4.0; 534 | }; 535 | name = Debug; 536 | }; 537 | 7708347B1A0913860008869E /* Release */ = { 538 | isa = XCBuildConfiguration; 539 | baseConfigurationReference = 279294971A488A24009C52E1 /* UniversalFramework_Framework.xcconfig */; 540 | buildSettings = { 541 | CLANG_ENABLE_MODULES = YES; 542 | COMBINE_HIDPI_IMAGES = YES; 543 | DEFINES_MODULE = YES; 544 | DYLIB_COMPATIBILITY_VERSION = 1; 545 | DYLIB_CURRENT_VERSION = 1; 546 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 547 | FRAMEWORK_VERSION = A; 548 | INFOPLIST_FILE = Sources/Info.plist; 549 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 550 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 551 | PRODUCT_BUNDLE_IDENTIFIER = "io.apiary.$(PRODUCT_NAME:rfc1034identifier)"; 552 | PRODUCT_NAME = Representor; 553 | SKIP_INSTALL = YES; 554 | SWIFT_VERSION = 4.0; 555 | }; 556 | name = Release; 557 | }; 558 | 7708347D1A0913860008869E /* Debug */ = { 559 | isa = XCBuildConfiguration; 560 | baseConfigurationReference = 279294981A488A24009C52E1 /* UniversalFramework_Test.xcconfig */; 561 | buildSettings = { 562 | COMBINE_HIDPI_IMAGES = YES; 563 | GCC_PREPROCESSOR_DEFINITIONS = ( 564 | "DEBUG=1", 565 | "$(inherited)", 566 | ); 567 | INFOPLIST_FILE = Tests/Info.plist; 568 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 569 | PRODUCT_BUNDLE_IDENTIFIER = "io.apiary.$(PRODUCT_NAME:rfc1034identifier)"; 570 | PRODUCT_NAME = RepresentorTests; 571 | SWIFT_VERSION = 4.0; 572 | }; 573 | name = Debug; 574 | }; 575 | 7708347E1A0913860008869E /* Release */ = { 576 | isa = XCBuildConfiguration; 577 | baseConfigurationReference = 279294981A488A24009C52E1 /* UniversalFramework_Test.xcconfig */; 578 | buildSettings = { 579 | COMBINE_HIDPI_IMAGES = YES; 580 | INFOPLIST_FILE = Tests/Info.plist; 581 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 582 | PRODUCT_BUNDLE_IDENTIFIER = "io.apiary.$(PRODUCT_NAME:rfc1034identifier)"; 583 | PRODUCT_NAME = RepresentorTests; 584 | SWIFT_VERSION = 4.0; 585 | }; 586 | name = Release; 587 | }; 588 | /* End XCBuildConfiguration section */ 589 | 590 | /* Begin XCConfigurationList section */ 591 | 7708345D1A0913860008869E /* Build configuration list for PBXProject "Representor" */ = { 592 | isa = XCConfigurationList; 593 | buildConfigurations = ( 594 | 770834771A0913860008869E /* Debug */, 595 | 770834781A0913860008869E /* Release */, 596 | ); 597 | defaultConfigurationIsVisible = 0; 598 | defaultConfigurationName = Release; 599 | }; 600 | 770834791A0913860008869E /* Build configuration list for PBXNativeTarget "Representor" */ = { 601 | isa = XCConfigurationList; 602 | buildConfigurations = ( 603 | 7708347A1A0913860008869E /* Debug */, 604 | 7708347B1A0913860008869E /* Release */, 605 | ); 606 | defaultConfigurationIsVisible = 0; 607 | defaultConfigurationName = Release; 608 | }; 609 | 7708347C1A0913860008869E /* Build configuration list for PBXNativeTarget "RepresentorTests" */ = { 610 | isa = XCConfigurationList; 611 | buildConfigurations = ( 612 | 7708347D1A0913860008869E /* Debug */, 613 | 7708347E1A0913860008869E /* Release */, 614 | ); 615 | defaultConfigurationIsVisible = 0; 616 | defaultConfigurationName = Release; 617 | }; 618 | /* End XCConfigurationList section */ 619 | }; 620 | rootObject = 7708345A1A0913860008869E /* Project object */; 621 | } 622 | -------------------------------------------------------------------------------- /Representor.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Representor.xcodeproj/xcshareddata/xcschemes/Representor.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 80 | 81 | 87 | 88 | 89 | 90 | 91 | 92 | 98 | 99 | 105 | 106 | 107 | 108 | 110 | 111 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /Sources/Blueprint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Blueprint.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 06/01/2015. 6 | // Copyright (c) 2015 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: Models 12 | 13 | public typealias Metadata = (name:String, value:String) 14 | 15 | /// A structure representing an API Blueprint AST 16 | public struct Blueprint { 17 | /// Name of the API 18 | public let name:String 19 | 20 | /// Top-level description of the API in Markdown (.raw) or HTML (.html) 21 | public let description:String? 22 | 23 | /// The collection of resource groups 24 | public let resourceGroups:[ResourceGroup] 25 | 26 | public let metadata:[Metadata] 27 | 28 | public init(name:String, description:String?, resourceGroups: [ResourceGroup]) { 29 | self.metadata = [] 30 | self.name = name 31 | self.description = description 32 | self.resourceGroups = resourceGroups 33 | } 34 | 35 | public init(ast: [String: AnyObject]) { 36 | metadata = parseMetadata(ast["metadata"] as? [[String: String]]) 37 | name = ast["name"] as? String ?? "" 38 | description = ast["description"] as? String 39 | resourceGroups = parseBlueprintResourceGroups(ast) 40 | } 41 | 42 | public init?(named:String, bundle:Bundle? = nil) { 43 | func loadFile(_ named:String, bundle:Bundle) -> [String: AnyObject]? { 44 | if let url = bundle.url(forResource: named, withExtension: nil) { 45 | if let data = try? Data(contentsOf: url) { 46 | let object: AnyObject? = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions(rawValue: 0)) as AnyObject 47 | return object as? [String:AnyObject] 48 | } 49 | } 50 | 51 | return nil 52 | } 53 | 54 | let ast = loadFile(named, bundle: bundle ?? Bundle.main) 55 | if let ast = ast { 56 | self.init(ast: ast) 57 | } else { 58 | return nil 59 | } 60 | } 61 | } 62 | 63 | /// Logical group of resources. 64 | public struct ResourceGroup { 65 | /// Name of the Resource Group 66 | public let name:String 67 | 68 | /// Description of the Resource Group (.raw or .html) 69 | public let description:String? 70 | 71 | /// Array of the respective resources belonging to the Resource Group 72 | public let resources:[Resource] 73 | 74 | public init(name:String, description:String?, resources:[Resource]) { 75 | self.name = name 76 | self.description = description 77 | self.resources = resources 78 | } 79 | } 80 | 81 | /// Description of one resource, or a cluster of resources defined by its URI template 82 | public struct Resource { 83 | /// Name of the Resource 84 | public let name:String 85 | 86 | /// Description of the Resource (.raw or .html) 87 | public let description:String? 88 | 89 | /// URI Template as defined in RFC6570 90 | // TODO, make this a URITemplate object 91 | public let uriTemplate:String 92 | 93 | /// Array of URI parameters 94 | public let parameters:[Parameter] 95 | 96 | /// Array of actions available on the resource each defining at least one complete HTTP transaction 97 | public let actions:[Action] 98 | 99 | public let content:[[String:AnyObject]] 100 | 101 | public init(name:String, description:String?, uriTemplate:String, parameters:[Parameter], actions:[Action], content:[[String:AnyObject]]? = nil) { 102 | self.name = name 103 | self.description = description 104 | self.uriTemplate = uriTemplate 105 | self.actions = actions 106 | self.parameters = parameters 107 | self.content = content ?? [] 108 | } 109 | } 110 | 111 | /// Description of one URI template parameter 112 | public struct Parameter { 113 | /// Name of the parameter 114 | public let name:String 115 | 116 | /// Description of the Parameter (.raw or .html) 117 | public let description:String? 118 | 119 | /// An arbitrary type of the parameter (a string) 120 | public let type:String? 121 | 122 | /// Boolean flag denoting whether the parameter is required (true) or not (false) 123 | public let required:Bool 124 | 125 | /// A default value of the parameter (a value assumed when the parameter is not specified) 126 | public let defaultValue:String? 127 | 128 | /// An example value of the parameter 129 | public let example:String? 130 | 131 | /// An array enumerating possible parameter values 132 | public let values:[String]? 133 | 134 | public init(name:String, description:String?, type:String?, required:Bool, defaultValue:String?, example:String?, values:[String]?) { 135 | self.name = name 136 | self.description = description 137 | self.type = type 138 | self.required = required 139 | self.defaultValue = defaultValue 140 | self.example = example 141 | self.values = values 142 | } 143 | } 144 | 145 | // An HTTP transaction (a request-response transaction). Actions are specified by an HTTP request method within a resource 146 | public struct Action { 147 | /// Name of the Action 148 | public let name:String 149 | 150 | /// Description of the Action (.raw or .html) 151 | public let description:String? 152 | 153 | /// HTTP request method defining the action 154 | public let method:String 155 | 156 | /// Array of URI parameters 157 | public let parameters:[Parameter] 158 | 159 | /// URI Template for the action, if it differs from the resource's URI 160 | public let uriTemplate:String? 161 | 162 | /// Link relation identifier of the action 163 | public let relation:String? 164 | 165 | /// HTTP transaction examples for the relevant HTTP request method 166 | public let examples:[TransactionExample] 167 | 168 | public let content:[[String:AnyObject]] 169 | 170 | public init(name:String, description:String?, method:String, parameters:[Parameter], uriTemplate:String? = nil, relation:String? = nil, examples:[TransactionExample]? = nil, content:[[String:AnyObject]]? = nil) { 171 | self.name = name 172 | self.description = description 173 | self.method = method 174 | self.parameters = parameters 175 | self.uriTemplate = uriTemplate 176 | self.relation = relation 177 | self.examples = examples ?? [] 178 | self.content = content ?? [] 179 | } 180 | } 181 | 182 | /// An HTTP transaction example with expected HTTP message request and response payload 183 | public struct TransactionExample { 184 | /// Name of the Transaction Example 185 | public let name:String 186 | 187 | /// Description of the Transaction Example (.raw or .html) 188 | public let description:String? 189 | 190 | /// Example transaction request payloads 191 | public let requests:[Payload] 192 | 193 | /// Example transaction response payloads 194 | public let responses:[Payload] 195 | 196 | public init(name:String, description:String? = nil, requests:[Payload]? = nil, responses:[Payload]? = nil) { 197 | self.name = name 198 | self.description = description 199 | self.requests = requests ?? [] 200 | self.responses = responses ?? [] 201 | } 202 | } 203 | 204 | 205 | /// An API Blueprint payload. 206 | public struct Payload { 207 | public typealias Header = (name:String, value:String) 208 | 209 | /// Name of the payload 210 | public let name:String 211 | 212 | /// Description of the Payload (.raw or .html) 213 | public let description:String? 214 | 215 | /// HTTP headers that are expected to be transferred with HTTP message represented by this payload 216 | public let headers:[Header] 217 | 218 | /// An entity body to be transferred with HTTP message represented by this payload 219 | public let body:Data? 220 | 221 | public let content:[[String:AnyObject]] 222 | 223 | public init(name:String, description:String? = nil, headers:[Header]? = nil, body:Data? = nil, content:[[String:AnyObject]]? = nil) { 224 | self.name = name 225 | self.description = description 226 | self.headers = headers ?? [] 227 | self.body = body 228 | self.content = content ?? [] 229 | } 230 | } 231 | 232 | 233 | // MARK: AST Parsing 234 | 235 | func parseMetadata(_ source:[[String:String]]?) -> [Metadata] { 236 | if let source = source { 237 | return source.flatMap { item in 238 | if let name = item["name"] { 239 | if let value = item["value"] { 240 | return (name: name, value: value) 241 | } 242 | } 243 | 244 | return nil 245 | } 246 | } 247 | 248 | return [] 249 | } 250 | 251 | func parseParameter(_ source:[[String:AnyObject]]?) -> [Parameter] { 252 | if let source = source { 253 | return source.map { item in 254 | let name = item["name"] as? String ?? "" 255 | let description = item["description"] as? String 256 | let type = item["type"] as? String 257 | let required = item["required"] as? Bool 258 | let defaultValue = item["default"] as? String 259 | let example = item["example"] as? String 260 | let values = item["values"] as? [String] 261 | return Parameter(name: name, description: description, type: type, required: required ?? true, defaultValue: defaultValue, example: example, values: values) 262 | } 263 | } 264 | 265 | return [] 266 | } 267 | 268 | func parseActions(_ source:[[String:AnyObject]]?) -> [Action] { 269 | if let source = source { 270 | return source.flatMap { item in 271 | let name = item["name"] as? String 272 | let description = item["description"] as? String 273 | let method = item["method"] as? String 274 | let parameters = parseParameter(item["parameters"] as? [[String:AnyObject]]) 275 | let attributes = item["attributes"] as? [String:String] 276 | let uriTemplate = attributes?["uriTemplate"] 277 | let relation = attributes?["relation"] 278 | let examples = parseExamples(item["examples"] as? [[String:AnyObject]]) 279 | let content = item["content"] as? [[String:AnyObject]] 280 | 281 | if let name = name { 282 | if let method = method { 283 | return Action(name: name, description: description, method: method, parameters: parameters, uriTemplate:uriTemplate, relation:relation, examples:examples, content:content) 284 | } 285 | } 286 | 287 | return nil 288 | } 289 | } 290 | 291 | return [] 292 | } 293 | 294 | func parseExamples(_ source:[[String:AnyObject]]?) -> [TransactionExample] { 295 | if let source = source { 296 | return source.flatMap { item in 297 | let name = item["name"] as? String 298 | let description = item["description"] as? String 299 | let requests = parsePayloads(item["requests"] as? [[String:AnyObject]]) 300 | let responses = parsePayloads(item["responses"] as? [[String:AnyObject]]) 301 | 302 | if let name = name { 303 | return TransactionExample(name: name, description: description, requests: requests, responses: responses) 304 | } 305 | 306 | return nil 307 | } 308 | } 309 | 310 | return [] 311 | } 312 | 313 | func parsePayloads(_ source:[[String:AnyObject]]?) -> [Payload] { 314 | if let source = source { 315 | return source.flatMap { item in 316 | let name = item["name"] as? String 317 | let description = item["description"] as? String 318 | let headers = parseHeaders(item["headers"] as? [[String:String]]) 319 | let bodyString = item["body"] as? String 320 | let body = bodyString?.data(using: String.Encoding.utf8, allowLossyConversion: true) 321 | let content = item["content"] as? [[String:AnyObject]] 322 | 323 | if let name = name { 324 | return Payload(name: name, description: description, headers: headers, body: body, content: content) 325 | } 326 | 327 | return nil 328 | } 329 | } 330 | 331 | return [] 332 | } 333 | 334 | func parseHeaders(_ source:[[String:String]]?) -> [Payload.Header] { 335 | if let source = source { 336 | return source.flatMap { item in 337 | if let name = item["name"], let value = item["value"] { 338 | return (name, value) 339 | } 340 | 341 | return nil 342 | } 343 | } 344 | 345 | return [] 346 | } 347 | 348 | func parseResources(_ source:[[String:AnyObject]]?) -> [Resource] { 349 | if let source = source { 350 | return source.flatMap { item in 351 | let name = item["name"] as? String 352 | let description = item["description"] as? String 353 | let uriTemplate = item["uriTemplate"] as? String 354 | let actions = parseActions(item["actions"] as? [[String:AnyObject]]) 355 | let parameters = parseParameter(item["parameters"] as? [[String:AnyObject]]) 356 | let content = item["content"] as? [[String:AnyObject]] 357 | 358 | if let name = name, let uriTemplate = uriTemplate { 359 | return Resource(name: name, description: description, uriTemplate: uriTemplate, parameters: parameters, actions: actions, content: content) 360 | } 361 | 362 | return nil 363 | } 364 | } 365 | 366 | return [] 367 | } 368 | 369 | private func parseBlueprintResourceGroups(_ blueprint:[String:AnyObject]) -> [ResourceGroup] { 370 | if let resourceGroups = blueprint["resourceGroups"] as? [[String:AnyObject]] { 371 | return resourceGroups.flatMap { dictionary in 372 | if let name = dictionary["name"] as? String { 373 | let resources = parseResources(dictionary["resources"] as? [[String:AnyObject]]) 374 | let description = dictionary["description"] as? String 375 | return ResourceGroup(name: name, description: description, resources: resources) 376 | } 377 | 378 | return nil 379 | } 380 | } 381 | 382 | return [] 383 | } 384 | -------------------------------------------------------------------------------- /Sources/BlueprintTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlueprintTransition.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 28/01/2015. 6 | // Copyright (c) 2015 Apiary LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Resource { 12 | func transition(_ actionName:String) -> HTTPTransition? { 13 | func filterAction(_ action:Action) -> Bool { 14 | if let relationName = action.relation { 15 | if relationName == actionName { 16 | return true 17 | } 18 | } 19 | 20 | if action.name == actionName { 21 | return true 22 | } 23 | 24 | return false 25 | } 26 | 27 | if let action = actions.filter(filterAction).first { 28 | return HTTPTransition.from(resource: self, action: action) 29 | } 30 | 31 | return nil 32 | } 33 | } 34 | 35 | func parseAttributes(_ dataStructure: [String: AnyObject], builder:HTTPTransitionBuilder) { 36 | func isPropertyRequired(_ property: [String: AnyObject]) -> Bool? { 37 | if let valueDefinition = property["valueDefinition"] as? [String:AnyObject], 38 | let typeDefinition = valueDefinition["typeDefinition"] as? [String:AnyObject], 39 | let attributes = typeDefinition["attributes"] as? [String] 40 | { 41 | return attributes.contains("required") 42 | } 43 | 44 | return nil 45 | } 46 | 47 | if let sections = dataStructure["sections"] as? [[String:AnyObject]] { 48 | if let section = sections.first { 49 | if (section["class"] as? String) ?? "" == "memberType" { 50 | if let content = section["content"] as? [[String:AnyObject]] { 51 | for property in content { 52 | if ((property["class"] as? String) ?? "") != "property" { 53 | continue 54 | } 55 | 56 | if let content = property["content"] as? [String:AnyObject], 57 | let name = content["name"] as? [String:AnyObject], 58 | let literal = name["literal"] as? String 59 | { 60 | builder.addAttribute(literal, value: "", defaultValue: "", required: isPropertyRequired(content)) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | extension HTTPTransition { 70 | public static func from(resource:Resource, action:Action, URL:String? = nil) -> HTTPTransition { 71 | return HTTPTransition(uri: URL ?? action.uriTemplate ?? resource.uriTemplate) { builder in 72 | builder.method = action.method 73 | 74 | func addParameter(_ parameter:Parameter) { 75 | let value = parameter.example 76 | let defaultValue = parameter.defaultValue 77 | builder.addParameter(parameter.name, value: value, defaultValue: defaultValue, required:parameter.required) 78 | } 79 | 80 | action.parameters.forEach(addParameter) 81 | 82 | let parameters = action.parameters.map { $0.name } 83 | let missingParentParameters = resource.parameters.filter { 84 | !parameters.contains($0.name) 85 | } 86 | missingParentParameters.forEach(addParameter) 87 | 88 | // Let's look at the MSON structure, we currently only look for the members 89 | // of an object since that's only what we can put in a transitions 90 | // attributes in the Representor 91 | let dataStructure = action.content.filter { ($0["element"] as? String) == "dataStructure" }.first 92 | if let dataStructure = dataStructure { 93 | parseAttributes(dataStructure, builder: builder) 94 | } 95 | } 96 | } 97 | } 98 | 99 | /// An extension to Blueprint providing transition conversion 100 | extension Blueprint { 101 | /// Returns a HTTPTransition representation of an action in a resource 102 | public func transition(_ resourceName:String, action actionName:String) -> HTTPTransition? { 103 | let resources = resourceGroups.map { resourceGroup in resourceGroup.resources }.reduce([], +) 104 | let resource = resources.filter { resource in resource.name == resourceName }.first 105 | if let resource = resource { 106 | return resource.transition(actionName) 107 | } 108 | 109 | return nil 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/HTTPDeserialization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPDeserialization.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 23/01/2015. 6 | // Copyright (c) 2015 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func jsonDeserializer(_ closure:@escaping (([String:AnyObject]) -> Representor?)) -> ((_ response:HTTPURLResponse, _ body:Data) -> Representor?) { 12 | return { (response, body) in 13 | let object: AnyObject? = try? JSONSerialization.jsonObject(with: body, options: JSONSerialization.ReadingOptions(rawValue: 0)) as AnyObject 14 | 15 | if let object = object as? [String:AnyObject] { 16 | return closure(object) 17 | } 18 | 19 | return nil 20 | } 21 | } 22 | 23 | public struct HTTPDeserialization { 24 | public typealias Deserializer = (_ response:HTTPURLResponse, _ body:Data) -> (Representor?) 25 | 26 | /// A dictionary storing the registered HTTP deserializer's and their corresponding content type. 27 | public static var deserializers: [String: Deserializer] = [ 28 | "application/hal+json": jsonDeserializer { payload in 29 | return deserializeHAL(payload) 30 | }, 31 | 32 | "application/vnd.siren+json": jsonDeserializer { payload in 33 | return deserializeSiren(payload) 34 | }, 35 | ] 36 | 37 | /// An array of the supported content types in order of preference 38 | public static var preferredContentTypes:[String] = [ 39 | "application/vnd.siren+json", 40 | "application/hal+json", 41 | ] 42 | 43 | /** Deserialize an NSHTTPURLResponse and body into a Representor. 44 | Uses the deserializers defined in HTTPDeserializers. 45 | - parameter response: The response to deserialize 46 | - parameter body: The HTTP Body 47 | :return: representor 48 | */ 49 | public static func deserialize(_ response:HTTPURLResponse, body:Data) -> Representor? { 50 | if let contentType = response.mimeType { 51 | if let deserializer = HTTPDeserialization.deserializers[contentType] { 52 | return deserializer(response, body) 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/HTTPHALAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPHALAdapter.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 08/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func parseHALLinks(_ halLinks: [String: AnyObject]) -> [String: [HTTPTransition]] { 12 | var links: [String: [HTTPTransition]] = [:] 13 | 14 | for (relation, options) in halLinks { 15 | if let options = options as? [String: AnyObject], 16 | let href = options["href"] as? String 17 | { 18 | let transition = HTTPTransition(uri: href) 19 | links[relation] = [transition] 20 | } else if let options = options as? [[String: AnyObject]] { 21 | links[relation] = options.flatMap { 22 | if let href = $0["href"] as? String { 23 | return HTTPTransition(uri: href) 24 | } 25 | 26 | return nil 27 | } 28 | } 29 | } 30 | 31 | return links 32 | } 33 | 34 | 35 | func parseEmbeddedHALs(_ embeddedHALs: [String: AnyObject]) -> [String: [Representor]] { 36 | var representors = [String: [Representor]]() 37 | 38 | func parseEmbedded(_ embedded:[String: AnyObject]) -> Representor { 39 | return deserializeHAL(embedded) 40 | } 41 | 42 | for (name, embedded) in embeddedHALs { 43 | if let embedded = embedded as? [[String: Any]] { 44 | representors[name] = embedded.map(deserializeHAL) 45 | } else if let embedded = embedded as? [String: AnyObject] { 46 | representors[name] = [deserializeHAL(embedded)] 47 | } 48 | } 49 | 50 | return representors 51 | } 52 | 53 | /// A function to deserialize a HAL structure into a HTTP Transition. 54 | public func deserializeHAL(_ hal:[String: Any]) -> Representor { 55 | var hal = hal 56 | 57 | var links: [String: [HTTPTransition]] = [:] 58 | if let halLinks = hal.removeValue(forKey: "_links") as? [String: AnyObject] { 59 | links = parseHALLinks(halLinks) 60 | } 61 | 62 | var representors:[String: [Representor]] = [:] 63 | if let embedded = hal.removeValue(forKey: "_embedded") as? [String: AnyObject] { 64 | representors = parseEmbeddedHALs(embedded) 65 | } 66 | 67 | return Representor(transitions: links, representors: representors, attributes: hal as [String: Any]) 68 | } 69 | 70 | /// A function to serialize a HTTP Representor into a Siren structure 71 | public func serializeHAL(_ representor: Representor) -> [String: Any] { 72 | var representation = representor.attributes 73 | 74 | if !representor.transitions.isEmpty { 75 | var links: [String: Any] = [:] 76 | 77 | for (relation, transitions) in representor.transitions { 78 | if transitions.count == 1 { 79 | links[relation] = ["href": transitions[0].uri] 80 | } else { 81 | links[relation] = transitions.map { 82 | ["href": $0.uri] 83 | } 84 | } 85 | } 86 | 87 | representation["_links"] = links as AnyObject 88 | } 89 | 90 | if !representor.representors.isEmpty { 91 | var embeddedHALs: [String: [[String: Any]]] = [:] 92 | 93 | for (name, representorSet) in representor.representors { 94 | embeddedHALs[name] = representorSet.map(serializeHAL) 95 | } 96 | 97 | representation["_embedded"] = embeddedHALs as AnyObject 98 | } 99 | 100 | return representation 101 | } 102 | -------------------------------------------------------------------------------- /Sources/HTTPSirenAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SirenAdapter.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 08/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private func sirenFieldToAttribute(_ builder: HTTPTransitionBuilder) -> (_ field: [String: Any]) -> Void { 12 | return { field in 13 | if let name = field["name"] as? String { 14 | let title = field["title"] as? String 15 | let value = field["value"] 16 | 17 | builder.addAttribute(name, title: title, value: value, defaultValue: nil) 18 | } 19 | } 20 | } 21 | 22 | private func sirenActionToTransition(_ action: [String: Any]) -> (name: String, transition: HTTPTransition)? { 23 | if let name = action["name"] as? String { 24 | if let href = action["href"] as? String { 25 | let transition = HTTPTransition(uri: href) { builder in 26 | if let method = action["method"] as? String { 27 | builder.method = method 28 | } 29 | 30 | if let contentType = action["type"] as? String { 31 | builder.suggestedContentTypes = [contentType] 32 | } 33 | 34 | if let fields = action["fields"] as? [[String: Any]] { 35 | fields.forEach(sirenFieldToAttribute(builder)) 36 | } 37 | } 38 | 39 | return (name, transition) 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | 46 | private func inputPropertyToSirenField(_ name: String, inputProperty: InputProperty) -> [String: AnyObject] { 47 | var field = [ 48 | "name": name 49 | ] 50 | 51 | if let value = inputProperty.value { 52 | field["value"] = "\(value)" 53 | } 54 | 55 | if let title = inputProperty.title { 56 | field["title"] = title 57 | } 58 | 59 | return field as [String : AnyObject] 60 | } 61 | 62 | private func transitionToSirenAction(_ relation: String, transition: HTTPTransition) -> [String: Any] { 63 | var action: [String: AnyObject] = [ 64 | "href": transition.uri as AnyObject, 65 | "name": relation as AnyObject, 66 | "method": transition.method as AnyObject 67 | ] 68 | 69 | if let contentType = transition.suggestedContentTypes.first { 70 | action["type"] = contentType as AnyObject 71 | } 72 | 73 | if transition.attributes.count > 0 { 74 | action["fields"] = transition.attributes.map(inputPropertyToSirenField) as NSArray 75 | } 76 | 77 | return action 78 | } 79 | 80 | /// A function to deserialize a Siren structure into a HTTP Transition. 81 | public func deserializeSiren(_ siren: [String: Any]) -> Representor { 82 | var representors: [String: [Representor]] = [:] 83 | var transitions: [String: [HTTPTransition]] = [:] 84 | var attributes: [String: Any] = [:] 85 | 86 | if let sirenLinks = siren["links"] as? [[String: Any]] { 87 | for link in sirenLinks { 88 | if let href = link["href"] as? String { 89 | if let relations = link["rel"] as? [String] { 90 | for relation in relations { 91 | transitions[relation] = [HTTPTransition(uri: href)] 92 | } 93 | } 94 | } 95 | } 96 | } 97 | 98 | if let entities = siren["entities"] as? [[String: Any]] { 99 | for entity in entities { 100 | let representor = deserializeSiren(entity) 101 | 102 | if let relations = entity["rel"] as? [String] { 103 | for relation in relations { 104 | if var reps = representors[relation] { 105 | reps.append(representor) 106 | representors[relation] = reps 107 | } else { 108 | representors[relation] = [representor] 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | if let actions = siren["actions"] as? [[String: Any]] { 116 | for action in actions { 117 | if let (name, transition) = sirenActionToTransition(action) { 118 | transitions[name] = [transition] 119 | } 120 | } 121 | } 122 | 123 | if let properties = siren["properties"] as? [String: Any] { 124 | attributes = properties 125 | } 126 | 127 | return Representor(transitions: transitions, representors: representors, attributes: attributes) 128 | } 129 | 130 | /// A function to serialize a HTTP Representor into a Siren structure 131 | public func serializeSiren(_ representor: Representor) -> [String: Any] { 132 | var representation: [String: Any] = [:] 133 | 134 | if !representor.representors.isEmpty { 135 | var entities: [[String: Any]] = [] 136 | 137 | for (relation, representorSet) in representor.representors { 138 | for representor in representorSet { 139 | var representation = serializeSiren(representor) 140 | representation["rel"] = [relation] 141 | entities.append(representation) 142 | } 143 | } 144 | 145 | representation["entities"] = entities as AnyObject 146 | } 147 | 148 | if !representor.attributes.isEmpty { 149 | representation["properties"] = representor.attributes as AnyObject 150 | } 151 | 152 | var links: [[String: Any]] = [] 153 | var actions: [[String: Any]] = [] 154 | 155 | for (relation, transitions) in representor.transitions { 156 | for transition in transitions { 157 | if transition.method == "GET" { 158 | links.append(["rel": [relation], "href": transition.uri]) 159 | } else { 160 | actions.append(transitionToSirenAction(relation, transition: transition)) 161 | } 162 | } 163 | } 164 | 165 | if !links.isEmpty { 166 | representation["links"] = links as AnyObject 167 | } 168 | 169 | if !actions.isEmpty { 170 | representation["actions"] = actions as AnyObject 171 | } 172 | 173 | return representation 174 | } 175 | -------------------------------------------------------------------------------- /Sources/HTTPTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPTransition.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 23/01/2015. 6 | // Copyright (c) 2015 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** An implementation of the Transition protocol for HTTP. */ 12 | public struct HTTPTransition : TransitionType { 13 | public typealias Builder = HTTPTransitionBuilder 14 | 15 | public var uri:String 16 | 17 | /// The HTTP Method that should be used to make the request 18 | public var method:String 19 | 20 | /// The suggested contentType that should be used to make the request 21 | public var suggestedContentTypes:[String] 22 | 23 | public var attributes:InputProperties 24 | public var parameters:InputProperties 25 | 26 | public init(uri:String, attributes:InputProperties? = nil, parameters:InputProperties? = nil) { 27 | self.uri = uri 28 | self.attributes = attributes ?? [:] 29 | self.parameters = parameters ?? [:] 30 | self.method = "GET" 31 | self.suggestedContentTypes = [String]() 32 | } 33 | 34 | public init(uri:String, _ block:((_ builder:Builder) -> ())) { 35 | let builder = Builder() 36 | 37 | block(builder) 38 | 39 | self.uri = uri 40 | self.attributes = builder.attributes 41 | self.parameters = builder.parameters 42 | self.method = builder.method 43 | self.suggestedContentTypes = builder.suggestedContentTypes 44 | } 45 | 46 | public var hashValue:Int { 47 | return uri.hashValue 48 | } 49 | } 50 | 51 | public func ==(lhs:HTTPTransition, rhs:HTTPTransition) -> Bool { 52 | return ( 53 | lhs.uri == rhs.uri && 54 | lhs.attributes == rhs.attributes && 55 | lhs.parameters == rhs.parameters && 56 | lhs.method == rhs.method && 57 | lhs.suggestedContentTypes == rhs.suggestedContentTypes 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /Sources/HTTPTransitionBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPTransitionBuilder.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 23/01/2015. 6 | // Copyright (c) 2015 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An implementation of TransitionBuilder used by the HTTPTransition 12 | public class HTTPTransitionBuilder : TransitionBuilderType { 13 | var attributes = InputProperties() 14 | var parameters = InputProperties() 15 | 16 | /// The suggested contentType that should be used to make the request 17 | public var method = "POST" 18 | /// The suggested contentType that should be used to make the request 19 | public var suggestedContentTypes = [String]() 20 | 21 | init() { 22 | 23 | } 24 | 25 | // MARK: Attributes 26 | 27 | /// Adds an attribute with a value or default value 28 | /// 29 | /// - parameter name: The name of the attribute 30 | /// - parameter title: The human readable title of the attribute 31 | /// - parameter value: The value of the attribute 32 | /// - parameter defaultValue: The default value of the attribute 33 | public func addAttribute(_ name: String, title: String? = nil, value: Any? = nil, defaultValue: Any? = nil, required: Bool? = nil) { 34 | let property = InputProperty(title: title, value: value, defaultValue: defaultValue, required: required) 35 | attributes[name] = property 36 | } 37 | 38 | // MARK: Parameters 39 | 40 | /// Adds a parameter with a value or default value 41 | /// 42 | /// - parameter name: The name of the attribute 43 | /// - parameter value: The value of the attribute 44 | /// - parameter value: The default value of the attribute 45 | public func addParameter(_ name: String, value: Any? = nil, defaultValue: Any? = nil, required: Bool? = nil) { 46 | let property = InputProperty(value: value, defaultValue: defaultValue, required:required) 47 | parameters[name] = property 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2014 Apiary. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Sources/Representor.h: -------------------------------------------------------------------------------- 1 | // 2 | // Representor.h 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 04/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Representor. 12 | FOUNDATION_EXPORT double RepresentorVersionNumber; 13 | 14 | //! Project version string for Representor. 15 | FOUNDATION_EXPORT const unsigned char RepresentorVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | -------------------------------------------------------------------------------- /Sources/Representor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Representor.swift 3 | // Representor 4 | // 5 | // Created by Zdenek Nemec on 8/17/14. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// A representor represents a hypermedia message 13 | public struct Representor : Equatable, Hashable { 14 | public typealias Builder = RepresentorBuilder 15 | 16 | /// The transitions available for the representor 17 | public var transitions: [String: [Transition]] 18 | 19 | /// The separate representors embedded in the current representor. 20 | public var representors: [String: [Representor]] 21 | 22 | public var metadata: [String: String] 23 | 24 | /// The attributes of the representor 25 | public var attributes: [String: Any] 26 | 27 | public init(transitions: [String: [Transition]]? = nil, representors: [String: [Representor]]? = nil, attributes: [String: Any]? = nil, metadata: [String: String]? = nil) { 28 | self.transitions = transitions ?? [:] 29 | self.representors = representors ?? [:] 30 | self.attributes = attributes ?? [:] 31 | self.metadata = metadata ?? [:] 32 | } 33 | 34 | public var hashValue: Int { 35 | return transitions.count + representors.count + metadata.count + attributes.count 36 | } 37 | 38 | /// An extension to Representor to provide a builder interface for creating a Representor. 39 | public init(_ block: ((_ builder:Builder) -> ())) { 40 | // This should belong in an extension, but due to a bug in the symbol 41 | // mangler in the Swift compiler it results in the symbol being incorrectly 42 | // mangled when being used from an extension. 43 | // 44 | // Swift ¯\_(ツ)_/¯ 45 | let builder = Builder() 46 | 47 | block(builder) 48 | 49 | self.transitions = builder.transitions 50 | self.representors = builder.representors 51 | self.attributes = builder.attributes 52 | self.metadata = builder.metadata 53 | } 54 | } 55 | 56 | public func ==(lhs: [String: [Value]], rhs: [String: [Value]]) -> Bool { 57 | // There is a strange Swift bug where you cannot compare a 58 | // dictionary which has an array of objects which conform to Equatable. 59 | // So to be clear, that's comparing the following: 60 | // 61 | // [Equatable: [Equatable]] 62 | // 63 | // If one day this problem is solved in a newer version of Swift, 64 | // this method can be removed and the default == implementation can be used. 65 | // 66 | // Swift ¯\_(ツ)_/¯ 67 | 68 | if lhs.count != rhs.count { 69 | return false 70 | } 71 | 72 | for (key, value) in lhs { 73 | if let rhsValue = rhs[key] { 74 | if (value != rhsValue) { 75 | return false 76 | } 77 | } else { 78 | return false 79 | } 80 | } 81 | 82 | return true 83 | } 84 | 85 | 86 | public func ==(lhs: Representor, rhs: Representor) -> Bool { 87 | return ( 88 | lhs.transitions == rhs.transitions && 89 | lhs.representors == rhs.representors && 90 | lhs.metadata == rhs.metadata && 91 | (lhs.attributes as NSDictionary) == (rhs.attributes as NSDictionary) 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /Sources/RepresentorBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepresentorBuilder.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 05/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A class used to build a representor using a builder pattern 12 | public class RepresentorBuilder { 13 | /// The added transitions 14 | fileprivate(set) public var transitions = [String:[Transition]]() 15 | 16 | /// The added representors 17 | fileprivate(set) public var representors = [String:[Representor]]() 18 | 19 | /// The added attributes 20 | fileprivate(set) public var attributes = [String:AnyObject]() 21 | 22 | /// The added metadata 23 | fileprivate(set) public var metadata = [String:String]() 24 | 25 | /// Adds an attribute 26 | /// 27 | /// - parameter name: The name of the attribute 28 | /// - parameter value: The value of the attribute 29 | public func addAttribute(_ name:String, value:AnyObject) { 30 | attributes[name] = value 31 | } 32 | 33 | // MARK: Representors 34 | 35 | /// Adds an embedded representor 36 | /// 37 | /// - parameter name: The name of the representor 38 | /// - parameter representor: The representor 39 | public func addRepresentor(_ name:String, representor:Representor) { 40 | if var representorSet = representors[name] { 41 | representorSet.append(representor) 42 | representors[name] = representorSet 43 | } else{ 44 | representors[name] = [representor] 45 | } 46 | } 47 | 48 | /// Adds an embedded representor using the builder pattern 49 | /// 50 | /// - parameter name: The name of the representor 51 | /// - parameter builder: A builder to build the representor 52 | public func addRepresentor(_ name:String, block:((_ builder:RepresentorBuilder) -> ())) { 53 | addRepresentor(name, representor:Representor(block)) 54 | } 55 | 56 | // MARK: Transition 57 | 58 | /// Adds a transition 59 | /// 60 | /// - parameter name: The name (or relation) for the transition 61 | /// - parameter transition: The transition 62 | public func addTransition(_ name:String, _ transition:Transition) { 63 | var transitions = self.transitions[name] ?? [] 64 | transitions.append(transition) 65 | self.transitions[name] = transitions 66 | } 67 | 68 | /// Adds a transition with a URI 69 | /// 70 | /// - parameter name: The name (or relation) for the transition 71 | /// - parameter uri: The URI of the transition 72 | public func addTransition(_ name:String, uri:String) { 73 | let transition = Transition(uri: uri, attributes:[:], parameters:[:]) 74 | addTransition(name, transition) 75 | } 76 | 77 | /// Adds a transition with a URI using a builder 78 | /// 79 | /// - parameter name: The name (or relation) for the transition 80 | /// - parameter uri: The URI of the transition 81 | /// - parameter builder: The builder used to create the transition 82 | public func addTransition(_ name:String, uri:String, builder:((Transition.Builder) -> ())) { 83 | let transition = Transition(uri: uri, builder) 84 | addTransition(name, transition) 85 | } 86 | 87 | // MARK: Metadata 88 | 89 | /// Adds an piece of metadata 90 | /// 91 | /// - parameter key: The key for the metadata 92 | /// - parameter value: The value of the key 93 | public func addMetaData(_ key:String, value:String) { 94 | metadata[key] = value 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/Transition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Transition.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 04/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct InputProperty: Equatable { 12 | public let title: String? 13 | 14 | public let defaultValue: Any? 15 | public let value: Any? 16 | public let required: Bool? 17 | 18 | // TODO: Define validators 19 | 20 | public init(title: String? = nil, value: T? = nil, defaultValue: T? = nil, required: Bool? = nil) { 21 | self.title = title 22 | self.value = value 23 | self.defaultValue = defaultValue 24 | self.required = required 25 | } 26 | } 27 | 28 | public func ==(lhs: InputProperty, rhs: InputProperty) -> Bool { 29 | return ( 30 | lhs.title == rhs.title && 31 | lhs.defaultValue as? NSObject == rhs.defaultValue as? NSObject && 32 | lhs.value as? NSObject == rhs.value as? NSObject && 33 | lhs.required == rhs.required 34 | ) 35 | } 36 | 37 | public typealias InputProperties = [String: InputProperty] 38 | 39 | /** Transition instances encapsulate information about interacting with links and forms. */ 40 | public protocol TransitionType: Hashable { 41 | associatedtype Builder = TransitionBuilderType 42 | 43 | init(uri: String, attributes: InputProperties?, parameters: InputProperties?) 44 | init(uri: String, _ block: ((_ builder:Builder) -> ())) 45 | 46 | var uri: String { get } 47 | 48 | var attributes: InputProperties { get } 49 | var parameters: InputProperties { get } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/TransitionBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionBuilder.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 05/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A base protocol used for building transition using a builder pattern 12 | public protocol TransitionBuilderType { 13 | } 14 | -------------------------------------------------------------------------------- /Tests/APIBlueprint/BlueprintTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlueprintTests.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 06/01/2015. 6 | // Copyright (c) 2015 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Representor 12 | 13 | 14 | class ResourceGroupTests : XCTestCase { 15 | var resourceGroup: ResourceGroup! 16 | 17 | override func setUp() { 18 | resourceGroup = ResourceGroup(name: "Group", description: "Description", resources: []) 19 | } 20 | 21 | func testName() { 22 | XCTAssertEqual(resourceGroup.name, "Group") 23 | } 24 | 25 | func testDescription() { 26 | XCTAssertEqual(resourceGroup.description!, "Description") 27 | } 28 | 29 | func testResources() { 30 | XCTAssertEqual(resourceGroup.resources.count, 0) 31 | } 32 | } 33 | 34 | 35 | class ResourceTests : XCTestCase { 36 | var resource:Resource! 37 | 38 | override func setUp() { 39 | resource = Resource(name: "Name", description: "Description", uriTemplate: "uri/{template}", parameters: [], actions: []) 40 | } 41 | 42 | func testName() { 43 | XCTAssertEqual(resource.name, "Name") 44 | } 45 | 46 | func testDescription() { 47 | XCTAssertEqual(resource.description!, "Description") 48 | } 49 | 50 | func testURIemplate() { 51 | XCTAssertEqual(resource.uriTemplate, "uri/{template}") 52 | } 53 | 54 | func testParameters() { 55 | XCTAssertEqual(resource.parameters.count, 0) 56 | } 57 | 58 | func testActions() { 59 | XCTAssertEqual(resource.actions.count, 0) 60 | } 61 | } 62 | 63 | 64 | class ActionTests : XCTestCase { 65 | var action:Action! 66 | 67 | override func setUp() { 68 | action = Action(name: "Name", description: "Description", method: "GET", parameters: [], uriTemplate: "/users/{username}") 69 | } 70 | 71 | func testName() { 72 | XCTAssertEqual(action.name, "Name") 73 | } 74 | 75 | func testDescription() { 76 | XCTAssertEqual(action.description!, "Description") 77 | } 78 | 79 | func testMethod() { 80 | XCTAssertEqual(action.method, "GET") 81 | } 82 | 83 | func testParameters() { 84 | XCTAssertEqual(action.parameters.count, 0) 85 | } 86 | 87 | func testRelation() { 88 | XCTAssertNil(action.relation) 89 | } 90 | 91 | func testURITemplate() { 92 | XCTAssertEqual(action.uriTemplate!, "/users/{username}") 93 | } 94 | } 95 | 96 | 97 | class TransactionExampleTests : XCTestCase { 98 | var example:TransactionExample! 99 | 100 | override func setUp() { 101 | example = TransactionExample(name: "Name", description: "Description") 102 | } 103 | 104 | func testName() { 105 | XCTAssertEqual(example.name, "Name") 106 | } 107 | 108 | func testDescription() { 109 | XCTAssertEqual(example.description!, "Description") 110 | } 111 | } 112 | 113 | 114 | class PayloadTests : XCTestCase { 115 | var payload:Payload! 116 | 117 | override func setUp() { 118 | payload = Payload(name: "Name", description: "Description") 119 | } 120 | 121 | func testName() { 122 | XCTAssertEqual(payload.name, "Name") 123 | } 124 | 125 | func testDescription() { 126 | XCTAssertEqual(payload.description!, "Description") 127 | } 128 | } 129 | 130 | 131 | class ParameterTests : XCTestCase { 132 | var parameter:Parameter! 133 | 134 | override func setUp() { 135 | parameter = Parameter(name: "Name", description: "Description", type: "string", required: true, defaultValue: "hi", example: "hey", values: nil) 136 | } 137 | 138 | func testName() { 139 | XCTAssertEqual(parameter.name, "Name") 140 | } 141 | 142 | func testDescription() { 143 | XCTAssertEqual(parameter.description!, "Description") 144 | } 145 | 146 | func testType() { 147 | XCTAssertEqual(parameter.type!, "string") 148 | } 149 | 150 | func testRequired() { 151 | XCTAssertTrue(parameter.required) 152 | } 153 | 154 | func testDefaultValue() { 155 | XCTAssertEqual(parameter.defaultValue!, "hi") 156 | } 157 | 158 | func testExample() { 159 | XCTAssertEqual(parameter.example!, "hey") 160 | } 161 | 162 | func testValues() { 163 | XCTAssertNil(parameter.values) 164 | } 165 | } 166 | 167 | 168 | class BlueprintTests : XCTestCase { 169 | func testLoadingNonExistantBlueprint() { 170 | let bundle = Bundle(for:object_getClass(self)!) 171 | let blueprint = Blueprint(named:"unknown.json", bundle:bundle) 172 | XCTAssertTrue(blueprint == nil) 173 | } 174 | 175 | func testParsingBlueprintAST() { 176 | let bundle = Bundle(for:object_getClass(self)!) 177 | let blueprint = Blueprint(named:"blueprint.json", bundle:bundle)! 178 | 179 | XCTAssertEqual(blueprint.name, "Polls") 180 | XCTAssertTrue(blueprint.description!.hasPrefix("Polls is a simple API allowing consumers to view polls and vote in them.")) 181 | 182 | let resourceGroups = blueprint.resourceGroups 183 | let resourceGroup = resourceGroups[1] 184 | XCTAssertEqual(resourceGroups.count, 2) 185 | XCTAssertEqual(resourceGroup.name, "Question") 186 | XCTAssertTrue(resourceGroup.description!.hasPrefix("Resources related to questions in the API.")) 187 | 188 | let resource = resourceGroup.resources[0] 189 | XCTAssertEqual(resourceGroup.resources.count, 3) 190 | XCTAssertEqual(resource.name, "Question") 191 | XCTAssertEqual(resource.uriTemplate, "/questions/{question_id}") 192 | 193 | let retrieveAction = resource.actions[0] 194 | XCTAssertEqual(resource.actions.count, 1) 195 | 196 | XCTAssertEqual(retrieveAction.name, "View a Questions Detail") 197 | XCTAssertEqual(retrieveAction.method, "GET") 198 | 199 | XCTAssertEqual(retrieveAction.examples.count, 1) 200 | 201 | let example1 = retrieveAction.examples[0] 202 | XCTAssertEqual(example1.name, "") 203 | XCTAssertEqual(example1.description!, "") 204 | XCTAssertEqual(example1.requests.count, 0) 205 | XCTAssertEqual(example1.responses.count, 1) 206 | 207 | let responsePayload = example1.responses[0] 208 | XCTAssertEqual(responsePayload.name, "200") 209 | XCTAssertEqual(responsePayload.description!, "") 210 | XCTAssertEqual(responsePayload.headers.count, 1) 211 | XCTAssertEqual(responsePayload.headers[0].name, "Content-Type") 212 | XCTAssertEqual(responsePayload.headers[0].value, "application/json") 213 | 214 | // Test the choices are parsed in resources 215 | XCTAssertEqual(resource.content.count, 1) 216 | 217 | // Test the choices are parsed in payloads 218 | let questionsResource = resourceGroup.resources[2] 219 | let questionsAction = questionsResource.actions[0] 220 | let questionsResponseExample = questionsAction.examples[0].responses[0] 221 | XCTAssertEqual(questionsResource.name, "Questions Collection") 222 | XCTAssertEqual(questionsResponseExample.content.count, 1) 223 | } 224 | 225 | func testParsingMetadataFromAST() { 226 | let bundle = Bundle(for:object_getClass(self)!) 227 | let blueprint = Blueprint(named:"blueprint.json", bundle:bundle)! 228 | 229 | let format = blueprint.metadata[0] 230 | let host = blueprint.metadata[1] 231 | 232 | XCTAssertEqual(format.name, "FORMAT") 233 | XCTAssertEqual(format.value, "1A") 234 | 235 | XCTAssertEqual(host.name, "HOST") 236 | XCTAssertEqual(host.value, "https://polls.apiblueprint.org/") 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Tests/APIBlueprint/BlueprintTransitionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlueprintTransitionTests.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 28/01/2015. 6 | // Copyright (c) 2015 Apiary LTD. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Representor 12 | 13 | class BlueprintTransitionTests: XCTestCase { 14 | func testResourceToTransitions() { 15 | let parameter = Parameter(name: "name", description: nil, type: "string", required: true, defaultValue: "default", example: "value", values: nil) 16 | let action = Action(name: "Create", description: nil, method: "PATCH", parameters: [parameter]) 17 | let resource = Resource(name: "Post", description: nil, uriTemplate: "/polls", parameters: [parameter], actions: [action]) 18 | let resourceGroup = ResourceGroup(name: "Name", description: nil, resources: [resource]) 19 | let blueprint = Blueprint(name: "Blueprint", description: nil, resourceGroups: [resourceGroup]) 20 | 21 | let transition = blueprint.transition("Post", action:"Create")! 22 | let transitionParameter = transition.parameters["name"]! 23 | 24 | XCTAssertEqual(transition.uri, "/polls") 25 | XCTAssertEqual(transition.method, "PATCH") 26 | XCTAssertEqual(Array(transition.parameters.keys), ["name"]) 27 | XCTAssertEqual(transitionParameter.value as? String, "value") 28 | XCTAssertEqual(transitionParameter.defaultValue as? String, "default") 29 | } 30 | 31 | func testResourceWithURITemplateToTransitions() { 32 | let parameter = Parameter(name: "name", description: nil, type: "string", required: true, defaultValue: "default", example: "value", values: nil) 33 | let action = Action(name: "Create", description: nil, method: "PATCH", parameters: [parameter], uriTemplate: "/polls/2", relation: "update") 34 | let resource = Resource(name: "Post", description: nil, uriTemplate: "/polls", parameters: [parameter], actions: [action]) 35 | let resourceGroup = ResourceGroup(name: "Name", description: nil, resources: [resource]) 36 | let blueprint = Blueprint(name: "Blueprint", description: nil, resourceGroups: [resourceGroup]) 37 | 38 | let transition = blueprint.transition("Post", action:"Create")! 39 | let transitionParameter = transition.parameters["name"]! 40 | 41 | XCTAssertEqual(transition.uri, "/polls/2") 42 | XCTAssertEqual(transition.method, "PATCH") 43 | XCTAssertEqual(Array(transition.parameters.keys), ["name"]) 44 | XCTAssertEqual(transitionParameter.value as? String, "value") 45 | XCTAssertEqual(transitionParameter.defaultValue as? String, "default") 46 | } 47 | 48 | func testResourceWithRelationNameToTransitions() { 49 | let parameter = Parameter(name: "name", description: nil, type: "string", required: true, defaultValue: "default", example: "value", values: nil) 50 | let action = Action(name: "Create", description: nil, method: "PATCH", parameters: [parameter], uriTemplate: "/polls/2", relation: "update") 51 | let resource = Resource(name: "Post", description: nil, uriTemplate: "/polls", parameters: [parameter], actions: [action]) 52 | let resourceGroup = ResourceGroup(name: "Name", description: nil, resources: [resource]) 53 | let blueprint = Blueprint(name: "Blueprint", description: nil, resourceGroups: [resourceGroup]) 54 | 55 | let transition = blueprint.transition("Post", action:"update")! 56 | let transitionParameter = transition.parameters["name"]! 57 | 58 | XCTAssertEqual(transition.uri, "/polls/2") 59 | XCTAssertEqual(transition.method, "PATCH") 60 | XCTAssertEqual(Array(transition.parameters.keys), ["name"]) 61 | XCTAssertEqual(transitionParameter.value as? String, "value") 62 | XCTAssertEqual(transitionParameter.defaultValue as? String, "default") 63 | } 64 | 65 | func testActionAttributesToTransitions() { 66 | let bundle = Bundle(for:object_getClass(self)!) 67 | let blueprint = Blueprint(named:"blueprint.json", bundle:bundle) 68 | let transition = blueprint!.transition("Questions Collection", action: "create") 69 | 70 | XCTAssertTrue(transition != nil) 71 | XCTAssertTrue(transition!.attributes["question"]!.required!) 72 | XCTAssertFalse(transition!.attributes["choices"]!.required!) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/Adapters/HALAdapterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HALAdapterTests.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 08/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Representor 12 | 13 | class HALAdapterTests: XCTestCase { 14 | func fixture() -> [String: Any] { 15 | return JSONFixture("poll.hal", forObject: self) 16 | } 17 | 18 | func testConversionFromHAL() { 19 | let representor = deserializeHAL(fixture()) as Representor 20 | let representorFixture = PollFixture(self) 21 | 22 | XCTAssertEqual(representor, representorFixture) 23 | } 24 | 25 | func testConversionToHAL() { 26 | let representor = PollFixture(self) 27 | let representation = serializeHAL(representor) 28 | 29 | XCTAssertEqual(representation as NSObject, fixture() as NSObject) 30 | } 31 | 32 | func testLinkConversionFromHALWithMultipleTransitions() { 33 | let representation = [ 34 | "_links": [ 35 | "items": [ 36 | [ "href": "/first_item" ], 37 | [ "href": "/second_item" ], 38 | ] 39 | ] 40 | ] 41 | let representor = deserializeHAL(representation as [String : AnyObject]) 42 | 43 | XCTAssertEqual(representor.transitions["items"]?.count, 2) 44 | } 45 | 46 | func testLinkConversionToHALWithMultipleTransitions() { 47 | let representor = Representor { builder in 48 | builder.addTransition("items", uri: "/first_item") 49 | builder.addTransition("items", uri: "/second_item") 50 | } 51 | let representation = serializeHAL(representor) 52 | 53 | XCTAssertEqual(representation as NSDictionary, [ 54 | "_links": [ 55 | "items": [ 56 | [ "href": "/first_item" ], 57 | [ "href": "/second_item" ], 58 | ] 59 | ] 60 | ]) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/Adapters/NSHTTPURLResponseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSHTTPURLResponseAdapterTests.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 21/12/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Representor 12 | 13 | class NSHTTPURLResponseAdapterTests: XCTestCase { 14 | 15 | func createResponse(_ contentType:String, data:Data) -> (HTTPURLResponse, Data) { 16 | let url = URL(string: "http://test.com/")! 17 | let headers = ["Content-Type": contentType] 18 | let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "1.1", headerFields: headers)! 19 | return (response, data) 20 | } 21 | 22 | func createResponse(_ contentType:String, fixtureNamed:String) -> (HTTPURLResponse, Data) { 23 | return createResponse(contentType, data: fixture(fixtureNamed, forObject: self)) 24 | } 25 | 26 | func JSONHALFixture() -> (HTTPURLResponse, Data) { 27 | return createResponse("application/hal+json", fixtureNamed: "poll.hal") 28 | } 29 | 30 | func JSONSirenFixture() -> (HTTPURLResponse, Data) { 31 | return createResponse("application/vnd.siren+json", fixtureNamed: "poll.siren") 32 | } 33 | 34 | func testPreferredContentTypes() { 35 | let contentTypes = HTTPDeserialization.preferredContentTypes 36 | 37 | XCTAssertEqual(contentTypes, ["application/vnd.siren+json", "application/hal+json"]) 38 | } 39 | 40 | func testDeserializationWithUnknownType() { 41 | let (response, data) = createResponse("application/unknown", data:Data()) 42 | let representor = HTTPDeserialization.deserialize(response, body: data) 43 | 44 | XCTAssertTrue(representor == nil) 45 | } 46 | 47 | func testDeserializationWithHALJSON() { 48 | let (response, body) = JSONHALFixture() 49 | let representor = HTTPDeserialization.deserialize(response, body: body) 50 | 51 | let representorFixture = PollFixture(self) 52 | 53 | XCTAssertEqual(representor, representorFixture) 54 | } 55 | 56 | func testDeserializationWithSirenJSON() { 57 | let (response, body) = JSONSirenFixture() 58 | let representor = HTTPDeserialization.deserialize(response, body: body) 59 | 60 | let representorFixture = PollFixture(self) 61 | 62 | XCTAssertEqual(representor!, representorFixture) 63 | } 64 | 65 | func testCustomDeserializer() { 66 | let representor = Representor { builder in 67 | builder.addAttribute("custom", value: true as AnyObject) 68 | } 69 | 70 | HTTPDeserialization.deserializers["application/custom"] = { (response:HTTPURLResponse, data:Data) in 71 | return representor 72 | } 73 | 74 | let (response, body) = createResponse("application/custom", fixtureNamed: "poll.siren") 75 | let deserializedRepresentor = HTTPDeserialization.deserialize(response, body: body) 76 | 77 | XCTAssertEqual(deserializedRepresentor!, representor) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/Adapters/SirenAdapterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SirenAdapterTests.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 08/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Representor 12 | // FIXME: comparison operators with optionals were removed from the Swift Standard Libary. 13 | // Consider refactoring the code to use the non-optional operators. 14 | fileprivate func < (lhs: T?, rhs: T?) -> Bool { 15 | switch (lhs, rhs) { 16 | case let (l?, r?): 17 | return l < r 18 | case (nil, _?): 19 | return true 20 | default: 21 | return false 22 | } 23 | } 24 | 25 | // FIXME: comparison operators with optionals were removed from the Swift Standard Libary. 26 | // Consider refactoring the code to use the non-optional operators. 27 | fileprivate func > (lhs: T?, rhs: T?) -> Bool { 28 | switch (lhs, rhs) { 29 | case let (l?, r?): 30 | return l > r 31 | default: 32 | return rhs < lhs 33 | } 34 | } 35 | 36 | 37 | class SirenAdapterTests: XCTestCase { 38 | let representation = ["actions": 39 | [ 40 | [ 41 | "name": "register", 42 | "href": "/register/", 43 | "method": "PATCH", 44 | "type": "application/x-www-form-urlencoded", 45 | "fields": [ 46 | [ "name": "username" ], 47 | [ "title": "First Name", "name": "first_name", "value": "John" ], 48 | [ "title": "Last Name", "name": "last_name", "value": "Doe" ], 49 | ] 50 | ], 51 | ] 52 | ] 53 | 54 | let transition = HTTPTransition(uri: "/register/") { builder in 55 | builder.method = "PATCH" 56 | builder.suggestedContentTypes = ["application/x-www-form-urlencoded"] 57 | 58 | builder.addAttribute("username") 59 | builder.addAttribute("first_name", title: "First Name", value: "John", defaultValue: nil) 60 | builder.addAttribute("last_name", title: "Last Name", value: "Doe", defaultValue: nil) 61 | } 62 | 63 | func fixture() -> [String: Any] { 64 | return JSONFixture("poll.siren", forObject: self) 65 | } 66 | 67 | func testConversionFromSiren() { 68 | let representor = deserializeSiren(fixture()) 69 | let representorFixture = PollFixture(self) 70 | 71 | XCTAssertEqual(representor, representorFixture) 72 | } 73 | 74 | func xtestConversionToSiren() { 75 | // Skipped because the order of items in the representation may differ from our fixture 76 | 77 | let representor = PollFixture(self) 78 | let representation = serializeSiren(representor) 79 | 80 | XCTAssertEqual(representation as NSDictionary, fixture() as NSDictionary) 81 | } 82 | 83 | func testConversionFromSirenWithAction() { 84 | let representor = deserializeSiren(representation) 85 | 86 | XCTAssertEqual(representor.transitions["register"]?.first, transition) 87 | } 88 | 89 | func testConversionToSirenWithAction() { 90 | let representor = Representor { builder in 91 | builder.addTransition("register", self.transition) 92 | } 93 | 94 | let actions = serializeSiren(representor)["actions"] as! [[String: AnyObject]] 95 | let action = actions[0] 96 | let fields = action["fields"] as! [[String: String]] 97 | let sortedFields = fields.sorted { (lhs, rhs) in 98 | lhs["name"] > rhs["name"] 99 | } 100 | 101 | XCTAssertEqual(action["name"] as? String, "register") 102 | XCTAssertEqual(action["href"] as? String, "/register/") 103 | XCTAssertEqual(action["method"] as? String, "PATCH") 104 | XCTAssertEqual(action["type"] as? String, "application/x-www-form-urlencoded") 105 | XCTAssertEqual(sortedFields as NSArray, [ 106 | ["name": "username"], 107 | ["title": "Last Name", "name": "last_name", "value": "Doe"], 108 | ["title": "First Name", "name": "first_name", "value": "John"], 109 | ] as NSArray) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Tests/Builder/HTTPTransitionBuilderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPTransitionBuilderTests.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 05/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Representor 12 | 13 | class HTTPTransitionBuilderTests: XCTestCase { 14 | func testTransitionBuildler() { 15 | let transition = HTTPTransition(uri:"/self/") { builder in 16 | 17 | } 18 | 19 | XCTAssertEqual(transition.uri, "/self/") 20 | } 21 | 22 | // MARK: Properties 23 | 24 | func testTransitionBuildlerMethod() { 25 | let transition = HTTPTransition(uri:"/self/") { builder in 26 | builder.method = "PATCH" 27 | } 28 | 29 | XCTAssertEqual(transition.method, "PATCH") 30 | } 31 | 32 | func testTransitionBuildlerContentType() { 33 | let transition = HTTPTransition(uri:"/self/") { builder in 34 | builder.suggestedContentTypes = ["application/json"] 35 | } 36 | 37 | XCTAssertEqual(transition.suggestedContentTypes, ["application/json"]) 38 | } 39 | 40 | // MARK: Attributes 41 | 42 | func testAddAttribute() { 43 | let transition = HTTPTransition(uri:"/self/") { builder in 44 | builder.addAttribute("name") 45 | } 46 | 47 | XCTAssertEqual(transition.uri, "/self/") 48 | XCTAssertTrue(transition.attributes["name"] != nil) 49 | } 50 | 51 | func testAddAttributeWithValue() { 52 | let transition = HTTPTransition(uri:"/self/") { builder in 53 | builder.addAttribute("name", value: "Kyle Fuller", defaultValue: nil) 54 | } 55 | 56 | XCTAssertEqual(transition.uri, "/self/") 57 | XCTAssertTrue(transition.attributes["name"] != nil) 58 | } 59 | 60 | // MARK: Parameters 61 | 62 | func testAddParameter() { 63 | let transition = HTTPTransition(uri:"/self/") { builder in 64 | builder.addParameter("name") 65 | } 66 | 67 | XCTAssertEqual(transition.uri, "/self/") 68 | XCTAssertTrue(transition.parameters["name"] != nil) 69 | } 70 | 71 | func testAddParameterWithValue() { 72 | let transition = HTTPTransition(uri:"/self/") { builder in 73 | builder.addParameter("name", value:"Kyle Fuller", defaultValue:nil) 74 | } 75 | 76 | XCTAssertEqual(transition.uri, "/self/") 77 | XCTAssertTrue(transition.parameters["name"] != nil) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/Builder/RepresentorBuilderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepresentorBuilderTests.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 05/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Representor 12 | 13 | class RepresentorBuilderTests: XCTestCase { 14 | func testAddAttribute() { 15 | let representor = Representor { builder in 16 | builder.addAttribute("name", value: "Kyle" as AnyObject) 17 | } 18 | 19 | XCTAssertEqual(representor.attributes["name"] as? String, "Kyle") 20 | } 21 | 22 | // MARK: Representors 23 | 24 | func testAddRepresentor() { 25 | let representor = Representor { builder in 26 | builder.addRepresentor("parent", representor: Representor()) 27 | } 28 | 29 | XCTAssertTrue(representor.representors["parent"] != nil) 30 | } 31 | 32 | func testAddRepresentorWithBuilder() { 33 | let representor = Representor { builder in 34 | builder.addRepresentor("parent") { builder in 35 | 36 | } 37 | } 38 | 39 | XCTAssertTrue(representor.representors["parent"] != nil) 40 | } 41 | 42 | func testAddingMultipleRepresentorsWithBuilder() { 43 | let representor = Representor { builder in 44 | builder.addRepresentor("parent") { builder in 45 | 46 | } 47 | 48 | builder.addRepresentor("parent") { builder in 49 | 50 | } 51 | } 52 | 53 | let parentRepresentors = representor.representors["parent"]! 54 | XCTAssertEqual(parentRepresentors.count, 2) 55 | } 56 | 57 | // MARK: Transition 58 | 59 | func testAddTransition() { 60 | let transition = HTTPTransition(uri:"/self/", attributes:[:], parameters:[:]) 61 | 62 | let representor = Representor { builder in 63 | builder.addTransition("self", transition) 64 | } 65 | 66 | XCTAssertTrue(representor.transitions["self"] != nil) 67 | } 68 | 69 | func testAddTransitionWithURI() { 70 | let representor = Representor { builder in 71 | builder.addTransition("self", uri:"/self/") 72 | } 73 | 74 | XCTAssertTrue(representor.transitions["self"] != nil) 75 | } 76 | 77 | func testAddTransitionWithBuilder() { 78 | let representor = Representor { builder in 79 | builder.addTransition("self", uri:"/self/") { builder in 80 | 81 | } 82 | } 83 | 84 | XCTAssertTrue(representor.transitions["self"] != nil) 85 | } 86 | 87 | // MARK: Metadata 88 | 89 | func testAddMetaData() { 90 | let representor = Representor { builder in 91 | builder.addMetaData("key", value:"value") 92 | } 93 | 94 | XCTAssertEqual(representor.metadata, ["key": "value"]) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Tests/Fixtures/blueprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "_version": "3.0", 3 | "metadata": [ 4 | { 5 | "name": "FORMAT", 6 | "value": "1A" 7 | }, 8 | { 9 | "name": "HOST", 10 | "value": "https://polls.apiblueprint.org/" 11 | } 12 | ], 13 | "name": "Polls", 14 | "description": "Polls is a simple API allowing consumers to view polls and vote in them.\n\n", 15 | "element": "category", 16 | "resourceGroups": [ 17 | { 18 | "name": "", 19 | "description": "", 20 | "resources": [ 21 | { 22 | "element": "resource", 23 | "name": "Polls API Root", 24 | "description": "This resource does not have any attributes. Instead it offers the initial\nAPI affordances in the form of the links in the JSON body.\n\nIt is recommend to follow the “url” link values,\n[Link](https://tools.ietf.org/html/rfc5988) or Location headers where\napplicable to retrieve resources. Instead of constructing your own URLs,\nto keep your client decoupled from implementation details.\n\n", 25 | "uriTemplate": "/", 26 | "model": {}, 27 | "parameters": [], 28 | "actions": [ 29 | { 30 | "name": "Retrieve the Entry Point", 31 | "description": "", 32 | "method": "GET", 33 | "parameters": [], 34 | "attributes": { 35 | "relation": "", 36 | "uriTemplate": "" 37 | }, 38 | "content": [], 39 | "examples": [ 40 | { 41 | "name": "", 42 | "description": "", 43 | "requests": [], 44 | "responses": [ 45 | { 46 | "name": "200", 47 | "description": "", 48 | "headers": [ 49 | { 50 | "name": "Content-Type", 51 | "value": "application/json" 52 | } 53 | ], 54 | "body": "{\n \"questions_url\": \"/questions\"\n}\n", 55 | "schema": "", 56 | "content": [ 57 | { 58 | "element": "asset", 59 | "attributes": { 60 | "role": "bodyExample" 61 | }, 62 | "content": "{\n \"questions_url\": \"/questions\"\n}\n" 63 | } 64 | ] 65 | } 66 | ] 67 | } 68 | ] 69 | } 70 | ], 71 | "content": [] 72 | } 73 | ] 74 | }, 75 | { 76 | "name": "Question", 77 | "description": "Resources related to questions in the API.\n\n", 78 | "resources": [ 79 | { 80 | "element": "resource", 81 | "name": "Question", 82 | "description": "", 83 | "uriTemplate": "/questions/{question_id}", 84 | "model": {}, 85 | "parameters": [ 86 | { 87 | "name": "question_id", 88 | "description": "ID of the Question in form of an integer", 89 | "type": "number", 90 | "required": true, 91 | "default": "", 92 | "example": "1", 93 | "values": [] 94 | } 95 | ], 96 | "actions": [ 97 | { 98 | "name": "View a Questions Detail", 99 | "description": "", 100 | "method": "GET", 101 | "parameters": [], 102 | "attributes": { 103 | "relation": "", 104 | "uriTemplate": "" 105 | }, 106 | "content": [], 107 | "examples": [ 108 | { 109 | "name": "", 110 | "description": "", 111 | "requests": [], 112 | "responses": [ 113 | { 114 | "name": "200", 115 | "description": "", 116 | "headers": [ 117 | { 118 | "name": "Content-Type", 119 | "value": "application/json" 120 | } 121 | ], 122 | "body": " {\n \"question\": \"Favourite programming language?\",\n \"published_at\": \"2014-11-11T08:40:51.620Z\",\n \"url\": \"/questions/1\",\n \"choices\": [\n {\n \"choice\": \"Swift\",\n \"url\": \"/questions/1/choices/1\",\n \"votes\": 2048\n }, {\n \"choice\": \"Python\",\n \"url\": \"/questions/1/choices/2\",\n \"votes\": 1024\n }, {\n \"choice\": \"Objective-C\",\n \"url\": \"/questions/1/choices/3\",\n \"votes\": 512\n }, {\n \"choice\": \"Ruby\",\n \"url\": \"/questions/1/choices/4\",\n \"votes\": 256\n }\n ]\n }\n", 123 | "schema": "", 124 | "content": [ 125 | { 126 | "element": "asset", 127 | "attributes": { 128 | "role": "bodyExample" 129 | }, 130 | "content": " {\n \"question\": \"Favourite programming language?\",\n \"published_at\": \"2014-11-11T08:40:51.620Z\",\n \"url\": \"/questions/1\",\n \"choices\": [\n {\n \"choice\": \"Swift\",\n \"url\": \"/questions/1/choices/1\",\n \"votes\": 2048\n }, {\n \"choice\": \"Python\",\n \"url\": \"/questions/1/choices/2\",\n \"votes\": 1024\n }, {\n \"choice\": \"Objective-C\",\n \"url\": \"/questions/1/choices/3\",\n \"votes\": 512\n }, {\n \"choice\": \"Ruby\",\n \"url\": \"/questions/1/choices/4\",\n \"votes\": 256\n }\n ]\n }\n" 131 | } 132 | ] 133 | } 134 | ] 135 | } 136 | ] 137 | } 138 | ], 139 | "content": [ 140 | { 141 | "element": "dataStructure", 142 | "name": { 143 | "literal": "Question", 144 | "variable": false 145 | }, 146 | "typeDefinition": { 147 | "typeSpecification": { 148 | "name": null, 149 | "nestedTypes": [] 150 | }, 151 | "attributes": [] 152 | }, 153 | "sections": [ 154 | { 155 | "class": "memberType", 156 | "content": [ 157 | { 158 | "content": { 159 | "name": { 160 | "literal": "question" 161 | }, 162 | "description": "", 163 | "valueDefinition": { 164 | "values": [ 165 | { 166 | "literal": "Favourite programming language?", 167 | "variable": false 168 | } 169 | ], 170 | "typeDefinition": { 171 | "typeSpecification": { 172 | "name": "string", 173 | "nestedTypes": [] 174 | }, 175 | "attributes": [ 176 | "required" 177 | ] 178 | } 179 | }, 180 | "sections": [] 181 | }, 182 | "class": "property" 183 | }, 184 | { 185 | "content": { 186 | "name": { 187 | "literal": "published_at" 188 | }, 189 | "description": "11-11T08:40:51.620Z (string) - An ISO8601 date when the question was published", 190 | "valueDefinition": { 191 | "values": [ 192 | { 193 | "literal": "2014", 194 | "variable": false 195 | } 196 | ], 197 | "typeDefinition": { 198 | "typeSpecification": { 199 | "name": null, 200 | "nestedTypes": [] 201 | }, 202 | "attributes": [] 203 | } 204 | }, 205 | "sections": [] 206 | }, 207 | "class": "property" 208 | }, 209 | { 210 | "content": { 211 | "name": { 212 | "literal": "choices" 213 | }, 214 | "description": "An array of Choice objects", 215 | "valueDefinition": { 216 | "values": [], 217 | "typeDefinition": { 218 | "typeSpecification": { 219 | "name": "array", 220 | "nestedTypes": [ 221 | { 222 | "literal": "Choice", 223 | "variable": false 224 | } 225 | ] 226 | }, 227 | "attributes": [ 228 | "required" 229 | ] 230 | } 231 | }, 232 | "sections": [] 233 | }, 234 | "class": "property" 235 | }, 236 | { 237 | "content": { 238 | "name": { 239 | "literal": "url" 240 | }, 241 | "description": "", 242 | "valueDefinition": { 243 | "values": [ 244 | { 245 | "literal": "/questions/1", 246 | "variable": false 247 | } 248 | ], 249 | "typeDefinition": { 250 | "typeSpecification": { 251 | "name": "string", 252 | "nestedTypes": [] 253 | }, 254 | "attributes": [] 255 | } 256 | }, 257 | "sections": [] 258 | }, 259 | "class": "property" 260 | } 261 | ] 262 | } 263 | ] 264 | } 265 | ] 266 | }, 267 | { 268 | "element": "resource", 269 | "name": "Choice", 270 | "description": "", 271 | "uriTemplate": "/questions/{question_id}/choices/{choice_id}", 272 | "model": {}, 273 | "parameters": [ 274 | { 275 | "name": "question_id", 276 | "description": "ID of the Question in form of an integer", 277 | "type": "number", 278 | "required": true, 279 | "default": "", 280 | "example": "1", 281 | "values": [] 282 | }, 283 | { 284 | "name": "choice_id", 285 | "description": "ID of the Choice in form of an integer", 286 | "type": "number", 287 | "required": true, 288 | "default": "", 289 | "example": "1", 290 | "values": [] 291 | } 292 | ], 293 | "actions": [ 294 | { 295 | "name": "Vote on a Choice", 296 | "description": "This action allows you to vote on a question's choice.\n\n", 297 | "method": "POST", 298 | "parameters": [], 299 | "attributes": { 300 | "relation": "", 301 | "uriTemplate": "" 302 | }, 303 | "content": [], 304 | "examples": [ 305 | { 306 | "name": "", 307 | "description": "", 308 | "requests": [], 309 | "responses": [ 310 | { 311 | "name": "201", 312 | "description": "", 313 | "headers": [ 314 | { 315 | "name": "Location", 316 | "value": "/questions/1" 317 | } 318 | ], 319 | "body": "", 320 | "schema": "", 321 | "content": [] 322 | } 323 | ] 324 | } 325 | ] 326 | } 327 | ], 328 | "content": [ 329 | { 330 | "element": "dataStructure", 331 | "name": { 332 | "literal": "Choice", 333 | "variable": false 334 | }, 335 | "typeDefinition": { 336 | "typeSpecification": { 337 | "name": null, 338 | "nestedTypes": [] 339 | }, 340 | "attributes": [] 341 | }, 342 | "sections": [ 343 | { 344 | "class": "memberType", 345 | "content": [ 346 | { 347 | "content": { 348 | "name": { 349 | "literal": "choice" 350 | }, 351 | "description": "", 352 | "valueDefinition": { 353 | "values": [ 354 | { 355 | "literal": "Swift", 356 | "variable": false 357 | } 358 | ], 359 | "typeDefinition": { 360 | "typeSpecification": { 361 | "name": "string", 362 | "nestedTypes": [] 363 | }, 364 | "attributes": [ 365 | "required" 366 | ] 367 | } 368 | }, 369 | "sections": [] 370 | }, 371 | "class": "property" 372 | }, 373 | { 374 | "content": { 375 | "name": { 376 | "literal": "votes" 377 | }, 378 | "description": "", 379 | "valueDefinition": { 380 | "values": [ 381 | { 382 | "literal": "0", 383 | "variable": false 384 | } 385 | ], 386 | "typeDefinition": { 387 | "typeSpecification": { 388 | "name": "number", 389 | "nestedTypes": [] 390 | }, 391 | "attributes": [ 392 | "required" 393 | ] 394 | } 395 | }, 396 | "sections": [] 397 | }, 398 | "class": "property" 399 | } 400 | ] 401 | } 402 | ] 403 | } 404 | ] 405 | }, 406 | { 407 | "element": "resource", 408 | "name": "Questions Collection", 409 | "description": "", 410 | "uriTemplate": "/questions{?page}", 411 | "model": {}, 412 | "parameters": [ 413 | { 414 | "name": "page", 415 | "description": "The page of questions to return", 416 | "type": "number", 417 | "required": false, 418 | "default": "", 419 | "example": "1", 420 | "values": [] 421 | } 422 | ], 423 | "actions": [ 424 | { 425 | "name": "List All Questions", 426 | "description": "", 427 | "method": "GET", 428 | "parameters": [], 429 | "attributes": { 430 | "relation": "", 431 | "uriTemplate": "" 432 | }, 433 | "content": [], 434 | "examples": [ 435 | { 436 | "name": "", 437 | "description": "", 438 | "requests": [], 439 | "responses": [ 440 | { 441 | "name": "200", 442 | "description": "", 443 | "headers": [ 444 | { 445 | "name": "Content-Type", 446 | "value": "application/json" 447 | }, 448 | { 449 | "name": "Link", 450 | "value": "; rel=\"next\"" 451 | } 452 | ], 453 | "body": "", 454 | "schema": "", 455 | "content": [ 456 | { 457 | "element": "dataStructure", 458 | "name": null, 459 | "typeDefinition": { 460 | "typeSpecification": { 461 | "name": "array", 462 | "nestedTypes": [ 463 | { 464 | "literal": "Question", 465 | "variable": false 466 | } 467 | ] 468 | }, 469 | "attributes": [] 470 | }, 471 | "sections": [] 472 | } 473 | ] 474 | } 475 | ] 476 | } 477 | ] 478 | }, 479 | { 480 | "name": "Create a New Question", 481 | "description": "You may create your own question using this action. It takes a JSON\nobject containing a question and a collection of answers in the\nform of choices.\n\n", 482 | "method": "POST", 483 | "parameters": [], 484 | "attributes": { 485 | "relation": "create", 486 | "uriTemplate": "" 487 | }, 488 | "content": [ 489 | { 490 | "element": "dataStructure", 491 | "name": null, 492 | "typeDefinition": { 493 | "typeSpecification": { 494 | "name": null, 495 | "nestedTypes": [] 496 | }, 497 | "attributes": [] 498 | }, 499 | "sections": [ 500 | { 501 | "class": "memberType", 502 | "content": [ 503 | { 504 | "content": { 505 | "name": { 506 | "literal": "question" 507 | }, 508 | "description": "The question", 509 | "valueDefinition": { 510 | "values": [], 511 | "typeDefinition": { 512 | "typeSpecification": { 513 | "name": "string", 514 | "nestedTypes": [] 515 | }, 516 | "attributes": [ 517 | "required" 518 | ] 519 | } 520 | }, 521 | "sections": [] 522 | }, 523 | "class": "property" 524 | }, 525 | { 526 | "content": { 527 | "name": { 528 | "literal": "choices" 529 | }, 530 | "description": "A collection of choices.", 531 | "valueDefinition": { 532 | "values": [], 533 | "typeDefinition": { 534 | "typeSpecification": { 535 | "name": "array", 536 | "nestedTypes": [ 537 | "string" 538 | ] 539 | }, 540 | "attributes": [] 541 | } 542 | }, 543 | "sections": [] 544 | }, 545 | "class": "property" 546 | } 547 | ] 548 | } 549 | ] 550 | } 551 | ], 552 | "examples": [ 553 | { 554 | "name": "", 555 | "description": "", 556 | "requests": [ 557 | { 558 | "name": "", 559 | "description": "", 560 | "headers": [ 561 | { 562 | "name": "Content-Type", 563 | "value": "application/json" 564 | } 565 | ], 566 | "body": " {\n \"question\": \"Favourite programming language?\",\n \"choices\": [\n \"Swift\",\n \"Python\",\n \"Objective-C\",\n \"Ruby\"\n ]\n }\n", 567 | "schema": "", 568 | "content": [ 569 | { 570 | "element": "asset", 571 | "attributes": { 572 | "role": "bodyExample" 573 | }, 574 | "content": " {\n \"question\": \"Favourite programming language?\",\n \"choices\": [\n \"Swift\",\n \"Python\",\n \"Objective-C\",\n \"Ruby\"\n ]\n }\n" 575 | } 576 | ] 577 | } 578 | ], 579 | "responses": [ 580 | { 581 | "name": "201", 582 | "description": "", 583 | "headers": [ 584 | { 585 | "name": "Content-Type", 586 | "value": "application/json" 587 | }, 588 | { 589 | "name": "Location", 590 | "value": "/questions/2" 591 | } 592 | ], 593 | "body": " {\n \"question\": \"Favourite programming language?\",\n \"published_at\": \"2014-11-11T08:40:51.620Z\",\n \"url\": \"/questions/2\",\n \"choices\": [\n {\n \"choice\": \"Swift\",\n \"url\": \"/questions/2/choices/1\",\n \"votes\": 0\n }, {\n \"choice\": \"Python\",\n \"url\": \"/questions/2/choices/2\",\n \"votes\": 0\n }, {\n \"choice\": \"Objective-C\",\n \"url\": \"/questions/2/choices/3\",\n \"votes\": 0\n }, {\n \"choice\": \"Ruby\",\n \"url\": \"/questions/2/choices/4\",\n \"votes\": 0\n }\n ]\n }\n", 594 | "schema": "", 595 | "content": [ 596 | { 597 | "element": "asset", 598 | "attributes": { 599 | "role": "bodyExample" 600 | }, 601 | "content": " {\n \"question\": \"Favourite programming language?\",\n \"published_at\": \"2014-11-11T08:40:51.620Z\",\n \"url\": \"/questions/2\",\n \"choices\": [\n {\n \"choice\": \"Swift\",\n \"url\": \"/questions/2/choices/1\",\n \"votes\": 0\n }, {\n \"choice\": \"Python\",\n \"url\": \"/questions/2/choices/2\",\n \"votes\": 0\n }, {\n \"choice\": \"Objective-C\",\n \"url\": \"/questions/2/choices/3\",\n \"votes\": 0\n }, {\n \"choice\": \"Ruby\",\n \"url\": \"/questions/2/choices/4\",\n \"votes\": 0\n }\n ]\n }\n" 602 | } 603 | ] 604 | } 605 | ] 606 | } 607 | ] 608 | } 609 | ], 610 | "content": [] 611 | } 612 | ] 613 | } 614 | ], 615 | "content": [ 616 | { 617 | "element": "category", 618 | "content": [ 619 | { 620 | "element": "resource", 621 | "name": "Polls API Root", 622 | "description": "This resource does not have any attributes. Instead it offers the initial\nAPI affordances in the form of the links in the JSON body.\n\nIt is recommend to follow the “url” link values,\n[Link](https://tools.ietf.org/html/rfc5988) or Location headers where\napplicable to retrieve resources. Instead of constructing your own URLs,\nto keep your client decoupled from implementation details.\n\n", 623 | "uriTemplate": "/", 624 | "model": {}, 625 | "parameters": [], 626 | "actions": [ 627 | { 628 | "name": "Retrieve the Entry Point", 629 | "description": "", 630 | "method": "GET", 631 | "parameters": [], 632 | "attributes": { 633 | "relation": "", 634 | "uriTemplate": "" 635 | }, 636 | "content": [], 637 | "examples": [ 638 | { 639 | "name": "", 640 | "description": "", 641 | "requests": [], 642 | "responses": [ 643 | { 644 | "name": "200", 645 | "description": "", 646 | "headers": [ 647 | { 648 | "name": "Content-Type", 649 | "value": "application/json" 650 | } 651 | ], 652 | "body": "{\n \"questions_url\": \"/questions\"\n}\n", 653 | "schema": "", 654 | "content": [ 655 | { 656 | "element": "asset", 657 | "attributes": { 658 | "role": "bodyExample" 659 | }, 660 | "content": "{\n \"questions_url\": \"/questions\"\n}\n" 661 | } 662 | ] 663 | } 664 | ] 665 | } 666 | ] 667 | } 668 | ], 669 | "content": [] 670 | } 671 | ] 672 | }, 673 | { 674 | "element": "category", 675 | "attributes": { 676 | "name": "Question" 677 | }, 678 | "content": [ 679 | { 680 | "element": "copy", 681 | "content": "Resources related to questions in the API.\n\n" 682 | }, 683 | { 684 | "element": "resource", 685 | "name": "Question", 686 | "description": "", 687 | "uriTemplate": "/questions/{question_id}", 688 | "model": {}, 689 | "parameters": [ 690 | { 691 | "name": "question_id", 692 | "description": "ID of the Question in form of an integer", 693 | "type": "number", 694 | "required": true, 695 | "default": "", 696 | "example": "1", 697 | "values": [] 698 | } 699 | ], 700 | "actions": [ 701 | { 702 | "name": "View a Questions Detail", 703 | "description": "", 704 | "method": "GET", 705 | "parameters": [], 706 | "attributes": { 707 | "relation": "", 708 | "uriTemplate": "" 709 | }, 710 | "content": [], 711 | "examples": [ 712 | { 713 | "name": "", 714 | "description": "", 715 | "requests": [], 716 | "responses": [ 717 | { 718 | "name": "200", 719 | "description": "", 720 | "headers": [ 721 | { 722 | "name": "Content-Type", 723 | "value": "application/json" 724 | } 725 | ], 726 | "body": " {\n \"question\": \"Favourite programming language?\",\n \"published_at\": \"2014-11-11T08:40:51.620Z\",\n \"url\": \"/questions/1\",\n \"choices\": [\n {\n \"choice\": \"Swift\",\n \"url\": \"/questions/1/choices/1\",\n \"votes\": 2048\n }, {\n \"choice\": \"Python\",\n \"url\": \"/questions/1/choices/2\",\n \"votes\": 1024\n }, {\n \"choice\": \"Objective-C\",\n \"url\": \"/questions/1/choices/3\",\n \"votes\": 512\n }, {\n \"choice\": \"Ruby\",\n \"url\": \"/questions/1/choices/4\",\n \"votes\": 256\n }\n ]\n }\n", 727 | "schema": "", 728 | "content": [ 729 | { 730 | "element": "asset", 731 | "attributes": { 732 | "role": "bodyExample" 733 | }, 734 | "content": " {\n \"question\": \"Favourite programming language?\",\n \"published_at\": \"2014-11-11T08:40:51.620Z\",\n \"url\": \"/questions/1\",\n \"choices\": [\n {\n \"choice\": \"Swift\",\n \"url\": \"/questions/1/choices/1\",\n \"votes\": 2048\n }, {\n \"choice\": \"Python\",\n \"url\": \"/questions/1/choices/2\",\n \"votes\": 1024\n }, {\n \"choice\": \"Objective-C\",\n \"url\": \"/questions/1/choices/3\",\n \"votes\": 512\n }, {\n \"choice\": \"Ruby\",\n \"url\": \"/questions/1/choices/4\",\n \"votes\": 256\n }\n ]\n }\n" 735 | } 736 | ] 737 | } 738 | ] 739 | } 740 | ] 741 | } 742 | ], 743 | "content": [ 744 | { 745 | "element": "dataStructure", 746 | "name": { 747 | "literal": "Question", 748 | "variable": false 749 | }, 750 | "typeDefinition": { 751 | "typeSpecification": { 752 | "name": null, 753 | "nestedTypes": [] 754 | }, 755 | "attributes": [] 756 | }, 757 | "sections": [ 758 | { 759 | "class": "memberType", 760 | "content": [ 761 | { 762 | "content": { 763 | "name": { 764 | "literal": "question" 765 | }, 766 | "description": "", 767 | "valueDefinition": { 768 | "values": [ 769 | { 770 | "literal": "Favourite programming language?", 771 | "variable": false 772 | } 773 | ], 774 | "typeDefinition": { 775 | "typeSpecification": { 776 | "name": "string", 777 | "nestedTypes": [] 778 | }, 779 | "attributes": [ 780 | "required" 781 | ] 782 | } 783 | }, 784 | "sections": [] 785 | }, 786 | "class": "property" 787 | }, 788 | { 789 | "content": { 790 | "name": { 791 | "literal": "published_at" 792 | }, 793 | "description": "11-11T08:40:51.620Z (string) - An ISO8601 date when the question was published", 794 | "valueDefinition": { 795 | "values": [ 796 | { 797 | "literal": "2014", 798 | "variable": false 799 | } 800 | ], 801 | "typeDefinition": { 802 | "typeSpecification": { 803 | "name": null, 804 | "nestedTypes": [] 805 | }, 806 | "attributes": [] 807 | } 808 | }, 809 | "sections": [] 810 | }, 811 | "class": "property" 812 | }, 813 | { 814 | "content": { 815 | "name": { 816 | "literal": "choices" 817 | }, 818 | "description": "An array of Choice objects", 819 | "valueDefinition": { 820 | "values": [], 821 | "typeDefinition": { 822 | "typeSpecification": { 823 | "name": "array", 824 | "nestedTypes": [ 825 | { 826 | "literal": "Choice", 827 | "variable": false 828 | } 829 | ] 830 | }, 831 | "attributes": [ 832 | "required" 833 | ] 834 | } 835 | }, 836 | "sections": [] 837 | }, 838 | "class": "property" 839 | }, 840 | { 841 | "content": { 842 | "name": { 843 | "literal": "url" 844 | }, 845 | "description": "", 846 | "valueDefinition": { 847 | "values": [ 848 | { 849 | "literal": "/questions/1", 850 | "variable": false 851 | } 852 | ], 853 | "typeDefinition": { 854 | "typeSpecification": { 855 | "name": "string", 856 | "nestedTypes": [] 857 | }, 858 | "attributes": [] 859 | } 860 | }, 861 | "sections": [] 862 | }, 863 | "class": "property" 864 | } 865 | ] 866 | } 867 | ] 868 | } 869 | ] 870 | }, 871 | { 872 | "element": "resource", 873 | "name": "Choice", 874 | "description": "", 875 | "uriTemplate": "/questions/{question_id}/choices/{choice_id}", 876 | "model": {}, 877 | "parameters": [ 878 | { 879 | "name": "question_id", 880 | "description": "ID of the Question in form of an integer", 881 | "type": "number", 882 | "required": true, 883 | "default": "", 884 | "example": "1", 885 | "values": [] 886 | }, 887 | { 888 | "name": "choice_id", 889 | "description": "ID of the Choice in form of an integer", 890 | "type": "number", 891 | "required": true, 892 | "default": "", 893 | "example": "1", 894 | "values": [] 895 | } 896 | ], 897 | "actions": [ 898 | { 899 | "name": "Vote on a Choice", 900 | "description": "This action allows you to vote on a question's choice.\n\n", 901 | "method": "POST", 902 | "parameters": [], 903 | "attributes": { 904 | "relation": "", 905 | "uriTemplate": "" 906 | }, 907 | "content": [], 908 | "examples": [ 909 | { 910 | "name": "", 911 | "description": "", 912 | "requests": [], 913 | "responses": [ 914 | { 915 | "name": "201", 916 | "description": "", 917 | "headers": [ 918 | { 919 | "name": "Location", 920 | "value": "/questions/1" 921 | } 922 | ], 923 | "body": "", 924 | "schema": "", 925 | "content": [] 926 | } 927 | ] 928 | } 929 | ] 930 | } 931 | ], 932 | "content": [ 933 | { 934 | "element": "dataStructure", 935 | "name": { 936 | "literal": "Choice", 937 | "variable": false 938 | }, 939 | "typeDefinition": { 940 | "typeSpecification": { 941 | "name": null, 942 | "nestedTypes": [] 943 | }, 944 | "attributes": [] 945 | }, 946 | "sections": [ 947 | { 948 | "class": "memberType", 949 | "content": [ 950 | { 951 | "content": { 952 | "name": { 953 | "literal": "choice" 954 | }, 955 | "description": "", 956 | "valueDefinition": { 957 | "values": [ 958 | { 959 | "literal": "Swift", 960 | "variable": false 961 | } 962 | ], 963 | "typeDefinition": { 964 | "typeSpecification": { 965 | "name": "string", 966 | "nestedTypes": [] 967 | }, 968 | "attributes": [ 969 | "required" 970 | ] 971 | } 972 | }, 973 | "sections": [] 974 | }, 975 | "class": "property" 976 | }, 977 | { 978 | "content": { 979 | "name": { 980 | "literal": "votes" 981 | }, 982 | "description": "", 983 | "valueDefinition": { 984 | "values": [ 985 | { 986 | "literal": "0", 987 | "variable": false 988 | } 989 | ], 990 | "typeDefinition": { 991 | "typeSpecification": { 992 | "name": "number", 993 | "nestedTypes": [] 994 | }, 995 | "attributes": [ 996 | "required" 997 | ] 998 | } 999 | }, 1000 | "sections": [] 1001 | }, 1002 | "class": "property" 1003 | } 1004 | ] 1005 | } 1006 | ] 1007 | } 1008 | ] 1009 | }, 1010 | { 1011 | "element": "resource", 1012 | "name": "Questions Collection", 1013 | "description": "", 1014 | "uriTemplate": "/questions{?page}", 1015 | "model": {}, 1016 | "parameters": [ 1017 | { 1018 | "name": "page", 1019 | "description": "The page of questions to return", 1020 | "type": "number", 1021 | "required": false, 1022 | "default": "", 1023 | "example": "1", 1024 | "values": [] 1025 | } 1026 | ], 1027 | "actions": [ 1028 | { 1029 | "name": "List All Questions", 1030 | "description": "", 1031 | "method": "GET", 1032 | "parameters": [], 1033 | "attributes": { 1034 | "relation": "", 1035 | "uriTemplate": "" 1036 | }, 1037 | "content": [], 1038 | "examples": [ 1039 | { 1040 | "name": "", 1041 | "description": "", 1042 | "requests": [], 1043 | "responses": [ 1044 | { 1045 | "name": "200", 1046 | "description": "", 1047 | "headers": [ 1048 | { 1049 | "name": "Content-Type", 1050 | "value": "application/json" 1051 | }, 1052 | { 1053 | "name": "Link", 1054 | "value": "; rel=\"next\"" 1055 | } 1056 | ], 1057 | "body": "", 1058 | "schema": "", 1059 | "content": [ 1060 | { 1061 | "element": "dataStructure", 1062 | "name": null, 1063 | "typeDefinition": { 1064 | "typeSpecification": { 1065 | "name": "array", 1066 | "nestedTypes": [ 1067 | { 1068 | "literal": "Question", 1069 | "variable": false 1070 | } 1071 | ] 1072 | }, 1073 | "attributes": [] 1074 | }, 1075 | "sections": [] 1076 | } 1077 | ] 1078 | } 1079 | ] 1080 | } 1081 | ] 1082 | }, 1083 | { 1084 | "name": "Create a New Question", 1085 | "description": "You may create your own question using this action. It takes a JSON\nobject containing a question and a collection of answers in the\nform of choices.\n\n", 1086 | "method": "POST", 1087 | "parameters": [], 1088 | "attributes": { 1089 | "relation": "create", 1090 | "uriTemplate": "" 1091 | }, 1092 | "content": [ 1093 | { 1094 | "element": "dataStructure", 1095 | "name": null, 1096 | "typeDefinition": { 1097 | "typeSpecification": { 1098 | "name": null, 1099 | "nestedTypes": [] 1100 | }, 1101 | "attributes": [] 1102 | }, 1103 | "sections": [ 1104 | { 1105 | "class": "memberType", 1106 | "content": [ 1107 | { 1108 | "content": { 1109 | "name": { 1110 | "literal": "question" 1111 | }, 1112 | "description": "The question", 1113 | "valueDefinition": { 1114 | "values": [], 1115 | "typeDefinition": { 1116 | "typeSpecification": { 1117 | "name": "string", 1118 | "nestedTypes": [] 1119 | }, 1120 | "attributes": [ 1121 | "required" 1122 | ] 1123 | } 1124 | }, 1125 | "sections": [] 1126 | }, 1127 | "class": "property" 1128 | }, 1129 | { 1130 | "content": { 1131 | "name": { 1132 | "literal": "choices" 1133 | }, 1134 | "description": "A collection of choices.", 1135 | "valueDefinition": { 1136 | "values": [], 1137 | "typeDefinition": { 1138 | "typeSpecification": { 1139 | "name": "array", 1140 | "nestedTypes": [ 1141 | "string" 1142 | ] 1143 | }, 1144 | "attributes": [] 1145 | } 1146 | }, 1147 | "sections": [] 1148 | }, 1149 | "class": "property" 1150 | } 1151 | ] 1152 | } 1153 | ] 1154 | } 1155 | ], 1156 | "examples": [ 1157 | { 1158 | "name": "", 1159 | "description": "", 1160 | "requests": [ 1161 | { 1162 | "name": "", 1163 | "description": "", 1164 | "headers": [ 1165 | { 1166 | "name": "Content-Type", 1167 | "value": "application/json" 1168 | } 1169 | ], 1170 | "body": " {\n \"question\": \"Favourite programming language?\",\n \"choices\": [\n \"Swift\",\n \"Python\",\n \"Objective-C\",\n \"Ruby\"\n ]\n }\n", 1171 | "schema": "", 1172 | "content": [ 1173 | { 1174 | "element": "asset", 1175 | "attributes": { 1176 | "role": "bodyExample" 1177 | }, 1178 | "content": " {\n \"question\": \"Favourite programming language?\",\n \"choices\": [\n \"Swift\",\n \"Python\",\n \"Objective-C\",\n \"Ruby\"\n ]\n }\n" 1179 | } 1180 | ] 1181 | } 1182 | ], 1183 | "responses": [ 1184 | { 1185 | "name": "201", 1186 | "description": "", 1187 | "headers": [ 1188 | { 1189 | "name": "Content-Type", 1190 | "value": "application/json" 1191 | }, 1192 | { 1193 | "name": "Location", 1194 | "value": "/questions/2" 1195 | } 1196 | ], 1197 | "body": " {\n \"question\": \"Favourite programming language?\",\n \"published_at\": \"2014-11-11T08:40:51.620Z\",\n \"url\": \"/questions/2\",\n \"choices\": [\n {\n \"choice\": \"Swift\",\n \"url\": \"/questions/2/choices/1\",\n \"votes\": 0\n }, {\n \"choice\": \"Python\",\n \"url\": \"/questions/2/choices/2\",\n \"votes\": 0\n }, {\n \"choice\": \"Objective-C\",\n \"url\": \"/questions/2/choices/3\",\n \"votes\": 0\n }, {\n \"choice\": \"Ruby\",\n \"url\": \"/questions/2/choices/4\",\n \"votes\": 0\n }\n ]\n }\n", 1198 | "schema": "", 1199 | "content": [ 1200 | { 1201 | "element": "asset", 1202 | "attributes": { 1203 | "role": "bodyExample" 1204 | }, 1205 | "content": " {\n \"question\": \"Favourite programming language?\",\n \"published_at\": \"2014-11-11T08:40:51.620Z\",\n \"url\": \"/questions/2\",\n \"choices\": [\n {\n \"choice\": \"Swift\",\n \"url\": \"/questions/2/choices/1\",\n \"votes\": 0\n }, {\n \"choice\": \"Python\",\n \"url\": \"/questions/2/choices/2\",\n \"votes\": 0\n }, {\n \"choice\": \"Objective-C\",\n \"url\": \"/questions/2/choices/3\",\n \"votes\": 0\n }, {\n \"choice\": \"Ruby\",\n \"url\": \"/questions/2/choices/4\",\n \"votes\": 0\n }\n ]\n }\n" 1206 | } 1207 | ] 1208 | } 1209 | ] 1210 | } 1211 | ] 1212 | } 1213 | ], 1214 | "content": [] 1215 | } 1216 | ] 1217 | } 1218 | ] 1219 | } 1220 | -------------------------------------------------------------------------------- /Tests/Fixtures/blueprint.md: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | HOST: https://polls.apiblueprint.org/ 3 | 4 | # Polls 5 | 6 | Polls is a simple API allowing consumers to view polls and vote in them. 7 | 8 | # Polls API Root [/] 9 | 10 | This resource does not have any attributes. Instead it offers the initial 11 | API affordances in the form of the links in the JSON body. 12 | 13 | It is recommend to follow the “url” link values, 14 | [Link](https://tools.ietf.org/html/rfc5988) or Location headers where 15 | applicable to retrieve resources. Instead of constructing your own URLs, 16 | to keep your client decoupled from implementation details. 17 | 18 | ## Retrieve the Entry Point [GET] 19 | 20 | + Response 200 (application/json) 21 | 22 | { 23 | "questions_url": "/questions" 24 | } 25 | 26 | ## Group Question 27 | 28 | Resources related to questions in the API. 29 | 30 | ## Question [/questions/{question_id}] 31 | 32 | + Parameters 33 | + question_id (required, number, `1`) ... ID of the Question in form of an integer 34 | 35 | + Attributes 36 | + question: Favourite programming language? (string, required) 37 | + published_at: 2014-11-11T08:40:51.620Z (string) - An ISO8601 date when the question was published 38 | + choices (array[Choice], required) - An array of Choice objects 39 | + url: /questions/1 (string) 40 | 41 | ### View a Questions Detail [GET] 42 | 43 | + Response 200 (application/json) 44 | 45 | { 46 | "question": "Favourite programming language?", 47 | "published_at": "2014-11-11T08:40:51.620Z", 48 | "url": "/questions/1", 49 | "choices": [ 50 | { 51 | "choice": "Swift", 52 | "url": "/questions/1/choices/1", 53 | "votes": 2048 54 | }, { 55 | "choice": "Python", 56 | "url": "/questions/1/choices/2", 57 | "votes": 1024 58 | }, { 59 | "choice": "Objective-C", 60 | "url": "/questions/1/choices/3", 61 | "votes": 512 62 | }, { 63 | "choice": "Ruby", 64 | "url": "/questions/1/choices/4", 65 | "votes": 256 66 | } 67 | ] 68 | } 69 | 70 | ## Choice [/questions/{question_id}/choices/{choice_id}] 71 | 72 | + Parameters 73 | + question_id (required, number, `1`) ... ID of the Question in form of an integer 74 | + choice_id (required, number, `1`) ... ID of the Choice in form of an integer 75 | 76 | + Attributes 77 | + choice: Swift (string, required) 78 | + votes: 0 (number, required) 79 | 80 | ### Vote on a Choice [POST] 81 | 82 | This action allows you to vote on a question's choice. 83 | 84 | + Response 201 85 | 86 | + Headers 87 | 88 | Location: /questions/1 89 | 90 | ## Questions Collection [/questions{?page}] 91 | 92 | + Parameters 93 | + page (optional, number, `1`) ... The page of questions to return 94 | 95 | ### List All Questions [GET] 96 | 97 | + Response 200 (application/json) 98 | 99 | + Headers 100 | 101 | Link: ; rel="next" 102 | 103 | + Attributes (array[Question]) 104 | 105 | ### Create a New Question [POST] 106 | 107 | You may create your own question using this action. It takes a JSON 108 | object containing a question and a collection of answers in the 109 | form of choices. 110 | 111 | + Relation: create 112 | + Attributes 113 | + question (string, required) - The question 114 | + choices (array[string]) - A collection of choices. 115 | 116 | + Request (application/json) 117 | 118 | { 119 | "question": "Favourite programming language?", 120 | "choices": [ 121 | "Swift", 122 | "Python", 123 | "Objective-C", 124 | "Ruby" 125 | ] 126 | } 127 | 128 | + Response 201 (application/json) 129 | 130 | + Headers 131 | 132 | Location: /questions/2 133 | 134 | + Body 135 | 136 | { 137 | "question": "Favourite programming language?", 138 | "published_at": "2014-11-11T08:40:51.620Z", 139 | "url": "/questions/2", 140 | "choices": [ 141 | { 142 | "choice": "Swift", 143 | "url": "/questions/2/choices/1", 144 | "votes": 0 145 | }, { 146 | "choice": "Python", 147 | "url": "/questions/2/choices/2", 148 | "votes": 0 149 | }, { 150 | "choice": "Objective-C", 151 | "url": "/questions/2/choices/3", 152 | "votes": 0 153 | }, { 154 | "choice": "Ruby", 155 | "url": "/questions/2/choices/4", 156 | "votes": 0 157 | } 158 | ] 159 | } 160 | 161 | -------------------------------------------------------------------------------- /Tests/Fixtures/poll.hal.json: -------------------------------------------------------------------------------- 1 | { 2 | "_links": { 3 | "self": { 4 | "href": "/polls/1/" 5 | }, 6 | "next": { 7 | "href": "/polls/2/" 8 | } 9 | }, 10 | "_embedded": { 11 | "next": [ 12 | { 13 | "_links": { 14 | "self": { 15 | "href": "/polls/2/" 16 | }, 17 | "next": { 18 | "href": "/polls/3/" 19 | }, 20 | "previous": { 21 | "href": "/polls/1/" 22 | } 23 | } 24 | } 25 | ] 26 | }, 27 | "published_at": "2014-11-11T08:40:51.620Z", 28 | "question": "Favourite programming language?", 29 | "choices": [ 30 | { 31 | "answer": "Swift", 32 | "votes": 2048 33 | }, 34 | { 35 | "answer": "Python", 36 | "votes": 1024 37 | }, 38 | { 39 | "answer": "Objective-C", 40 | "votes": 512 41 | }, 42 | { 43 | "answer": "Ruby", 44 | "votes": 256 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /Tests/Fixtures/poll.siren.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": [ 3 | { 4 | "rel": [ "next" ], 5 | "href": "/polls/2/" 6 | }, { 7 | "rel": [ "self" ], 8 | "href": "/polls/1/" 9 | } 10 | ], 11 | "entities": [ 12 | { 13 | "rel": [ "next" ], 14 | "links": [ 15 | { "rel": [ "previous" ], "href": "/polls/1/" }, 16 | { "rel": [ "next" ], "href": "/polls/3/" }, 17 | { "rel": [ "self" ], "href": "/polls/2/" } 18 | ] 19 | } 20 | ], 21 | "properties": { 22 | "question": "Favourite programming language?", 23 | "published_at": "2014-11-11T08:40:51.620Z", 24 | "choices": [ 25 | { 26 | "answer": "Swift", 27 | "votes": 2048 28 | }, 29 | { 30 | "answer": "Python", 31 | "votes": 1024 32 | }, 33 | { 34 | "answer": "Objective-C", 35 | "votes": 512 36 | }, 37 | { 38 | "answer": "Ruby", 39 | "votes": 256 40 | } 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/HTTPTransitionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPTransitionTests.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 04/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Representor 12 | 13 | 14 | class InputPropertyTests : XCTestCase { 15 | var property: InputProperty! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | property = InputProperty(value: "Kyle Fuller", defaultValue: nil) 20 | } 21 | 22 | func testHasValue() { 23 | XCTAssertEqual(property.value as? String, "Kyle Fuller") 24 | } 25 | 26 | func testHasDefaultValue() { 27 | XCTAssertTrue(property.defaultValue == nil) 28 | } 29 | 30 | func testEquality() { 31 | XCTAssertEqual(property, InputProperty(value: "Kyle Fuller", defaultValue: nil)) 32 | XCTAssertNotEqual(property, InputProperty(value: "Kyle Fuller", defaultValue: "Name")) 33 | } 34 | } 35 | 36 | 37 | class HTTPTransitionTests : XCTestCase { 38 | var transition:HTTPTransition! 39 | 40 | override func setUp() { 41 | super.setUp() 42 | transition = HTTPTransition(uri:"/self/") 43 | } 44 | 45 | func testHasURI() { 46 | XCTAssertEqual(transition.uri, "/self/") 47 | } 48 | 49 | func testHasAttributes() { 50 | XCTAssertEqual(transition.attributes.count, 0) 51 | } 52 | 53 | func testHasParameters() { 54 | XCTAssertEqual(transition.parameters.count, 0) 55 | } 56 | 57 | func testHasMethod() { 58 | XCTAssertEqual(transition.method, "GET") 59 | } 60 | 61 | func testHasContentType() { 62 | XCTAssertEqual(transition.suggestedContentTypes, []) 63 | } 64 | 65 | func testEquality() { 66 | XCTAssertEqual(transition, HTTPTransition(uri:"/self/")) 67 | XCTAssertNotEqual(transition, HTTPTransition(uri:"/next/")) 68 | } 69 | 70 | func testHashValue() { 71 | let transition1 = HTTPTransition(uri:"/self/") 72 | let transition2 = HTTPTransition(uri:"/self/") 73 | XCTAssertEqual(transition1.hashValue, transition2.hashValue) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/RepresentorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepresentorTests.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 04/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Representor 12 | 13 | class RepresentorTests: XCTestCase { 14 | var transition:HTTPTransition! 15 | var embeddedRepresentor:Representor! 16 | var representor:Representor! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | transition = HTTPTransition(uri:"/self/") 21 | embeddedRepresentor = Representor() 22 | representor = Representor(transitions:["self": [transition]], representors:["embedded": [embeddedRepresentor]], attributes:["name": "Kyle" as AnyObject], metadata: ["key": "value"]) 23 | } 24 | 25 | func testHasTransitions() { 26 | XCTAssertTrue(representor.transitions["self"] != nil) 27 | } 28 | 29 | func testHasRepresentors() { 30 | XCTAssertTrue(representor.representors["embedded"] != nil) 31 | } 32 | 33 | func testHasAttributes() { 34 | XCTAssertEqual(representor.attributes["name"] as? String, "Kyle") 35 | } 36 | 37 | func testHasMetaData() { 38 | XCTAssertEqual(representor.metadata, ["key": "value"]) 39 | } 40 | 41 | func testEquality() { 42 | XCTAssertEqual(representor, Representor(transitions:["self": [transition]], representors:["embedded": [embeddedRepresentor]], attributes:["name": "Kyle" as AnyObject], metadata:["key": "value"])) 43 | XCTAssertNotEqual(representor, Representor()) 44 | } 45 | 46 | func testHashValue() { 47 | let representor1 = Representor() 48 | let representor2 = Representor() 49 | XCTAssertEqual(representor1.hashValue, representor2.hashValue) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 18/11/2014. 6 | // Copyright (c) 2014 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Representor 11 | 12 | func fixture(_ named:String, forObject:AnyObject) -> Data { 13 | let bundle = Bundle(for:object_getClass(forObject)!) 14 | let path = bundle.url(forResource: named, withExtension: "json")! 15 | let data = try! Data(contentsOf: path) 16 | return data 17 | } 18 | 19 | func JSONFixture(_ named: String, forObject: AnyObject) -> [String: Any] { 20 | let data = fixture(named, forObject: forObject) 21 | let object = try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions(rawValue: 0)) 22 | return object as! [String: Any] 23 | } 24 | 25 | func PollFixtureAttributes(_ forObject: AnyObject) -> [String: Any] { 26 | return JSONFixture("poll.attributes", forObject: forObject) 27 | } 28 | 29 | func PollFixture(_ forObject:AnyObject) -> Representor { 30 | return Representor { builder in 31 | builder.addTransition("self", uri:"/polls/1/") 32 | builder.addTransition("next", uri:"/polls/2/") 33 | 34 | builder.addAttribute("question", value: "Favourite programming language?" as AnyObject) 35 | builder.addAttribute("published_at", value: "2014-11-11T08:40:51.620Z" as AnyObject) 36 | builder.addAttribute("choices", value: [ 37 | [ 38 | "answer": "Swift", 39 | "votes": 2048, 40 | ], [ 41 | "answer": "Python", 42 | "votes": 1024, 43 | ], [ 44 | "answer": "Objective-C", 45 | "votes": 512, 46 | ], [ 47 | "answer": "Ruby", 48 | "votes": 256, 49 | ], 50 | ] as AnyObject) 51 | 52 | builder.addRepresentor("next") { builder in 53 | builder.addTransition("self", uri:"/polls/2/") 54 | builder.addTransition("next", uri:"/polls/3/") 55 | builder.addTransition("previous", uri:"/polls/1/") 56 | } 57 | } 58 | } 59 | --------------------------------------------------------------------------------