Repository: cloudinary/cloudinary_ios Branch: master Commit: ea94096c41ad Files: 444 Total size: 3.2 MB Directory structure: gitextract_f1wonv0o/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── pull_request_template.md ├── .gitignore ├── .gitmodules ├── .swiftpm/ │ └── xcode/ │ └── package.xcworkspace/ │ └── contents.xcworkspacedata ├── .travis.yml ├── CHANGELOG.md ├── Cloudinary/ │ ├── Assets/ │ │ └── .gitkeep │ └── Classes/ │ ├── Core/ │ │ ├── BaseNetwork/ │ │ │ ├── CLDNConvertible.swift │ │ │ ├── CLDNError.swift │ │ │ ├── CLDNMultipartFormData.swift │ │ │ ├── CLDNParameterEncoding.swift │ │ │ ├── CLDNRequest.swift │ │ │ ├── CLDNResponse.swift │ │ │ ├── CLDNResponseSerialization.swift │ │ │ ├── CLDNResult.swift │ │ │ ├── CLDNSessionDelegate.swift │ │ │ ├── CLDNSessionManager.swift │ │ │ ├── CLDNTaskDelegate.swift │ │ │ ├── CLDNTimeline.swift │ │ │ ├── CLDNValidation.swift │ │ │ └── DispatchQueue+Cloudinary.swift │ │ ├── CLDAnalytics.swift │ │ ├── CLDCloudinary.swift │ │ ├── CLDCompatibility.swift │ │ ├── CLDConfiguration.swift │ │ ├── CLDEagerTransformation.swift │ │ ├── CLDResponsiveParams.swift │ │ ├── Cloudinary.h │ │ ├── Features/ │ │ │ ├── CacheSystem/ │ │ │ │ └── Enum/ │ │ │ │ └── HTTPStatusCode.swift │ │ │ ├── Downloader/ │ │ │ │ └── CLDDownloader.swift │ │ │ ├── Helpers/ │ │ │ │ ├── CLDBaseNetworkObject.swift │ │ │ │ ├── CLDConditionExpression.swift │ │ │ │ ├── CLDDefinitions.swift │ │ │ │ ├── CLDExpression.swift │ │ │ │ ├── CLDOperators.swift │ │ │ │ ├── CLDTransformation.swift │ │ │ │ ├── CLDVariable.swift │ │ │ │ ├── Layers/ │ │ │ │ │ ├── CLDFetchLayer.swift │ │ │ │ │ ├── CLDLayer.swift │ │ │ │ │ ├── CLDSubtitlesLayer.swift │ │ │ │ │ └── CLDTextLayer.swift │ │ │ │ ├── RequestParams/ │ │ │ │ │ ├── CLDRequestParams.swift │ │ │ │ │ └── Helpers/ │ │ │ │ │ └── CLDRequestParamsHelpers.swift │ │ │ │ ├── Requests/ │ │ │ │ │ └── CLDRequest.swift │ │ │ │ └── Results/ │ │ │ │ ├── CLDBaseResult.swift │ │ │ │ └── Helpers/ │ │ │ │ ├── AccessibilityAnalysis/ │ │ │ │ │ ├── CLDAccessibilityAnalysisResult.swift │ │ │ │ │ └── CLDColorblindAccessibilityAnalysisResult.swift │ │ │ │ ├── CLDBoundingBox.swift │ │ │ │ ├── CLDDetection.swift │ │ │ │ ├── CLDFace.swift │ │ │ │ ├── CLDInfo.swift │ │ │ │ ├── CLDOcr/ │ │ │ │ │ ├── CLDAdvOcrResult.swift │ │ │ │ │ ├── CLDOcrBlockResult.swift │ │ │ │ │ ├── CLDOcrBoundindBlockResult.swift │ │ │ │ │ ├── CLDOcrDataResult.swift │ │ │ │ │ ├── CLDOcrDetectedLanguagesResult.swift │ │ │ │ │ ├── CLDOcrFullTextAnnotationResult.swift │ │ │ │ │ ├── CLDOcrPageResult.swift │ │ │ │ │ ├── CLDOcrParagraphResult.swift │ │ │ │ │ ├── CLDOcrPropertyResult.swift │ │ │ │ │ ├── CLDOcrResult.swift │ │ │ │ │ ├── CLDOcrSymbolResult.swift │ │ │ │ │ ├── CLDOcrTextAnnotationResult.swift │ │ │ │ │ └── CLDOcrWordResult.swift │ │ │ │ ├── CLDQualityAnalysis/ │ │ │ │ │ └── CLDQualityAnalysis.swift │ │ │ │ ├── CLDRekognitionFace.swift │ │ │ │ └── CommonResultKeys.swift │ │ │ ├── ManagementApi/ │ │ │ │ ├── CLDManagementApi.swift │ │ │ │ ├── Requests/ │ │ │ │ │ ├── CLDDeleteRequest.swift │ │ │ │ │ ├── CLDExplicitRequest.swift │ │ │ │ │ ├── CLDExplodeRequest.swift │ │ │ │ │ ├── CLDMultiRequest.swift │ │ │ │ │ ├── CLDRenameRequest.swift │ │ │ │ │ ├── CLDSpriteRequest.swift │ │ │ │ │ ├── CLDTagRequest.swift │ │ │ │ │ └── CLDTextRequest.swift │ │ │ │ ├── RequestsParams/ │ │ │ │ │ ├── CLDDeleteByTokenRequestParams.swift │ │ │ │ │ ├── CLDDestroyRequestParams.swift │ │ │ │ │ ├── CLDExplicitRequestParams.swift │ │ │ │ │ ├── CLDExplodeRequestParams.swift │ │ │ │ │ ├── CLDMultiRequestParams.swift │ │ │ │ │ ├── CLDRenameRequestParams.swift │ │ │ │ │ ├── CLDSpriteRequestParams.swift │ │ │ │ │ ├── CLDTagsRequestParams.swift │ │ │ │ │ └── CLDTextRequestParams.swift │ │ │ │ └── Results/ │ │ │ │ ├── CLDDeleteResult.swift │ │ │ │ ├── CLDExplicitResult.swift │ │ │ │ ├── CLDExplodeResult.swift │ │ │ │ ├── CLDMultiResult.swift │ │ │ │ ├── CLDRenameResult.swift │ │ │ │ ├── CLDSpriteResult.swift │ │ │ │ ├── CLDTagResult.swift │ │ │ │ └── CLDTextResult.swift │ │ │ ├── UploadWidget/ │ │ │ │ ├── CLDUploaderWidget.swift │ │ │ │ ├── CropView/ │ │ │ │ │ ├── CLDCropOverlayView.swift │ │ │ │ │ ├── CLDCropScrollView.swift │ │ │ │ │ ├── CLDCropScrollViewController.swift │ │ │ │ │ ├── CLDCropView.swift │ │ │ │ │ ├── CLDCropViewCalculator.swift │ │ │ │ │ └── CLDCropViewUIManager.swift │ │ │ │ ├── WidgetConfiguration/ │ │ │ │ │ └── CLDWidgetConfiguration.swift │ │ │ │ ├── WidgetElements/ │ │ │ │ │ ├── CLDWidgetAssetContainer.swift │ │ │ │ │ └── CLDWidgetPreviewCollectionCell.swift │ │ │ │ ├── WidgetImages/ │ │ │ │ │ ├── BackIconInstructions.swift │ │ │ │ │ ├── CropIconInstructions.swift │ │ │ │ │ ├── CropRotateIconInstructions.swift │ │ │ │ │ ├── DoneIconInstructions.swift │ │ │ │ │ ├── RatioLockedIconInstructions.swift │ │ │ │ │ ├── RatioOpenedIconInstructions.swift │ │ │ │ │ └── RotateIconInstructions.swift │ │ │ │ ├── WidgetVideo/ │ │ │ │ │ ├── CLDDisplayLinkObserver.swift │ │ │ │ │ ├── CLDVideoControlsState.swift │ │ │ │ │ ├── CLDVideoControlsView.swift │ │ │ │ │ ├── CLDVideoHiddenAndPausedState.swift │ │ │ │ │ ├── CLDVideoHiddenAndPlayingState.swift │ │ │ │ │ ├── CLDVideoPlayerView.swift │ │ │ │ │ ├── CLDVideoShownAndPausedState.swift │ │ │ │ │ ├── CLDVideoShownAndPlayingState.swift │ │ │ │ │ └── CLDVideoView.swift │ │ │ │ └── WidgetViewControllers/ │ │ │ │ ├── CLDWidgetEditViewController.swift │ │ │ │ ├── CLDWidgetPreviewViewController.swift │ │ │ │ └── CLDWidgetViewController.swift │ │ │ ├── Uploader/ │ │ │ │ ├── CLDUploader.swift │ │ │ │ ├── Preupload/ │ │ │ │ │ └── CLDPreprocessChain.swift │ │ │ │ ├── RequestParams/ │ │ │ │ │ └── CLDUploadRequestParams.swift │ │ │ │ ├── Requests/ │ │ │ │ │ ├── CLDDefaultUploadRequest.swift │ │ │ │ │ ├── CLDUploadRequest.swift │ │ │ │ │ └── CLDUploadRequestWrapper.swift │ │ │ │ └── Results/ │ │ │ │ └── CLDUploadResult.swift │ │ │ └── Url/ │ │ │ └── CLDUrl.swift │ │ ├── Network/ │ │ │ ├── Adapter/ │ │ │ │ └── CLDNetworkAdapter.swift │ │ │ ├── CLDDefaultNetworkAdapter.swift │ │ │ ├── CLDNetworkCoordinator.swift │ │ │ ├── NetworkRequest/ │ │ │ │ ├── CLDAsyncNetworkUploadRequest.swift │ │ │ │ ├── CLDErrorRequest.swift │ │ │ │ ├── CLDGenericNetworkRequest.swift │ │ │ │ ├── CLDNetworkDataRequestImpl.swift │ │ │ │ ├── CLDNetworkDownloadRequest.swift │ │ │ │ └── CLDNetworkUploadRequest.swift │ │ │ └── PrivacyInfo.xcprivacy │ │ └── Utils/ │ │ ├── CLDBuildParamsUtils.swift │ │ ├── CLDCryptoUtils.swift │ │ ├── CLDDictionaryUtils.swift │ │ ├── CLDError.swift │ │ ├── CLDFileUtils.swift │ │ ├── CLDImageGenerator/ │ │ │ └── CLDImageGenerator.swift │ │ ├── CLDImageUtils.swift │ │ ├── CLDJsonUtils.swift │ │ ├── CLDLogManager.swift │ │ └── CLDStringUtils.swift │ └── ios/ │ ├── Extensions/ │ │ ├── CLDTransformation+Ios.swift │ │ ├── ExtensionCLDDownloader.swift │ │ ├── UIButton+Cloudinary.swift │ │ ├── UIImageView+Cloudinary.swift │ │ └── UIView+Cloudinary.swift │ ├── NetworkRequest/ │ │ ├── CLDFetchAssetRequestImpl.swift │ │ └── CLDFetchImageRequestImpl.swift │ ├── UIViews/ │ │ ├── CLDResponsiveViewHelper.swift │ │ └── CLDUIImageView.swift │ ├── Uploader/ │ │ ├── CLDImagePreprocessChain.swift │ │ ├── CLDPreprocessHelpers.swift │ │ ├── CLDVideoPreprocessChain.swift │ │ ├── CLDVideoPreprocessHelpers.swift │ │ └── CLDVideoTranscode.swift │ └── Video/ │ ├── Analytics/ │ │ ├── VideoAnalytics.swift │ │ └── VideoEventsManager.swift │ └── CLDVideoPlayer.swift ├── Cloudinary.podspec ├── Cloudinary.xcodeproj/ │ ├── Cloudinary_Info.plist │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata/ │ └── xcschemes/ │ └── Cloudinary-Package.xcscheme ├── Example/ │ ├── Cloudinary/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ ├── Contents.json │ │ │ │ └── app_icon 1.heic │ │ │ ├── Contents.json │ │ │ ├── Delivery/ │ │ │ │ ├── Contents.json │ │ │ │ ├── Inner/ │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── ski.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── sofa.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Video/ │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── instagram.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── tiktok.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── youtube.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── background_normalizing.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── color_alternation.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── delivery-city.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── localization_branding.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── smart_cropping.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Upload/ │ │ │ │ ├── Contents.json │ │ │ │ ├── question_mark.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── upload_missing.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Video/ │ │ │ │ ├── Contents.json │ │ │ │ └── TikTok/ │ │ │ │ ├── Contents.json │ │ │ │ ├── tiktok_bar_icon.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── tiktok_comments.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── tiktok_discover.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── tiktok_home.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── tiktok_inbox.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── tiktok_like.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── tiktok_more.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── tiktok_music.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── tiktok_note.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── tiktok_share.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── tiktok_social_icon.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Widgets/ │ │ │ │ ├── Contents.json │ │ │ │ └── house.imageset/ │ │ │ │ ├── Contents.json │ │ │ │ └── Frame_871_ao5o4r_2_viaeyi.heic │ │ │ ├── back_arrow.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── car-speed-limiter.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── cloudinary_logo.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── info_icon.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── splash.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── splash_without_logo.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── upload.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── video.imageset/ │ │ │ │ └── Contents.json │ │ │ └── widgets.imageset/ │ │ │ └── Contents.json │ │ ├── CldModel.xcdatamodeld/ │ │ │ └── CldModel.xcdatamodel/ │ │ │ └── contents │ │ ├── Cloudinary_Example-Bridging-Header.h │ │ ├── Colors.xcassets/ │ │ │ ├── Contents.json │ │ │ ├── gradient-first.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── gradient-second.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── primary.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── secondary.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── size_green.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── size_red.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── surface.colorset/ │ │ │ │ └── Contents.json │ │ │ └── text_not_selected.colorset/ │ │ │ └── Contents.json │ │ ├── Controllers/ │ │ │ ├── Base/ │ │ │ │ └── BaseViewController.swift │ │ │ ├── Inner Views/ │ │ │ │ ├── Transform/ │ │ │ │ │ ├── DeliveryViewController.swift │ │ │ │ │ ├── Optimization/ │ │ │ │ │ │ └── OptimizationViewController.swift │ │ │ │ │ ├── Transformation/ │ │ │ │ │ │ ├── DeliveryTransformCollectionController.swift │ │ │ │ │ │ ├── Reveal Image/ │ │ │ │ │ │ │ └── RevealImageController.swift │ │ │ │ │ │ ├── Smart Cropping/ │ │ │ │ │ │ │ └── SmartCroppingController.swift │ │ │ │ │ │ ├── TransformCollectionCell.swift │ │ │ │ │ │ ├── TransformCollectionController.swift │ │ │ │ │ │ ├── TransformViewController.swift │ │ │ │ │ │ └── TransformationCell.swift │ │ │ │ │ └── Use Cases/ │ │ │ │ │ ├── DeliveryUseCasesCollectionController.swift │ │ │ │ │ ├── Screens/ │ │ │ │ │ │ ├── ConditionalProductViewController.swift │ │ │ │ │ │ ├── IntegrateAIViewController.swift │ │ │ │ │ │ └── NormalizingProductAssetsViewController.swift │ │ │ │ │ ├── UseCaseCell.swift │ │ │ │ │ ├── UseCaseCollectionCell.swift │ │ │ │ │ ├── UseCasesCollectionController.swift │ │ │ │ │ └── UseCasesViewController.swift │ │ │ │ ├── Upload/ │ │ │ │ │ ├── InnerUploadFrame.swift │ │ │ │ │ ├── Single Upload/ │ │ │ │ │ │ ├── SingleUploadCell.swift │ │ │ │ │ │ ├── SingleUploadCollectionController.swift │ │ │ │ │ │ ├── SingleUploadCollectionLayout.swift │ │ │ │ │ │ ├── SingleUploadPreview.swift │ │ │ │ │ │ └── SingleUploadViewController.swift │ │ │ │ │ ├── Upload Does Not Exist/ │ │ │ │ │ │ └── UploadDoesNotExistController.swift │ │ │ │ │ ├── UploadChoiceController.swift │ │ │ │ │ ├── UploadNoCloudController.swift │ │ │ │ │ └── UploadViewController.swift │ │ │ │ └── Video/ │ │ │ │ ├── Video Feed/ │ │ │ │ │ └── VideoFeedCell.swift │ │ │ │ └── VideoFeedCollectionController.swift │ │ │ ├── MainViewController.swift │ │ │ ├── SplashViewController.swift │ │ │ ├── Video Feed/ │ │ │ │ ├── Controllers/ │ │ │ │ │ ├── MainPageController.swift │ │ │ │ │ ├── VideoFeedContainerController.swift │ │ │ │ │ ├── VideoFeedController.swift │ │ │ │ │ ├── VideoFeedViewController.swift │ │ │ │ │ ├── VideoSocialOverlaysController.swift │ │ │ │ │ └── VideoViewController.swift │ │ │ │ ├── Helpers/ │ │ │ │ │ └── VideoHelper.swift │ │ │ │ └── Resources/ │ │ │ │ └── video_links.plist │ │ │ └── Widgets/ │ │ │ ├── ImageWidgetViewController.swift │ │ │ ├── UploadWidgetViewController.swift │ │ │ └── WidgetsViewController.swift │ │ ├── Core Data/ │ │ │ ├── AssetItems+CoreDataClass.swift │ │ │ ├── AssetItems+CoreDataProperties.swift │ │ │ ├── AssetModel.swift │ │ │ ├── AssetModel.xcdatamodeld/ │ │ │ │ └── Model.xcdatamodel/ │ │ │ │ └── contents │ │ │ └── CoreDataHelper.swift │ │ ├── Custom Views/ │ │ │ ├── GradientView.swift │ │ │ ├── RevealImageView.swift │ │ │ ├── ToolBar/ │ │ │ │ ├── Item/ │ │ │ │ │ ├── ToolbarItem.swift │ │ │ │ │ └── ToolbarItem.xib │ │ │ │ ├── Toolbar.swift │ │ │ │ └── Toolbar.xib │ │ │ └── Upload Loading View/ │ │ │ ├── UploadLoadingView.swift │ │ │ └── UploadLoadingView.xib │ │ ├── Extensions/ │ │ │ └── Double+Extension.swift │ │ ├── GoogleService-Info.plist │ │ ├── Helpers/ │ │ │ ├── AnimationHelper.swift │ │ │ ├── Events/ │ │ │ │ ├── EventObject.swift │ │ │ │ ├── EventsHandler.swift │ │ │ │ ├── EventsProvider.swift │ │ │ │ └── FirebaseEventsHandler.swift │ │ │ └── ImageHelper.swift │ │ ├── Info.plist │ │ ├── SceneDelegate.swift │ │ ├── Utils/ │ │ │ ├── CloudinaryHelper.swift │ │ │ └── FileUtils.swift │ │ └── Views/ │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Inner Views/ │ │ │ ├── Base/ │ │ │ │ └── Base.storyboard │ │ │ ├── Transform/ │ │ │ │ ├── Delivery.storyboard │ │ │ │ ├── Optimization.storyboard │ │ │ │ ├── Use Cases/ │ │ │ │ │ ├── ConditionalProduct.storyboard │ │ │ │ │ ├── IntegrateAI.storyboard │ │ │ │ │ └── NormalizingProductAssets.storyboard │ │ │ │ └── UseCases.storyboard │ │ │ ├── Upload/ │ │ │ │ ├── InnerUploadFrame.xib │ │ │ │ ├── Upload.storyboard │ │ │ │ └── UploadNoCloud.storyboard │ │ │ ├── Video/ │ │ │ │ ├── Social/ │ │ │ │ │ └── VideoSocialOverlays.storyboard │ │ │ │ ├── Video.storyboard │ │ │ │ └── VideoFeed.storyboard │ │ │ └── Widgets/ │ │ │ ├── ImageWidget.storyboard │ │ │ ├── UploadWidget.storyboard │ │ │ └── Widgets.storyboard │ │ ├── Splash.storyboard │ │ ├── Transform/ │ │ │ ├── RevealImage.storyboard │ │ │ ├── SmartCropping.storyboard │ │ │ └── Transform.storyboard │ │ └── Upload/ │ │ ├── SingleUpload.storyboard │ │ ├── SingleUploadPreview.storyboard │ │ ├── UploadChoice.storyboard │ │ └── UploadDoesNotExist.storyboard │ ├── Cloudinary.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ ├── Cloudinary-Example.xcscheme │ │ └── travis_public_scheme.xcscheme │ ├── Podfile │ ├── README.md │ ├── Tests/ │ │ ├── BaseNetwork/ │ │ │ ├── BaseTestCase.swift │ │ │ ├── Core/ │ │ │ │ ├── AuthenticationTests.swift │ │ │ │ ├── CLDCloudinaryTests.m │ │ │ │ ├── CLDCloudinaryTests.swift │ │ │ │ ├── ParameterEncodingTests.swift │ │ │ │ ├── RequestTests.swift │ │ │ │ ├── ResponseTests.swift │ │ │ │ ├── ResultTests.swift │ │ │ │ ├── SessionDelegateTests.swift │ │ │ │ ├── SessionManagerTests.swift │ │ │ │ └── UploadTests.swift │ │ │ ├── Extensions/ │ │ │ │ ├── CLDNDataResponse+CloudinaryTests.swift │ │ │ │ ├── CLDNError+CloudinaryTests.swift │ │ │ │ ├── CLDNResult+CloudinaryTests.swift │ │ │ │ └── FileManager+CloudinaryTests.swift │ │ │ ├── Features/ │ │ │ │ ├── CacheTests.swift │ │ │ │ ├── MultipartFormDataTests.swift │ │ │ │ ├── ResponseSerializationTests.swift │ │ │ │ ├── URLProtocolTests.swift │ │ │ │ └── ValidationTests.swift │ │ │ ├── ObjcBaseTestCase.h │ │ │ ├── ObjcBaseTestCase.m │ │ │ └── Resources/ │ │ │ └── Responses/ │ │ │ ├── JSON/ │ │ │ │ ├── empty_data.json │ │ │ │ ├── invalid_data.json │ │ │ │ └── valid_data.json │ │ │ ├── Property List/ │ │ │ │ ├── empty.data │ │ │ │ ├── invalid.data │ │ │ │ └── valid.data │ │ │ └── String/ │ │ │ ├── empty_string.txt │ │ │ ├── utf32_string.txt │ │ │ └── utf8_string.txt │ │ ├── ConfigurationTests/ │ │ │ ├── CLDAnalyticsTests.swift │ │ │ ├── CLDConfigurationTests.m │ │ │ └── CLDConfigurationTests.swift │ │ ├── CryptoUtilsTests/ │ │ │ ├── CryptoUtilsTests.m │ │ │ └── CryptoUtilsTests.swift │ │ ├── FileUtilsTests.swift │ │ ├── GenerateUrlTests/ │ │ │ ├── UrlTests.m │ │ │ └── UrlTests.swift │ │ ├── Info.plist │ │ ├── NetworkTests/ │ │ │ ├── AccessibilityUploderTests/ │ │ │ │ ├── UploaderAccessibilityTests.m │ │ │ │ └── UploaderAccessibilityTests.swift │ │ │ ├── DownloaderAssetTests.swift │ │ │ ├── DownloaderTests.swift │ │ │ ├── ManagementApiTests.swift │ │ │ ├── NetworkBaseTests/ │ │ │ │ ├── NetworkBaseTest.swift │ │ │ │ ├── NetworkBaseTestObjc.h │ │ │ │ └── NetworkBaseTestObjc.m │ │ │ ├── NetworkTestUtils.swift │ │ │ └── UploaderTests/ │ │ │ ├── MockProvider/ │ │ │ │ └── BaseMockProvider.swift │ │ │ ├── OcrUploaderTests/ │ │ │ │ ├── ExplicitMockOcrTests.m │ │ │ │ ├── ExplicitMockOcrTests.swift │ │ │ │ ├── OcrMockProvider.swift │ │ │ │ ├── UploaderMockOcrTests.m │ │ │ │ ├── UploaderMockOcrTests.swift │ │ │ │ └── UploaderOcrTests.swift │ │ │ ├── PreprocessUploaderTests/ │ │ │ │ └── PreprocessUploaderTests.swift │ │ │ ├── QualityAnalysisUploaderTests/ │ │ │ │ ├── MockProviderQualityAnalysis.swift │ │ │ │ ├── ObjcQualityAnalysisExplicitResultParserTests.m │ │ │ │ ├── ObjcQualityAnalysisUploadResultParserTests.m │ │ │ │ ├── ObjcUploaderQualityAnalysisTests.m │ │ │ │ ├── QualityAnalysisExplicitResultParserTests.swift │ │ │ │ ├── QualityAnalysisUploadResultParserTests.swift │ │ │ │ └── UploaderQualityAnalysisTests.swift │ │ │ └── UploaderTests.swift │ │ ├── ParamTests/ │ │ │ └── UploadRequestParamsTests.swift │ │ ├── Preprocess/ │ │ │ ├── PreprocessTests.swift │ │ │ └── VideoPreprocessTests.swift │ │ ├── Resources/ │ │ │ └── Docs/ │ │ │ └── docx.docx │ │ ├── StringUtilsTest.swift │ │ ├── TestableCloudinary.swift │ │ ├── TransformationTests/ │ │ │ ├── CLDConditionExpressionTests/ │ │ │ │ ├── CLDConditionExpressionHelpersTests.swift │ │ │ │ ├── CLDConditionExpressionTests.m │ │ │ │ └── CLDConditionExpressionTests.swift │ │ │ ├── CLDExpressionTests/ │ │ │ │ ├── CLDExpressionTests.m │ │ │ │ └── CLDExpressionTests.swift │ │ │ ├── CLDTransformationTests/ │ │ │ │ ├── CLDTransformationBaselineTests.swift │ │ │ │ ├── CLDTransformationConditionsTests.swift │ │ │ │ ├── CLDTransformationExpressionsTests.swift │ │ │ │ ├── CLDTransformationTests.m │ │ │ │ ├── CLDTransformationTests.swift │ │ │ │ └── CLDTransformationVariablesTests.swift │ │ │ └── CLDVariableTests/ │ │ │ ├── CLDVariableTests.m │ │ │ └── CLDVariableTests.swift │ │ ├── UIExtensions/ │ │ │ ├── CLDVideoPlayerTests.swift │ │ │ ├── UIBaseTest.swift │ │ │ ├── UIButtonTests.swift │ │ │ ├── UIImageViewTests.swift │ │ │ └── Video Analytics/ │ │ │ ├── VideoEventsManagerTests.swift │ │ │ └── VideoEventsTests.swift │ │ └── UploadWidgetTests/ │ │ ├── UploadWidgetHelpersTests/ │ │ │ ├── UploaderWidgetAssetContainerTests.swift │ │ │ ├── UploaderWidgetConfigurationTests.m │ │ │ └── UploaderWidgetConfigurationTests.swift │ │ ├── UploadWidgetVideoTests/ │ │ │ ├── UploaderWidgetVideoControlsTests.swift │ │ │ ├── UploaderWidgetVideoDisplayLinkTests.swift │ │ │ ├── UploaderWidgetVideoPlayerTests.swift │ │ │ └── UploaderWidgetVideoViewTests.swift │ │ ├── UploaderWidgetEditTests/ │ │ │ └── UploaderWidgetEditViewControllerTests.swift │ │ ├── UploaderWidgetPreviewTests/ │ │ │ ├── UploaderWidgetCollectionCellTests.swift │ │ │ └── UploaderWidgetPreviewViewControllerTests.swift │ │ ├── UploaderWidgetTests/ │ │ │ ├── UploaderWidgetTests.m │ │ │ └── UploaderWidgetTests.swift │ │ ├── UploaderWidgetViewControllerTests/ │ │ │ └── UploaderWidgetViewControllerTests.swift │ │ └── WidgetBaseTest.swift │ └── WidgetUITests/ │ ├── Info.plist │ └── WidgetUITests.swift ├── LICENSE ├── Package.swift ├── Package@swift-5.3.swift ├── README.md └── tools/ └── get_test_cloud.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- ## Bug report for Cloudinary iOS SDK Before proceeding, please update to latest version and test if the issue persists ## Describe the bug in a sentence or two. … ## Issue Type (Can be multiple) [ ] Build - Can’t install or import the SDK [ ] Performance - Performance issues [ ] Behaviour - Functions aren’t working as expected (Such as generate URL) [ ] Documentation - Inconsistency between the docs and behaviour [ ] Other (Specify) ## Steps to reproduce … if applicable ## Error screenshots or Stack Trace (if applicable) … ## Build/Dependency management [ ] Cocoa-Pods [ ] Carthage [ ] Manual import [ ] Other (Specify) ## Is the issue reproducible only on a specific device? [ ] No [ ] Yes (specify model + iOS version) ## Versions and Libraries (fill in the version numbers) iOS Cloudinary SDK version - 0.0.0 OSX (on the dev environment) - 0.0.0 XCode - 0.0.0 Swift - 0.0.0 Target iOS - 0.0.0 Repository If possible, please provide a link to a reproducible repository that showcases the problem ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- ## Feature request for Cloudinary iOS SDK …(If your feature is for other SDKs, please request them there) ## Explain your use case … (A high level explanation of why you need this feature) ## Describe the problem you’re trying to solve … (A more technical view of what you’d like to accomplish, and how this feature will help you achieve it) ## Do you have a proposed solution? … (yes, no? Please elaborate if needed) ================================================ FILE: .github/pull_request_template.md ================================================ ### Brief Summary of Changes #### What does this PR address? - [ ] GitHub issue (Add reference - #XX) - [ ] Refactoring - [ ] New feature - [ ] Bug fix - [ ] Adds more tests #### Are tests included? - [ ] Yes - [ ] No #### Reviewer, please note: #### Checklist: - [ ] My code follows the code style of this project. - [ ] My change requires a change to the documentation. - [ ] I ran the full test suite before pushing the changes and all the tests pass. ================================================ FILE: .gitignore ================================================ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore *.DS_Store ## Build generated build/ DerivedData/ ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata/ ## Other *.moved-aside *.xcuserstate ## Obj-C/Swift specific *.hmap *.ipa *.dSYM.zip *.dSYM ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ .build/ # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # Pods/ _Pods.xcodeproj # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. Example/Carthage/Checkouts Example/Carthage/Build # fastlane # # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # screenshots whenever they are needed. # For more information about the recommended setup visit: # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output iOSInjectionProject/ Cloudinary/Frameworks/CLDCrypto # Samples AppDelegate+config.swift Example/Pods/ Example/Podfile.lock Example/Cloudinary.xcworkspace/ ================================================ FILE: .gitmodules ================================================ ================================================ FILE: .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: .travis.yml ================================================ language: objective-c os: osx before_script: > export CLOUDINARY_URL=$(bash tools/get_test_cloud.sh); echo cloud_name: "$(echo $CLOUDINARY_URL | cut -d'@' -f2)" notifications: email: recipients: - sdk_developers@cloudinary.com jobs: - osx_image: xcode12 xcode_workspace: Example/Cloudinary.xcworkspace xcode_scheme: travis_public_scheme podfile: Example/Podfile install: pod install --project-directory=Example env: CLOUDINARY_URL=$CLOUDINARY_URL xcode_destination: platform=iOS Simulator,OS=13.0,name=iPhone 8 - osx_image: xcode12 xcode_workspace: Example/Cloudinary.xcworkspace xcode_scheme: travis_public_scheme podfile: Example/Podfile install: pod install --project-directory=Example env: CLOUDINARY_URL=$CLOUDINARY_URL xcode_destination: platform=iOS Simulator,OS=14.0,name=iPhone 8 - osx_image: xcode13.2 xcode_workspace: Example/Cloudinary.xcworkspace xcode_scheme: travis_public_scheme podfile: Example/Podfile install: pod install --project-directory=Example env: CLOUDINARY_URL=$CLOUDINARY_URL xcode_destination: platform=iOS Simulator,OS=15.2,name=iPhone 8 ================================================ FILE: CHANGELOG.md ================================================ 5.2.5 / 2026-01-08 ================== * Remove _Pods.xcodeproj 5.2.4 / 2025-11-30 ================== Other changes ------------- * Fix duration attribute for video analytics 5.2.3 / 2025-08-12 ================== Other Changes ------------- * Add video analytics duration event * Add flush events support to video analytics 5.2.2 / 2025-07-30 ================== Other Changes -------------- * Fix video analytics duration event 5.2.1 / 2025-06-25 ================== Other Changes -------------- * Add video analytics as public API 5.2.0 / 2025-06-18 ================== New functionality ----------------- * Add support for `auto_chaptering` and `auto_transcription` in Upload API * Add extra headers support Other Changes -------------- * Fix API parameters signature 5.1.0 / 2024-09-17 ================== New functionality ----------------- * Add video transcode * Add auto_chaptering and auto_transcription to Upload API 5.0.0 / 2024-04-14 ================== Breaking Changes ---------------- * Remove CLDURLCache in favor of native URLCache * Remove ImageCache * Old cache saved to the disk will be purged New functionality ----------------- * Add URLCache support for `CLDUIImageVIew` 4.7.0 / 2024-03-25 ================== Other Changes ------------- * Fix privacy manifest * Fix `CLDVideoPlayer` 4.6.0 / 2024-03-12 ================== New functionality ----------------- * Add support for `media_metadata` Other Changes ------------- * Fix video analytics 4.5.0 / 2024-02-18 ================== Other Changes ------------- * Add privacy manifest 4.4.0 / 2024-01-15 ================== Other Changes ------------- * Update analytics token 4.3.0 / 2023-12-25 ================== Other Changes ------------- * Add video player analytics 4.2.0 / 2023-10-12 ================== Other Changes ------------- * Update analytics token 4.1.1 / 2023-09-18 ================== Other Changes ------------- * Fix analytics import 4.1.0 / 2023-08-06 ================== New functionality ----------------- * Add video player widget 4.0.1 / 2023-06-12 ================== Other changes ------------- * Fix analytics token prefix 4.0.0 / 2023-05-22 ================== New functionality ----------------- * Turn on `CLDURLCache` on by default Other changes ------------- * Make upload request respect timeout 3.4.0 / 2023-03-12 ================== New functionality ----------------- * Add URLCache support for images * Add tests for explicit and rename Other changes ------------- * Fix warning using `URLCredentialStorage` 3.3.0 / 2022-06-12 ================== New functionality ----------------- * Add support for folder decoupling * Add support for `startOffset` and `endOffset` as expression * Allow to disable b-frames * Send tags as an array * Add support for `originalFilename` upload parameter Other changes ------------- * Fix expression normalisation 3.2.1 / 2022-01-11 ================== * Fix `Carthage` 3.2.0 / 2022-01-10 ================== New functionality ----------------- * Add support for `apiKey` argument in Upload API * Add support for `preview` effect Other changes ------------- * Update travis job to support multiple iOS versions * Add test for preview effect with duration parameter * Recover `Cloudinary.xcodeproj` file * Fix `ImageView` size in preview widget `CollectionView` * Improve network error handling * Add `PNG` image upload unit tests 3.1.0 / 2021-12-03 ================== New functionality ----------------- * Add support for `backgroundRemoval` upload parameter Other changes ------------- * Fix `Carthage` 3.0.3 / 2021-11-14 ================== * Fix error handling in `fetchImage` 3.0.2 / 2021-11-02 ================== * Improve error handling in `fetchImage` 3.0.1 / 2021-08-31 ============= * Fix deprecation warnings 3.0.0 / 2021-04-26 ============= Breaking changes ----------------- * Bump minimum iOS target version to 9.0 (#334) New functionality ----------------- * Add new url cache system (#331) * Add operationQueue names (#333) * Add support for video assets in upload widget (#322) Other changes ------------- * Fix image assets for tests in multiple OS versions (#329) * Add missing imports for SPM (#326) 2.10.1 / 2021-01-20 ================== * Re-add `Cloudinary.xscheme` to fix carthage support (#324) 2.10.0 / 2021-01-10 ================== New functionality ----------------- * Add Upload Widget (#320) * Add support for crop preprocess action (#263) * Add support for rotation preprocess action (#264) * Add support for enhanced quality analysis scores * Add support for user defined variables and conditional expressions (#238) * Support array of values for radius (#235) * Add `eval` parameter to the upload Params. * Add support for accessibility analysis (#260) * Add option to control url signature algorithm (#256) * Add support for custom pre-functions (#253) * Add support for Long signature in URLs (#250) * Add global timeout support(#251) * Add OCR support un transformations and Upload APIs. Other changes ------------- * Add checks to validate responsive transformation. * Fix space encoding in a generated URL (#274) * Change local cache-keys encoding to `sha256` * Fix OCR parameter usage in `UploadRequestParamsTests` (#262) * Support urls with mime-type suffix in uploader. * Update SPM definitions file (#234) 2.9.0 / 2020-04-16 ================== * Remove Alamofire dependency (#229) * Add Swift Package Manager support. * Add Carthage support. * Rearrange workspace (#226) * Add support for `assist_colorblind` effect. (#227) * Validate URL scheme (#224) 2.9.0-beta.2 / 2020-04-05 ========================= * Remove Alamofire dependency (#229) 2.9.0-beta.1 / 2020-03-23 ========================= * Add Swift Package Manager support. * Add Carthage support. * Rearrange workspace (#226) * Add support for `assist_colorblind` effect. (#227) * Validate URL scheme (#224) 2.8.0 / 2019-11-11 ================== * Add shared scheme (#220) * Add support for artistic filters. (#214) * Fix chunk upload to keep original file name. (#217) * Fix face coordinates test. (#215) 2.7.0 / 2019-04-07 ================== New functionality ----------------- * Support swift 5, fix related warnings. * Bump Alamofire version to 4.8.2 * Add support for fetch layer in transformations. (#197) * Add support for `quality_analysis` parameter in upload. (#195) * Add support for font antialiasing and hinting in text overlays. (#193) * Add support for `streaming_profile` param in `CLDTransformation`. (#194) * Support excluding the default version from the generated cloudinary-url. (#206) * Add quality_override parameter to upload/explicit. (#199) Other changes ------------- * Replace crypto kit files with `CommonCrypto` import. * Fix memory cache limit not getting initially set in `CLDImageCache` (#201) * Fix bas64 string validation in uploader. (#196) 2.6.1 / 2019-01-30 ================== * Fix project setup and tests for swift 4.2 (#191) 2.6.0 / 2019-01-29 ================== New functionality ----------------- * Add support for google storage (`gs://`) urls in uploader. (#184) * Add support for `fps` parameter in Transformations. (#182) * Add format field to responsive breakpoints in upload params (#152) * Add `removeFromCache` to `CLDCloudinary` Other changes ------------- * Bump Alamofire version to 4.8.1 * Add device and os data to user agent. (#180) * Remove duplicate Alamofire reference 2.5.0 / 2018-11-05 ================== * Migrate to Swift 4.2 * Add support for custom transformation functions * Fix duplicate upload callbacks when using preprocessing * Bump Alamofire to version to 4.7.3 * Add String extensions for base64 encoding * Add `@discardableResult` annotations for uploadLarge methods 2.4.0 / 2018-06-26 ================== New functionality ----------------- * Add support for `async` upload parameter * Add support for eager transformation format * Add support for automatic quality in transformations. (#150) * Add cache max-memory config (#98) * Add Keyframe interval transformation parameter (#90) Other changes ------------- * Refactor CLDBaseParam for compatibility with iOS 11.4 * Remove wrong project config (library search paths) * Bump Alamofire version to 4.7.2 * Fix Alamofire submodule definition * Fix signature parameter handling in Management Api. (#161) * Fix `faceCenter` and `facesCenter` values in `CLDGravity` (#159) * Fix calculation of UIImage memory cache cost 2.3.0 / 2018-03-16 ================== * Add access control parameter to upload (#142) 2.2.2 / 2018-03-05 ================== * Add baseline Objective-C test. * Use `@objcMembers` where applies, for improved Objective-C compatibility. 2.2.1 / 2018-02-14 ================== * Fix objective-c compatibility issues with `CLDResponsiveParams` properties. 2.2.0 / 2018-02-01 ================== New functionality ----------------- * Add support for responsive image download * Add `auto` to `CLDGravity` Other changes ------------- * Fix synchronization issue when using `cldSetImage()` on `UIViews` within view collections. 2.1.0 / 2018-01-04 ================== New functionality ----------------- * Add image preprocessing and validations * Resizing * Format and quality settings * Support for custom preprocessors and validators Other changes ------------- * Remove custom operators * Update Alamofire to version 4.6.0 2.0.4 / 2017-12-20 ================== * Fix user-agent sdk version * Replace CommonCrypto wrapper with CryptoKit based code * Remove autotagging test (behaviour change) * Support Swift 4 2.0.3 / 2017-11-23 ================== New functionality ----------------- * Add support for chunked upload Other changes ------------- * Update Readme to point to HTTPS URLs of cloudinary.com * Fix manual installation repository url. 2.0.2 / 2017-06-08 ================== New functionality ----------------- * Support SEO suffix for private images. Other changes ------------- * Escape `|` and `=` characters in context values. * Double encode commas and slashes instead of using special UTF-8 values * Generate CLDCrypto framework in every build. Fixes #80. * Removing extra CLDCrypto path from podspec * Update `README.md` Alamo version to 4.1.0 * Add 3 images in PhotoViewController to demonstrate transformations. * Add progress indicator * Updated the framework and project deployment target to 8.0, updated podspec deployment target to 8.0 * Fixed signed upload using CLDSignature - Added unit test to test a signed upload using CLDSignature 2.0.1 / 2017-01-24 ================== * Fix pod install issue. Fixes #57. * Fix shellScript * Fix URLs in tests * Increment Alamofire to ~>4.1. Fixes #59. * Update configuration 2.0.1 / 2017-01-22 ================== * Fix Framework path in podspec/xcconfig 2.0.0 / 2017-01-18 ================== * New Swift 3.0 code ================================================ FILE: Cloudinary/Assets/.gitkeep ================================================ ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNConvertible.swift ================================================ // // CLDNConvertible.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation /// Types adopting the `CLDNURLConvertible` protocol can be used to construct URLs, which are then used to construct /// URL requests. internal protocol CLDNURLConvertible { /// Returns a URL that conforms to RFC 2396 or throws an `Error`. /// /// - throws: An `Error` if the type cannot be converted to a `URL`. /// /// - returns: A URL or throws an `Error`. func CLDN_AsURL() throws -> URL } extension String: CLDNURLConvertible { /// Returns a URL if `self` represents a valid URL string that conforms to RFC 2396 or throws an `CLDNError`. /// /// - throws: An `CLDNError.invalidURL` if `self` is not a valid URL string. /// /// - returns: A URL or throws an `CLDNError`. internal func CLDN_AsURL() throws -> URL { guard let url = URL(string: self) else { throw CLDNError.invalidURL(url: self) } return url } } extension URL: CLDNURLConvertible { /// Returns self. internal func CLDN_AsURL() throws -> URL { return self } } extension URLComponents: CLDNURLConvertible { /// Returns a URL if `url` is not nil, otherwise throws an `Error`. /// /// - throws: An `CLDNError.invalidURL` if `url` is `nil`. /// /// - returns: A URL or throws an `CLDNError`. internal func CLDN_AsURL() throws -> URL { guard let url = url else { throw CLDNError.invalidURL(url: self) } return url } } // MARK: - /// Types adopting the `CLDNURLRequestConvertible` protocol can be used to construct URL requests. internal protocol CLDNURLRequestConvertible { /// Returns a URL request or throws if an `Error` was encountered. /// /// - throws: An `Error` if the underlying `URLRequest` is `nil`. /// /// - returns: A URL request. func CLDN_AsURLRequest() throws -> URLRequest } extension CLDNURLRequestConvertible { /// The URL request. internal var urlRequest: URLRequest? { return try? CLDN_AsURLRequest() } } extension URLRequest: CLDNURLRequestConvertible { /// Returns a URL request or throws if an `Error` was encountered. internal func CLDN_AsURLRequest() throws -> URLRequest { return self } } // MARK: - extension URLRequest { /// Creates an instance with the specified `method`, `urlString` and `headers`. /// /// - parameter url: The URL. /// - parameter method: The HTTP method. /// - parameter headers: The HTTP headers. `nil` by default. /// /// - returns: The new `URLRequest` instance. internal init(url: CLDNURLConvertible, method: CLDNHTTPMethod, headers: CLDNHTTPHeaders? = nil) throws { let url = try url.CLDN_AsURL() self.init(url: url) httpMethod = method.rawValue if let headers = headers { for (headerField, headerValue) in headers { setValue(headerValue, forHTTPHeaderField: headerField) } } } func adapt(using adapter: CLDNRequestAdapter?) throws -> URLRequest { guard let adapter = adapter else { return self } return try adapter.CLDN_Adapt(self) } } ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNError.swift ================================================ // // CLDNError.swift // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation /// `CLDNError` is the error type returned by Cloudinary. It encompasses a few different types of errors, each with /// their own associated reasons. /// /// - invalidURL: Returned when a `URLConvertible` type fails to create a valid `URL`. /// - parameterEncodingFailed: Returned when a parameter encoding object throws an error during the encoding process. /// - multipartEncodingFailed: Returned when some step in the multipart encoding process fails. /// - responseValidationFailed: Returned when a `validate()` call fails. /// - responseSerializationFailed: Returned when a response serializer encounters an error in the serialization process. internal enum CLDNError: Error { /// The underlying reason the parameter encoding error occurred. /// /// - missingURL: The URL request did not have a URL to encode. /// - jsonEncodingFailed: JSON serialization failed with an underlying system error during the /// encoding process. /// - propertyListEncodingFailed: Property list serialization failed with an underlying system error during /// encoding process. internal enum ParameterEncodingFailureReason { case missingURL case jsonEncodingFailed(error: Error) case propertyListEncodingFailed(error: Error) } /// The underlying reason the multipart encoding error occurred. /// /// - bodyPartURLInvalid: The `fileURL` provided for reading an encodable body part isn't a /// file URL. /// - bodyPartFilenameInvalid: The filename of the `fileURL` provided has either an empty /// `lastPathComponent` or `pathExtension. /// - bodyPartFileNotReachable: The file at the `fileURL` provided was not reachable. /// - bodyPartFileNotReachableWithError: Attempting to check the reachability of the `fileURL` provided threw /// an error. /// - bodyPartFileIsDirectory: The file at the `fileURL` provided is actually a directory. /// - bodyPartFileSizeNotAvailable: The size of the file at the `fileURL` provided was not returned by /// the system. /// - bodyPartFileSizeQueryFailedWithError: The attempt to find the size of the file at the `fileURL` provided /// threw an error. /// - bodyPartInputStreamCreationFailed: An `InputStream` could not be created for the provided `fileURL`. /// - outputStreamCreationFailed: An `OutputStream` could not be created when attempting to write the /// encoded data to disk. /// - outputStreamFileAlreadyExists: The encoded body data could not be writtent disk because a file /// already exists at the provided `fileURL`. /// - outputStreamURLInvalid: The `fileURL` provided for writing the encoded body data to disk is /// not a file URL. /// - outputStreamWriteFailed: The attempt to write the encoded body data to disk failed with an /// underlying error. /// - inputStreamReadFailed: The attempt to read an encoded body part `InputStream` failed with /// underlying system error. internal enum MultipartEncodingFailureReason { case bodyPartURLInvalid(url: URL) case bodyPartFilenameInvalid(in: URL) case bodyPartFileNotReachable(at: URL) case bodyPartFileNotReachableWithError(atURL: URL, error: Error) case bodyPartFileIsDirectory(at: URL) case bodyPartFileSizeNotAvailable(at: URL) case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error) case bodyPartInputStreamCreationFailed(for: URL) case outputStreamCreationFailed(for: URL) case outputStreamFileAlreadyExists(at: URL) case outputStreamURLInvalid(url: URL) case outputStreamWriteFailed(error: Error) case inputStreamReadFailed(error: Error) } /// The underlying reason the response validation error occurred. /// /// - dataFileNil: The data file containing the server response did not exist. /// - dataFileReadFailed: The data file containing the server response could not be read. /// - missingContentType: The response did not contain a `Content-Type` and the `acceptableContentTypes` /// provided did not contain wildcard type. /// - unacceptableContentType: The response `Content-Type` did not match any type in the provided /// `acceptableContentTypes`. /// - unacceptableStatusCode: The response status code was not acceptable. internal enum ResponseValidationFailureReason { case dataFileNil case dataFileReadFailed(at: URL) case missingContentType(acceptableContentTypes: [String]) case unacceptableContentType(acceptableContentTypes: [String], responseContentType: String) case unacceptableStatusCode(code: Int) } /// The underlying reason the response serialization error occurred. /// /// - inputDataNil: The server response contained no data. /// - inputDataNilOrZeroLength: The server response contained no data or the data was zero length. /// - inputFileNil: The file containing the server response did not exist. /// - inputFileReadFailed: The file containing the server response could not be read. /// - stringSerializationFailed: String serialization failed using the provided `String.Encoding`. /// - jsonSerializationFailed: JSON serialization failed with an underlying system error. /// - propertyListSerializationFailed: Property list serialization failed with an underlying system error. internal enum ResponseSerializationFailureReason { case inputDataNil case inputDataNilOrZeroLength case inputFileNil case inputFileReadFailed(at: URL) case stringSerializationFailed(encoding: String.Encoding) case jsonSerializationFailed(error: Error) case propertyListSerializationFailed(error: Error) } case invalidURL(url: CLDNURLConvertible) case parameterEncodingFailed(reason: ParameterEncodingFailureReason) case multipartEncodingFailed(reason: MultipartEncodingFailureReason) case responseValidationFailed(reason: ResponseValidationFailureReason) case responseSerializationFailed(reason: ResponseSerializationFailureReason) } // MARK: - Adapt Error struct AdaptError: Error { let error: Error } extension Error { var underlyingAdaptError: Error? { return (self as? AdaptError)?.error } } // MARK: - Error Booleans extension CLDNError { /// Returns whether the CLDNError is an invalid URL error. internal var isInvalidURLError: Bool { if case .invalidURL = self { return true } return false } /// Returns whether the CLDNError is a parameter encoding error. When `true`, the `underlyingError` property will /// contain the associated value. internal var isParameterEncodingError: Bool { if case .parameterEncodingFailed = self { return true } return false } /// Returns whether the CLDNError is a multipart encoding error. When `true`, the `url` and `underlyingError` properties /// will contain the associated values. internal var isMultipartEncodingError: Bool { if case .multipartEncodingFailed = self { return true } return false } /// Returns whether the `CLDNError` is a response validation error. When `true`, the `acceptableContentTypes`, /// `responseContentType`, and `responseCode` properties will contain the associated values. internal var isResponseValidationError: Bool { if case .responseValidationFailed = self { return true } return false } /// Returns whether the `CLDNError` is a response serialization error. When `true`, the `failedStringEncoding` and /// `underlyingError` properties will contain the associated values. internal var isResponseSerializationError: Bool { if case .responseSerializationFailed = self { return true } return false } } // MARK: - Convenience Properties extension CLDNError { /// The `URLConvertible` associated with the error. var urlConvertible: CLDNURLConvertible? { switch self { case .invalidURL(let url): return url default: return nil } } /// The `URL` associated with the error. internal var url: URL? { switch self { case .multipartEncodingFailed(let reason): return reason.url default: return nil } } /// The `Error` returned by a system framework associated with a `.parameterEncodingFailed`, /// `.multipartEncodingFailed` or `.responseSerializationFailed` error. internal var underlyingError: Error? { switch self { case .parameterEncodingFailed(let reason): return reason.underlyingError case .multipartEncodingFailed(let reason): return reason.underlyingError case .responseSerializationFailed(let reason): return reason.underlyingError default: return nil } } /// The acceptable `Content-Type`s of a `.responseValidationFailed` error. internal var acceptableContentTypes: [String]? { switch self { case .responseValidationFailed(let reason): return reason.acceptableContentTypes default: return nil } } /// The response `Content-Type` of a `.responseValidationFailed` error. internal var responseContentType: String? { switch self { case .responseValidationFailed(let reason): return reason.responseContentType default: return nil } } /// The response code of a `.responseValidationFailed` error. internal var responseCode: Int? { switch self { case .responseValidationFailed(let reason): return reason.responseCode default: return nil } } /// The `String.Encoding` associated with a failed `.stringResponse()` call. internal var failedStringEncoding: String.Encoding? { switch self { case .responseSerializationFailed(let reason): return reason.failedStringEncoding default: return nil } } } extension CLDNError.ParameterEncodingFailureReason { var underlyingError: Error? { switch self { case .jsonEncodingFailed(let error), .propertyListEncodingFailed(let error): return error default: return nil } } } extension CLDNError.MultipartEncodingFailureReason { var url: URL? { switch self { case .bodyPartURLInvalid(let url), .bodyPartFilenameInvalid(let url), .bodyPartFileNotReachable(let url), .bodyPartFileIsDirectory(let url), .bodyPartFileSizeNotAvailable(let url), .bodyPartInputStreamCreationFailed(let url), .outputStreamCreationFailed(let url), .outputStreamFileAlreadyExists(let url), .outputStreamURLInvalid(let url), .bodyPartFileNotReachableWithError(let url, _), .bodyPartFileSizeQueryFailedWithError(let url, _): return url default: return nil } } var underlyingError: Error? { switch self { case .bodyPartFileNotReachableWithError(_, let error), .bodyPartFileSizeQueryFailedWithError(_, let error), .outputStreamWriteFailed(let error), .inputStreamReadFailed(let error): return error default: return nil } } } extension CLDNError.ResponseValidationFailureReason { var acceptableContentTypes: [String]? { switch self { case .missingContentType(let types), .unacceptableContentType(let types, _): return types default: return nil } } var responseContentType: String? { switch self { case .unacceptableContentType(_, let responseType): return responseType default: return nil } } var responseCode: Int? { switch self { case .unacceptableStatusCode(let code): return code default: return nil } } } extension CLDNError.ResponseSerializationFailureReason { var failedStringEncoding: String.Encoding? { switch self { case .stringSerializationFailed(let encoding): return encoding default: return nil } } var underlyingError: Error? { switch self { case .jsonSerializationFailed(let error), .propertyListSerializationFailed(let error): return error default: return nil } } } // MARK: - Error Descriptions extension CLDNError: LocalizedError { internal var errorDescription: String? { switch self { case .invalidURL(let url): return "URL is not valid: \(url)" case .parameterEncodingFailed(let reason): return reason.localizedDescription case .multipartEncodingFailed(let reason): return reason.localizedDescription case .responseValidationFailed(let reason): return reason.localizedDescription case .responseSerializationFailed(let reason): return reason.localizedDescription } } } extension CLDNError.ParameterEncodingFailureReason { var localizedDescription: String { switch self { case .missingURL: return "URL request to encode was missing a URL" case .jsonEncodingFailed(let error): return "JSON could not be encoded because of error:\n\(error.localizedDescription)" case .propertyListEncodingFailed(let error): return "PropertyList could not be encoded because of error:\n\(error.localizedDescription)" } } } extension CLDNError.MultipartEncodingFailureReason { var localizedDescription: String { switch self { case .bodyPartURLInvalid(let url): return "The URL provided is not a file URL: \(url)" case .bodyPartFilenameInvalid(let url): return "The URL provided does not have a valid filename: \(url)" case .bodyPartFileNotReachable(let url): return "The URL provided is not reachable: \(url)" case .bodyPartFileNotReachableWithError(let url, let error): return ( "The system returned an error while checking the provided URL for " + "reachability.\nURL: \(url)\nError: \(error)" ) case .bodyPartFileIsDirectory(let url): return "The URL provided is a directory: \(url)" case .bodyPartFileSizeNotAvailable(let url): return "Could not fetch the file size from the provided URL: \(url)" case .bodyPartFileSizeQueryFailedWithError(let url, let error): return ( "The system returned an error while attempting to fetch the file size from the " + "provided URL.\nURL: \(url)\nError: \(error)" ) case .bodyPartInputStreamCreationFailed(let url): return "Failed to create an InputStream for the provided URL: \(url)" case .outputStreamCreationFailed(let url): return "Failed to create an OutputStream for URL: \(url)" case .outputStreamFileAlreadyExists(let url): return "A file already exists at the provided URL: \(url)" case .outputStreamURLInvalid(let url): return "The provided OutputStream URL is invalid: \(url)" case .outputStreamWriteFailed(let error): return "OutputStream write failed with error: \(error)" case .inputStreamReadFailed(let error): return "InputStream read failed with error: \(error)" } } } extension CLDNError.ResponseSerializationFailureReason { var localizedDescription: String { switch self { case .inputDataNil: return "Response could not be serialized, input data was nil." case .inputDataNilOrZeroLength: return "Response could not be serialized, input data was nil or zero length." case .inputFileNil: return "Response could not be serialized, input file was nil." case .inputFileReadFailed(let url): return "Response could not be serialized, input file could not be read: \(url)." case .stringSerializationFailed(let encoding): return "String could not be serialized with encoding: \(encoding)." case .jsonSerializationFailed(let error): return "JSON could not be serialized because of error:\n\(error.localizedDescription)" case .propertyListSerializationFailed(let error): return "PropertyList could not be serialized because of error:\n\(error.localizedDescription)" } } } extension CLDNError.ResponseValidationFailureReason { var localizedDescription: String { switch self { case .dataFileNil: return "Response could not be validated, data file was nil." case .dataFileReadFailed(let url): return "Response could not be validated, data file could not be read: \(url)." case .missingContentType(let types): return ( "Response Content-Type was missing and acceptable content types " + "(\(types.joined(separator: ","))) do not match \"*/*\"." ) case .unacceptableContentType(let acceptableTypes, let responseType): return ( "Response Content-Type \"\(responseType)\" does not match any acceptable types: " + "\(acceptableTypes.joined(separator: ","))." ) case .unacceptableStatusCode(let code): return "Response status code was unacceptable: \(code)." } } } ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNMultipartFormData.swift ================================================ // // CLDNMultipartFormData.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation #if os(iOS) || os(watchOS) || os(tvOS) import MobileCoreServices #elseif os(macOS) import CoreServices #endif /// Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode /// multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead /// to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the /// data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for /// larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset. /// /// For more information on `multipart/form-data` in general, please refer to the RFC-2388 and RFC-2045 specs as well /// and the w3 form documentation. /// /// - https://www.ietf.org/rfc/rfc2388.txt /// - https://www.ietf.org/rfc/rfc2045.txt /// - https://www.w3.org/TR/html401/interact/forms.html#h-17.13 internal class CLDNMultipartFormData { // MARK: - Helper Types struct EncodingCharacters { static let crlf = "\r\n" } struct BoundaryGenerator { enum BoundaryType { case initial, encapsulated, final } static func randomBoundary() -> String { return String(format: "Cloudinary.boundary.%08x%08x", arc4random(), arc4random()) } static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data { let boundaryText: String switch boundaryType { case .initial: boundaryText = "--\(boundary)\(EncodingCharacters.crlf)" case .encapsulated: boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)" case .final: boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)" } return boundaryText.data(using: String.Encoding.utf8, allowLossyConversion: false)! } } class BodyPart { let headers: CLDNHTTPHeaders let bodyStream: InputStream let bodyContentLength: UInt64 var hasInitialBoundary = false var hasFinalBoundary = false init(headers: CLDNHTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64) { self.headers = headers self.bodyStream = bodyStream self.bodyContentLength = bodyContentLength } } // MARK: - Properties /// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`. internal lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)" /// The content length of all body parts used to generate the `multipart/form-data` not including the boundaries. internal var contentLength: UInt64 { return bodyParts.reduce(0) { $0 + $1.bodyContentLength } } /// The boundary used to separate the body parts in the encoded form data. internal var boundary: String private var bodyParts: [BodyPart] private var bodyPartError: CLDNError? private let streamBufferSize: Int // MARK: - Lifecycle /// Creates a multipart form data object. /// /// - returns: The multipart form data object. internal init() { self.boundary = BoundaryGenerator.randomBoundary() self.bodyParts = [] /// /// The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more /// information, please refer to the following article: /// - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html /// self.streamBufferSize = 1024 } // MARK: - Body Parts /// Creates a body part from the data and appends it to the multipart form data object. /// /// The body part data will be encoded using the following format: /// /// - `Content-Disposition: form-data; name=#{name}` (HTTP Header) /// - Encoded data /// - Multipart form boundary /// /// - parameter data: The data to encode into the multipart form data. /// - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header. internal func append(_ data: Data, withName name: String) { let headers = contentHeaders(withName: name) let stream = InputStream(data: data) let length = UInt64(data.count) append(stream, withLength: length, headers: headers) } /// Creates a body part from the data and appends it to the multipart form data object. /// /// The body part data will be encoded using the following format: /// /// - `Content-Disposition: form-data; name=#{name}` (HTTP Header) /// - `Content-Type: #{generated mimeType}` (HTTP Header) /// - Encoded data /// - Multipart form boundary /// /// - parameter data: The data to encode into the multipart form data. /// - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header. /// - parameter mimeType: The MIME type to associate with the data content type in the `Content-Type` HTTP header. internal func append(_ data: Data, withName name: String, mimeType: String) { let headers = contentHeaders(withName: name, mimeType: mimeType) let stream = InputStream(data: data) let length = UInt64(data.count) append(stream, withLength: length, headers: headers) } /// Creates a body part from the data and appends it to the multipart form data object. /// /// The body part data will be encoded using the following format: /// /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) /// - `Content-Type: #{mimeType}` (HTTP Header) /// - Encoded file data /// - Multipart form boundary /// /// - parameter data: The data to encode into the multipart form data. /// - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header. /// - parameter fileName: The filename to associate with the data in the `Content-Disposition` HTTP header. /// - parameter mimeType: The MIME type to associate with the data in the `Content-Type` HTTP header. internal func append(_ data: Data, withName name: String, fileName: String, mimeType: String) { let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) let stream = InputStream(data: data) let length = UInt64(data.count) append(stream, withLength: length, headers: headers) } /// Creates a body part from the file and appends it to the multipart form data object. /// /// The body part data will be encoded using the following format: /// /// - `Content-Disposition: form-data; name=#{name}; filename=#{generated filename}` (HTTP Header) /// - `Content-Type: #{generated mimeType}` (HTTP Header) /// - Encoded file data /// - Multipart form boundary /// /// The filename in the `Content-Disposition` HTTP header is generated from the last path component of the /// `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the /// system associated MIME type. /// /// - parameter fileURL: The URL of the file whose content will be encoded into the multipart form data. /// - parameter name: The name to associate with the file content in the `Content-Disposition` HTTP header. internal func append(_ fileURL: URL, withName name: String) { let fileName = fileURL.lastPathComponent let pathExtension = fileURL.pathExtension if !fileName.isEmpty && !pathExtension.isEmpty { let mime = mimeType(forPathExtension: pathExtension) append(fileURL, withName: name, fileName: fileName, mimeType: mime) } else { setBodyPartError(withReason: .bodyPartFilenameInvalid(in: fileURL)) } } /// Creates a body part from the file and appends it to the multipart form data object. /// /// The body part data will be encoded using the following format: /// /// - Content-Disposition: form-data; name=#{name}; filename=#{filename} (HTTP Header) /// - Content-Type: #{mimeType} (HTTP Header) /// - Encoded file data /// - Multipart form boundary /// /// - parameter fileURL: The URL of the file whose content will be encoded into the multipart form data. /// - parameter name: The name to associate with the file content in the `Content-Disposition` HTTP header. /// - parameter fileName: The filename to associate with the file content in the `Content-Disposition` HTTP header. /// - parameter mimeType: The MIME type to associate with the file content in the `Content-Type` HTTP header. internal func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String) { let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) //============================================================ // Check 1 - is file URL? //============================================================ guard fileURL.isFileURL else { setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL)) return } //============================================================ // Check 2 - is file URL reachable? //============================================================ do { let isReachable = try fileURL.checkPromisedItemIsReachable() guard isReachable else { setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL)) return } } catch { setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error)) return } //============================================================ // Check 3 - is file URL a directory? //============================================================ var isDirectory: ObjCBool = false let path = fileURL.path guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else { setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL)) return } //============================================================ // Check 4 - can the file size be extracted? //============================================================ let bodyContentLength: UInt64 do { guard let fileSize = try FileManager.default.attributesOfItem(atPath: path)[.size] as? NSNumber else { setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL)) return } bodyContentLength = fileSize.uint64Value } catch { setBodyPartError(withReason: .bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error)) return } //============================================================ // Check 5 - can a stream be created from file URL? //============================================================ guard let stream = InputStream(url: fileURL) else { setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL)) return } append(stream, withLength: bodyContentLength, headers: headers) } /// Creates a body part from the stream and appends it to the multipart form data object. /// /// The body part data will be encoded using the following format: /// /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) /// - `Content-Type: #{mimeType}` (HTTP Header) /// - Encoded stream data /// - Multipart form boundary /// /// - parameter stream: The input stream to encode in the multipart form data. /// - parameter length: The content length of the stream. /// - parameter name: The name to associate with the stream content in the `Content-Disposition` HTTP header. /// - parameter fileName: The filename to associate with the stream content in the `Content-Disposition` HTTP header. /// - parameter mimeType: The MIME type to associate with the stream content in the `Content-Type` HTTP header. internal func append( _ stream: InputStream, withLength length: UInt64, name: String, fileName: String, mimeType: String) { let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) append(stream, withLength: length, headers: headers) } /// Creates a body part with the headers, stream and length and appends it to the multipart form data object. /// /// The body part data will be encoded using the following format: /// /// - HTTP headers /// - Encoded stream data /// - Multipart form boundary /// /// - parameter stream: The input stream to encode in the multipart form data. /// - parameter length: The content length of the stream. /// - parameter headers: The HTTP headers for the body part. internal func append(_ stream: InputStream, withLength length: UInt64, headers: CLDNHTTPHeaders) { let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length) bodyParts.append(bodyPart) } // MARK: - Data Encoding /// Encodes all the appended body parts into a single `Data` value. /// /// It is important to note that this method will load all the appended body parts into memory all at the same /// time. This method should only be used when the encoded data will have a small memory footprint. For large data /// cases, please use the `writeEncodedDataToDisk(fileURL:completionHandler:)` method. /// /// - throws: An `CLDNError` if encoding encounters an error. /// /// - returns: The encoded `Data` if encoding is successful. internal func encode() throws -> Data { if let bodyPartError = bodyPartError { throw bodyPartError } var encoded = Data() bodyParts.first?.hasInitialBoundary = true bodyParts.last?.hasFinalBoundary = true for bodyPart in bodyParts { let encodedData = try encode(bodyPart) encoded.append(encodedData) } return encoded } /// Writes the appended body parts into the given file URL. /// /// This process is facilitated by reading and writing with input and output streams, respectively. Thus, /// this approach is very memory efficient and should be used for large body part data. /// /// - parameter fileURL: The file URL to write the multipart form data into. /// /// - throws: An `CLDNError` if encoding encounters an error. internal func writeEncodedData(to fileURL: URL) throws { if let bodyPartError = bodyPartError { throw bodyPartError } if FileManager.default.fileExists(atPath: fileURL.path) { throw CLDNError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL)) } else if !fileURL.isFileURL { throw CLDNError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL)) } guard let outputStream = OutputStream(url: fileURL, append: false) else { throw CLDNError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL)) } outputStream.open() defer { outputStream.close() } self.bodyParts.first?.hasInitialBoundary = true self.bodyParts.last?.hasFinalBoundary = true for bodyPart in self.bodyParts { try write(bodyPart, to: outputStream) } } // MARK: - Private - Body Part Encoding private func encode(_ bodyPart: BodyPart) throws -> Data { var encoded = Data() let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData() encoded.append(initialData) let headerData = encodeHeaders(for: bodyPart) encoded.append(headerData) let bodyStreamData = try encodeBodyStream(for: bodyPart) encoded.append(bodyStreamData) if bodyPart.hasFinalBoundary { encoded.append(finalBoundaryData()) } return encoded } private func encodeHeaders(for bodyPart: BodyPart) -> Data { var headerText = "" for (key, value) in bodyPart.headers { headerText += "\(key): \(value)\(EncodingCharacters.crlf)" } headerText += EncodingCharacters.crlf return headerText.data(using: String.Encoding.utf8, allowLossyConversion: false)! } private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data { let inputStream = bodyPart.bodyStream inputStream.open() defer { inputStream.close() } var encoded = Data() while inputStream.hasBytesAvailable { var buffer = [UInt8](repeating: 0, count: streamBufferSize) let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize) if let error = inputStream.streamError { throw CLDNError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error)) } if bytesRead > 0 { encoded.append(buffer, count: bytesRead) } else { break } } return encoded } // MARK: - Private - Writing Body Part to Output Stream private func write(_ bodyPart: BodyPart, to outputStream: OutputStream) throws { try writeInitialBoundaryData(for: bodyPart, to: outputStream) try writeHeaderData(for: bodyPart, to: outputStream) try writeBodyStream(for: bodyPart, to: outputStream) try writeFinalBoundaryData(for: bodyPart, to: outputStream) } private func writeInitialBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData() return try write(initialData, to: outputStream) } private func writeHeaderData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { let headerData = encodeHeaders(for: bodyPart) return try write(headerData, to: outputStream) } private func writeBodyStream(for bodyPart: BodyPart, to outputStream: OutputStream) throws { let inputStream = bodyPart.bodyStream inputStream.open() defer { inputStream.close() } while inputStream.hasBytesAvailable { var buffer = [UInt8](repeating: 0, count: streamBufferSize) let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize) if let streamError = inputStream.streamError { throw CLDNError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: streamError)) } if bytesRead > 0 { if buffer.count != bytesRead { buffer = Array(buffer[0.. 0, outputStream.hasSpaceAvailable { let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite) if let error = outputStream.streamError { throw CLDNError.multipartEncodingFailed(reason: .outputStreamWriteFailed(error: error)) } bytesToWrite -= bytesWritten if bytesToWrite > 0 { buffer = Array(buffer[bytesWritten.. String { if let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(), let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() { return contentType as String } return "application/octet-stream" } // MARK: - Private - Content Headers private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> [String: String] { var disposition = "form-data; name=\"\(name)\"" if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" } var headers = ["Content-Disposition": disposition] if let mimeType = mimeType { headers["Content-Type"] = mimeType } return headers } // MARK: - Private - Boundary Encoding private func initialBoundaryData() -> Data { return BoundaryGenerator.boundaryData(forBoundaryType: .initial, boundary: boundary) } private func encapsulatedBoundaryData() -> Data { return BoundaryGenerator.boundaryData(forBoundaryType: .encapsulated, boundary: boundary) } private func finalBoundaryData() -> Data { return BoundaryGenerator.boundaryData(forBoundaryType: .final, boundary: boundary) } // MARK: - Private - Errors private func setBodyPartError(withReason reason: CLDNError.MultipartEncodingFailureReason) { guard bodyPartError == nil else { return } bodyPartError = CLDNError.multipartEncodingFailed(reason: reason) } } ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNParameterEncoding.swift ================================================ // // CLDNParameterEncoding.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation /// HTTP method definitions. /// /// See https://tools.ietf.org/html/rfc7231#section-4.3 internal enum CLDNHTTPMethod: String { case options = "OPTIONS" case get = "GET" case head = "HEAD" case post = "POST" case put = "PUT" case patch = "PATCH" case delete = "DELETE" case trace = "TRACE" case connect = "CONNECT" } // MARK: - /// A dictionary of parameters to apply to a `URLRequest`. internal typealias CLDNParameters = [String: Any] /// A type used to define how a set of parameters are applied to a `URLRequest`. internal protocol CLDNParameterEncoding { /// Creates a URL request by encoding parameters and applying them onto an existing request. /// /// - parameter urlRequest: The request to have parameters applied. /// - parameter parameters: The parameters to apply. /// /// - throws: An `CLDNError.parameterEncodingFailed` error if encoding fails. /// /// - returns: The encoded request. func CLDN_Encode(_ urlRequest: CLDNURLRequestConvertible, with parameters: CLDNParameters?) throws -> URLRequest } // MARK: - /// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP /// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as /// the HTTP body depends on the destination of the encoding. /// /// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to /// `application/x-www-form-urlencoded; charset=utf-8`. /// /// There is no published specification for how to encode collection types. By default the convention of appending /// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for /// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the /// square brackets appended to array keys. /// /// `BoolEncoding` can be used to configure how boolean values are encoded. The default behavior is to encode /// `true` as 1 and `false` as 0. internal struct CLDNURLEncoding: CLDNParameterEncoding { // MARK: Helper Types /// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the /// resulting URL request. /// /// - methodDependent: Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE` /// requests and sets as the HTTP body for requests with any other HTTP method. /// - queryString: Sets or appends encoded query string result to existing query string. /// - httpBody: Sets encoded query string result as the HTTP body of the URL request. internal enum Destination { case methodDependent, queryString, httpBody } /// Configures how `Array` parameters are encoded. /// /// - brackets: An empty set of square brackets is appended to the key for every value. /// This is the default behavior. /// - noBrackets: No brackets are appended. The key is encoded as is. internal enum ArrayEncoding { case brackets, noBrackets func encode(key: String) -> String { switch self { case .brackets: return "\(key)[]" case .noBrackets: return key } } } /// Configures how `Bool` parameters are encoded. /// /// - numeric: Encode `true` as `1` and `false` as `0`. This is the default behavior. /// - literal: Encode `true` and `false` as string literals. internal enum BoolEncoding { case numeric, literal func encode(value: Bool) -> String { switch self { case .numeric: return value ? "1" : "0" case .literal: return value ? "true" : "false" } } } // MARK: Properties /// Returns a default `URLEncoding` instance. internal static var `default`: CLDNURLEncoding { return CLDNURLEncoding() } /// Returns a `URLEncoding` instance with a `.methodDependent` destination. internal static var methodDependent: CLDNURLEncoding { return CLDNURLEncoding() } /// Returns a `URLEncoding` instance with a `.queryString` destination. internal static var queryString: CLDNURLEncoding { return CLDNURLEncoding(destination: .queryString) } /// Returns a `URLEncoding` instance with an `.httpBody` destination. internal static var httpBody: CLDNURLEncoding { return CLDNURLEncoding(destination: .httpBody) } /// The destination defining where the encoded query string is to be applied to the URL request. internal let destination: Destination /// The encoding to use for `Array` parameters. internal let arrayEncoding: ArrayEncoding /// The encoding to use for `Bool` parameters. internal let boolEncoding: BoolEncoding // MARK: Initialization /// Creates a `CLDNURLEncoding` instance using the specified destination. /// /// - parameter destination: The destination defining where the encoded query string is to be applied. /// - parameter arrayEncoding: The encoding to use for `Array` parameters. /// - parameter boolEncoding: The encoding to use for `Bool` parameters. /// /// - returns: The new `CLDNURLEncoding` instance. internal init(destination: Destination = .methodDependent, arrayEncoding: ArrayEncoding = .brackets, boolEncoding: BoolEncoding = .numeric) { self.destination = destination self.arrayEncoding = arrayEncoding self.boolEncoding = boolEncoding } // MARK: Encoding /// Creates a URL request by encoding parameters and applying them onto an existing request. /// /// - parameter urlRequest: The request to have parameters applied. /// - parameter parameters: The parameters to apply. /// /// - throws: An `Error` if the encoding process encounters an error. /// /// - returns: The encoded request. internal func CLDN_Encode(_ urlRequest: CLDNURLRequestConvertible, with parameters: CLDNParameters?) throws -> URLRequest { var urlRequest = try urlRequest.CLDN_AsURLRequest() guard let parameters = parameters else { return urlRequest } if let method = CLDNHTTPMethod(rawValue: urlRequest.httpMethod ?? "GET"), encodesParametersInURL(with: method) { guard let url = urlRequest.url else { throw CLDNError.parameterEncodingFailed(reason: .missingURL) } if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty { let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters) urlComponents.percentEncodedQuery = percentEncodedQuery urlRequest.url = urlComponents.url } } else { if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type") } urlRequest.httpBody = query(parameters).data(using: .utf8, allowLossyConversion: false) } return urlRequest } /// Creates percent-escaped, URL encoded query string components from the given key-value pair using recursion. /// /// - parameter key: The key of the query component. /// - parameter value: The value of the query component. /// /// - returns: The percent-escaped, URL encoded query string components. internal func queryComponents(fromKey key: String, value: Any) -> [(String, String)] { var components: [(String, String)] = [] if let dictionary = value as? [String: Any] { for (nestedKey, value) in dictionary { components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value) } } else if let array = value as? [Any] { for value in array { components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value) } } else if let value = value as? NSNumber { if value.isBool { components.append((escape(key), escape(boolEncoding.encode(value: value.boolValue)))) } else { components.append((escape(key), escape("\(value)"))) } } else if let bool = value as? Bool { components.append((escape(key), escape(boolEncoding.encode(value: bool)))) } else { components.append((escape(key), escape("\(value)"))) } return components } /// Returns a percent-escaped string following RFC 3986 for a query string key or value. /// /// RFC 3986 states that the following characters are "reserved" characters. /// /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/" /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=" /// /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/" /// should be percent-escaped in the query string. /// /// - parameter string: The string to be percent-escaped. /// /// - returns: The percent-escaped string. internal func escape(_ string: String) -> String { let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 let subDelimitersToEncode = "!$&'()*+,;=" var allowedCharacterSet = CharacterSet.urlQueryAllowed allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") var escaped = "" //========================================================================================================== // // Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few // hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no // longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more // info, please refer to: // // - https://github.com/Alamofire/Alamofire/issues/206 // //========================================================================================================== if #available(iOS 8.3, *) { escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string } else { let batchSize = 50 var index = string.startIndex while index != string.endIndex { let startIndex = index let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex let range = startIndex.. String { var components: [(String, String)] = [] for key in parameters.keys.sorted(by: <) { let value = parameters[key]! components += queryComponents(fromKey: key, value: value) } return components.map { "\($0)=\($1)" }.joined(separator: "&") } private func encodesParametersInURL(with method: CLDNHTTPMethod) -> Bool { switch destination { case .queryString: return true case .httpBody: return false default: break } switch method { case .get, .head, .delete: return true default: return false } } } // MARK: - /// Uses `JSONSerialization` to create a JSON representation of the parameters object, which is set as the body of the /// request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`. internal struct CLDNJSONEncoding: CLDNParameterEncoding { // MARK: Properties /// Returns a `JSONEncoding` instance with default writing options. internal static var `default`: CLDNJSONEncoding { return CLDNJSONEncoding() } /// Returns a `JSONEncoding` instance with `.prettyPrinted` writing options. internal static var prettyPrinted: CLDNJSONEncoding { return CLDNJSONEncoding(options: .prettyPrinted) } /// The options for writing the parameters as JSON data. internal let options: JSONSerialization.WritingOptions // MARK: Initialization /// Creates a `CLDNJSONEncoding` instance using the specified options. /// /// - parameter options: The options for writing the parameters as JSON data. /// /// - returns: The new `CLDNJSONEncoding` instance. internal init(options: JSONSerialization.WritingOptions = []) { self.options = options } // MARK: Encoding /// Creates a URL request by encoding parameters and applying them onto an existing request. /// /// - parameter urlRequest: The request to have parameters applied. /// - parameter parameters: The parameters to apply. /// /// - throws: An `Error` if the encoding process encounters an error. /// /// - returns: The encoded request. internal func CLDN_Encode(_ urlRequest: CLDNURLRequestConvertible, with parameters: CLDNParameters?) throws -> URLRequest { var urlRequest = try urlRequest.CLDN_AsURLRequest() guard let parameters = parameters else { return urlRequest } do { let data = try JSONSerialization.data(withJSONObject: parameters, options: options) if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") } urlRequest.httpBody = data } catch { throw CLDNError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) } return urlRequest } /// Creates a URL request by encoding the JSON object and setting the resulting data on the HTTP body. /// /// - parameter urlRequest: The request to apply the JSON object to. /// - parameter jsonObject: The JSON object to apply to the request. /// /// - throws: An `Error` if the encoding process encounters an error. /// /// - returns: The encoded request. internal func encode(_ urlRequest: CLDNURLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest { var urlRequest = try urlRequest.CLDN_AsURLRequest() guard let jsonObject = jsonObject else { return urlRequest } do { let data = try JSONSerialization.data(withJSONObject: jsonObject, options: options) if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") } urlRequest.httpBody = data } catch { throw CLDNError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) } return urlRequest } } // MARK: - extension NSNumber { fileprivate var isBool: Bool { return CFBooleanGetTypeID() == CFGetTypeID(self) } } ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNRequest.swift ================================================ // // CLDNRequest.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation /// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary. internal protocol CLDNRequestAdapter { /// Inspects and adapts the specified `URLRequest` in some manner if necessary and returns the result. /// /// - parameter urlRequest: The URL request to adapt. /// /// - throws: An `Error` if the adaptation encounters an error. /// /// - returns: The adapted `URLRequest`. func CLDN_Adapt(_ urlRequest: URLRequest) throws -> URLRequest } // MARK: - /// A closure executed when the `CLDNRequestRetrier` determines whether a `CLDNRequest` should be retried or not. internal typealias CLDNRequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) -> Void /// A type that determines whether a request should be retried after being executed by the specified session manager /// and encountering an error. internal protocol CLDNRequestRetrier { /// Determines whether the `CLDNRequest` should be retried by calling the `completion` closure. /// /// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs /// to be retried. The one requirement is that the completion closure is called to ensure the request is properly /// cleaned up after. /// /// - parameter manager: The session manager the request was executed on. /// - parameter request: The request that failed due to the encountered error. /// - parameter error: The error encountered when executing the request. /// - parameter completion: The completion closure to be executed when retry decision has been determined. func CLDN_Should(_ manager: CLDNSessionManager, retry request: CLDNRequest, with error: Error, completion: @escaping CLDNRequestRetryCompletion) } // MARK: - protocol CLDNTaskConvertible { func CLDN_Task(session: URLSession, adapter: CLDNRequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask } /// A dictionary of headers to apply to a `URLRequest`. internal typealias CLDNHTTPHeaders = [String: String] // MARK: - /// Responsible for sending a request and receiving the response and associated data from the server, as well as /// managing its underlying `URLSessionTask`. internal class CLDNRequest { // MARK: Helper Types /// A closure executed when monitoring upload or download progress of a request. internal typealias ProgressHandler = (Progress) -> Void enum RequestTask { case data(CLDNTaskConvertible?, URLSessionTask?) case upload(CLDNTaskConvertible?, URLSessionTask?) } // MARK: Properties /// The delegate for the underlying task. internal var delegate: CLDNTaskDelegate { get { taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() } return taskDelegate } set { taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() } taskDelegate = newValue } } /// The underlying task. internal var task: URLSessionTask? { return delegate.task } /// The session belonging to the underlying task. internal let session: URLSession /// The request sent or to be sent to the server. internal var request: URLRequest? { return task?.originalRequest } /// The response received from the server, if any. internal var response: HTTPURLResponse? { return task?.response as? HTTPURLResponse } /// The number of times the request has been retried. internal var retryCount: UInt = 0 let originalTask: CLDNTaskConvertible? var startTime: CFAbsoluteTime? var endTime: CFAbsoluteTime? var validations: [() -> Void] = [] private var taskDelegate: CLDNTaskDelegate private var taskDelegateLock = NSLock() // MARK: Lifecycle init(session: URLSession, requestTask: RequestTask, error: Error? = nil) { self.session = session switch requestTask { case .data(let originalTask, let task): taskDelegate = CLDNDataTaskDelegate(task: task) self.originalTask = originalTask case .upload(let originalTask, let task): taskDelegate = CLDNUploadTaskDelegate(task: task) self.originalTask = originalTask } delegate.error = error delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() } } // MARK: Authentication /// Associates an HTTP Basic credential with the request. /// /// - parameter user: The user. /// - parameter password: The password. /// - parameter persistence: The URL credential persistence. `.ForSession` by default. /// /// - returns: The request. @discardableResult internal func authenticate( user: String, password: String, persistence: URLCredential.Persistence = .forSession) -> Self { let credential = URLCredential(user: user, password: password, persistence: persistence) return authenticate(usingCredential: credential) } /// Associates a specified credential with the request. /// /// - parameter credential: The credential. /// /// - returns: The request. @discardableResult internal func authenticate(usingCredential credential: URLCredential) -> Self { delegate.credential = credential return self } /// Returns a base64 encoded basic authentication credential as an authorization header tuple. /// /// - parameter user: The user. /// - parameter password: The password. /// /// - returns: A tuple with Authorization header and credential value if encoding succeeds, `nil` otherwise. internal class func authorizationHeader(user: String, password: String) -> (key: String, value: String)? { guard let data = "\(user):\(password)".data(using: .utf8) else { return nil } let credential = data.base64EncodedString(options: []) return (key: "Authorization", value: "Basic \(credential)") } // MARK: State /// Resumes the request. internal func resume() { guard let task = task else { delegate.queue.isSuspended = false ; return } if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() } task.resume() // NotificationCenter.default.post( // name: Notification.Name.Task.DidResume, // object: self, // userInfo: [Notification.Key.Task: task] // ) } /// Suspends the request. internal func suspend() { guard let task = task else { return } task.suspend() } /// Cancels the request. internal func cancel() { guard let task = task else { return } task.cancel() } } // MARK: - CustomStringConvertible extension CLDNRequest: CustomStringConvertible { /// The textual representation used when written to an output stream, which includes the HTTP method and URL, as /// well as the response status code if a response has been received. internal var description: String { var components: [String] = [] if let HTTPMethod = request?.httpMethod { components.append(HTTPMethod) } if let urlString = request?.url?.absoluteString { components.append(urlString) } if let response = response { components.append("(\(response.statusCode))") } return components.joined(separator: " ") } } // MARK: - CustomDebugStringConvertible extension CLDNRequest: CustomDebugStringConvertible { /// The textual representation used when written to an output stream, in the form of a cURL command. internal var debugDescription: String { return cURLRepresentation() } func cURLRepresentation() -> String { var components = ["$ curl -v"] guard let request = self.request, let url = request.url, let host = url.host else { return "$ curl command could not be created" } if let httpMethod = request.httpMethod, httpMethod != "GET" { components.append("-X \(httpMethod)") } if let credentialStorage = self.session.configuration.urlCredentialStorage { let protectionSpace = URLProtectionSpace( host: host, port: url.port ?? 0, protocol: url.scheme, realm: host, authenticationMethod: NSURLAuthenticationMethodHTTPBasic ) if let credentials = credentialStorage.credentials(for: protectionSpace)?.values { for credential in credentials { guard let user = credential.user, let password = credential.password else { continue } components.append("-u \(user):\(password)") } } else { if let credential = delegate.credential, let user = credential.user, let password = credential.password { components.append("-u \(user):\(password)") } } } if session.configuration.httpShouldSetCookies { if let cookieStorage = session.configuration.httpCookieStorage, let cookies = cookieStorage.cookies(for: url), !cookies.isEmpty { let string = cookies.reduce("") { $0 + "\($1.name)=\($1.value);" } #if swift(>=3.2) components.append("-b \"\(string[.. URLSessionTask { do { let urlRequest = try self.urlRequest.adapt(using: adapter) return queue.sync { session.dataTask(with: urlRequest) } } catch { throw AdaptError(error: error) } } } // MARK: Properties /// The request sent or to be sent to the server. internal override var request: URLRequest? { if let request = super.request { return request } if let requestable = originalTask as? Requestable { return requestable.urlRequest } return nil } /// The progress of fetching the response data from the server for the request. internal var progress: Progress { return dataDelegate.progress } var dataDelegate: CLDNDataTaskDelegate { return delegate as! CLDNDataTaskDelegate } // MARK: Stream /// Sets a closure to be called periodically during the lifecycle of the request as data is read from the server. /// /// This closure returns the bytes most recently received from the server, not including data from previous calls. /// If this closure is set, data will only be available within this closure, and will not be saved elsewhere. It is /// also important to note that the server data in any `Response` object will be `nil`. /// /// - parameter closure: The code to be executed periodically during the lifecycle of the request. /// /// - returns: The request. @discardableResult internal func stream(closure: ((Data) -> Void)? = nil) -> Self { dataDelegate.dataStream = closure return self } // MARK: Progress /// Sets a closure to be called periodically during the lifecycle of the `CLDNRequest` as data is read from the server. /// /// - parameter queue: The dispatch queue to execute the closure on. /// - parameter closure: The code to be executed periodically as data is read from the server. /// /// - returns: The request. @discardableResult internal func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self { dataDelegate.progressHandler = (closure, queue) return self } } // MARK: - /// Specific type of `CLDNRequest` that manages an underlying `URLSessionUploadTask`. internal class CLDNUploadRequest: CLDNDataRequest { // MARK: Helper Types enum Uploadable: CLDNTaskConvertible { case data(Data, URLRequest) case file(URL, URLRequest) case stream(InputStream, URLRequest) func CLDN_Task(session: URLSession, adapter: CLDNRequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask { do { let task: URLSessionTask switch self { case let .data(data, urlRequest): let urlRequest = try urlRequest.adapt(using: adapter) task = queue.sync { session.uploadTask(with: urlRequest, from: data) } case let .file(url, urlRequest): let urlRequest = try urlRequest.adapt(using: adapter) task = queue.sync { session.uploadTask(with: urlRequest, fromFile: url) } case let .stream(_, urlRequest): let urlRequest = try urlRequest.adapt(using: adapter) task = queue.sync { session.uploadTask(withStreamedRequest: urlRequest) } } return task } catch { throw AdaptError(error: error) } } } // MARK: Properties /// The request sent or to be sent to the server. internal override var request: URLRequest? { if let request = super.request { return request } guard let uploadable = originalTask as? Uploadable else { return nil } switch uploadable { case .data(_, let urlRequest), .file(_, let urlRequest), .stream(_, let urlRequest): return urlRequest } } /// The progress of uploading the payload to the server for the upload request. internal var uploadProgress: Progress { return uploadDelegate.uploadProgress } var uploadDelegate: CLDNUploadTaskDelegate { return delegate as! CLDNUploadTaskDelegate } // MARK: Upload Progress /// Sets a closure to be called periodically during the lifecycle of the `CLDNUploadRequest` as data is sent to /// the server. /// /// After the data is sent to the server, the `progress(queue:closure:)` APIs can be used to monitor the progress /// of data being read from the server. /// /// - parameter queue: The dispatch queue to execute the closure on. /// - parameter closure: The code to be executed periodically as data is sent to the server. /// /// - returns: The request. @discardableResult internal func uploadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self { uploadDelegate.uploadProgressHandler = (closure, queue) return self } } ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNResponse.swift ================================================ // // CLDNResponse.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation /// Used to store all data associated with an non-serialized response of a data or upload request. internal struct CLDNDefaultDataResponse { /// The URL request sent to the server. internal let request: URLRequest? /// The server's response to the URL request. internal let response: HTTPURLResponse? /// The data returned by the server. internal let data: Data? /// The error encountered while executing or validating the request. internal let error: Error? /// The timeline of the complete lifecycle of the request. internal let timeline: CLDNTimeline var _metrics: AnyObject? /// Creates a `CLDNDefaultDataResponse` instance from the specified parameters. /// /// - Parameters: /// - request: The URL request sent to the server. /// - response: The server's response to the URL request. /// - data: The data returned by the server. /// - error: The error encountered while executing or validating the request. /// - timeline: The timeline of the complete lifecycle of the request. `CLDNTimeline()` by default. /// - metrics: The task metrics containing the request / response statistics. `nil` by default. internal init( request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?, timeline: CLDNTimeline = CLDNTimeline(), metrics: AnyObject? = nil) { self.request = request self.response = response self.data = data self.error = error self.timeline = timeline } } // MARK: - /// Used to store all data associated with a serialized response of a data or upload request. internal struct CLDNDataResponse { /// The URL request sent to the server. internal let request: URLRequest? /// The server's response to the URL request. internal let response: HTTPURLResponse? /// The data returned by the server. internal let data: Data? /// The result of response serialization. internal let result: CLDNResult /// The timeline of the complete lifecycle of the request. internal let timeline: CLDNTimeline /// Returns the associated value of the result if it is a success, `nil` otherwise. internal var value: Value? { return result.value } /// Returns the associated error value if the result if it is a failure, `nil` otherwise. internal var error: Error? { return result.error } var _metrics: AnyObject? /// Creates a `CLDNDataResponse` instance with the specified parameters derived from response serialization. /// /// - parameter request: The URL request sent to the server. /// - parameter response: The server's response to the URL request. /// - parameter data: The data returned by the server. /// - parameter result: The result of response serialization. /// - parameter timeline: The timeline of the complete lifecycle of the `CLDNRequest`. Defaults to `CLDNTimeline()`. /// /// - returns: The new `CLDNDataResponse` instance. internal init( request: URLRequest?, response: HTTPURLResponse?, data: Data?, result: CLDNResult, timeline: CLDNTimeline = CLDNTimeline()) { self.request = request self.response = response self.data = data self.result = result self.timeline = timeline } } // MARK: - extension CLDNDataResponse: CustomStringConvertible, CustomDebugStringConvertible { /// The textual representation used when written to an output stream, which includes whether the result was a /// success or failure. internal var description: String { return result.debugDescription } /// The debug textual representation used when written to an output stream, which includes the URL request, the URL /// response, the server data, the response serialization result and the timeline. internal var debugDescription: String { let requestDescription = request.map { "\($0.httpMethod ?? "GET") \($0)"} ?? "nil" let requestBody = request?.httpBody.map { String(decoding: $0, as: UTF8.self) } ?? "None" let responseDescription = response.map { "\($0)" } ?? "nil" let responseBody = data.map { String(decoding: $0, as: UTF8.self) } ?? "None" return """ [Request]: \(requestDescription) [Request Body]: \n\(requestBody) [Response]: \(responseDescription) [Response Body]: \n\(responseBody) [Result]: \(result) [Timeline]: \(timeline.debugDescription) """ } } // MARK: - protocol CLDNResponse { /// The task metrics containing the request / response statistics. var _metrics: AnyObject? { get set } mutating func CLDN_Add(_ metrics: AnyObject?) } extension CLDNResponse { mutating func CLDN_Add(_ metrics: AnyObject?) { #if !os(watchOS) guard #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) else { return } guard let metrics = metrics as? URLSessionTaskMetrics else { return } _metrics = metrics #endif } } // MARK: - @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) extension CLDNDefaultDataResponse: CLDNResponse { #if !os(watchOS) /// The task metrics containing the request / response statistics. internal var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics } #endif } @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) extension CLDNDataResponse: CLDNResponse { #if !os(watchOS) /// The task metrics containing the request / response statistics. internal var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics } #endif } ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNResponseSerialization.swift ================================================ // // CLDNResponseSerialization.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation /// The type in which all data response serializers must conform to in order to serialize a response. internal protocol CLDNDataResponseSerializerProtocol { /// The type of serialized object to be created by this `DataResponseSerializerType`. associatedtype SerializedObject /// A closure used by response handlers that takes a request, response, data and error and returns a result. var serializeResponse: (URLRequest?, HTTPURLResponse?, Data?, Error?) -> CLDNResult { get } } // MARK: - /// A generic `DataResponseSerializerType` used to serialize a request, response, and data into a serialized object. internal struct CLDNDataResponseSerializer: CLDNDataResponseSerializerProtocol { /// The type of serialized object to be created by this `CLDNDataResponseSerializer`. internal typealias SerializedObject = Value /// A closure used by response handlers that takes a request, response, data and error and returns a result. internal var serializeResponse: (URLRequest?, HTTPURLResponse?, Data?, Error?) -> CLDNResult /// Initializes the `ResponseSerializer` instance with the given serialize response closure. /// /// - parameter serializeResponse: The closure used to serialize the response. /// /// - returns: The new generic response serializer instance. internal init(serializeResponse: @escaping (URLRequest?, HTTPURLResponse?, Data?, Error?) -> CLDNResult) { self.serializeResponse = serializeResponse } } // MARK: - Timeline extension CLDNRequest { var timeline: CLDNTimeline { let requestStartTime = self.startTime ?? CFAbsoluteTimeGetCurrent() let requestCompletedTime = self.endTime ?? CFAbsoluteTimeGetCurrent() let initialResponseTime = self.delegate.initialResponseTime ?? requestCompletedTime return CLDNTimeline( requestStartTime: requestStartTime, initialResponseTime: initialResponseTime, requestCompletedTime: requestCompletedTime, serializationCompletedTime: CFAbsoluteTimeGetCurrent() ) } } // MARK: - Default extension CLDNDataRequest { /// Adds a handler to be called once the request has finished. /// /// - parameter queue: The queue on which the completion handler is dispatched. /// - parameter completionHandler: The code to be executed once the request has finished. /// /// - returns: The request. @discardableResult internal func response(queue: DispatchQueue? = nil, completionHandler: @escaping (CLDNDefaultDataResponse) -> Void) -> Self { delegate.queue.addOperation { (queue ?? DispatchQueue.main).async { var dataResponse = CLDNDefaultDataResponse( request: self.request, response: self.response, data: self.delegate.data, error: self.delegate.error, timeline: self.timeline ) if #available(iOS 10.0, *) { dataResponse.CLDN_Add(self.delegate.metrics) } completionHandler(dataResponse) } } return self } /// Adds a handler to be called once the request has finished. /// /// - parameter queue: The queue on which the completion handler is dispatched. /// - parameter responseSerializer: The response serializer responsible for serializing the request, response, /// and data. /// - parameter completionHandler: The code to be executed once the request has finished. /// /// - returns: The request. @discardableResult internal func response( queue: DispatchQueue? = nil, responseSerializer: T, completionHandler: @escaping (CLDNDataResponse) -> Void) -> Self { delegate.queue.addOperation { let result = responseSerializer.serializeResponse( self.request, self.response, self.delegate.data, self.delegate.error ) var dataResponse = CLDNDataResponse( request: self.request, response: self.response, data: self.delegate.data, result: result, timeline: self.timeline ) if #available(iOS 10.0, *) { dataResponse.CLDN_Add(self.delegate.metrics) } (queue ?? DispatchQueue.main).async { completionHandler(dataResponse) } } return self } } // MARK: - Data extension CLDNRequest { /// Returns a result data type that contains the response data as-is. /// /// - parameter response: The response from the server. /// - parameter data: The data returned from the server. /// - parameter error: The error already encountered if it exists. /// /// - returns: The result data type. internal static func serializeResponseData(response: HTTPURLResponse?, data: Data?, error: Error?) -> CLDNResult { guard error == nil else { return .failure(error!) } if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(Data()) } guard let validData = data else { if let statusCode = response?.statusCode, let httpStatusCode = HTTPStatusCode(rawValue: statusCode), httpStatusCode.isError { return .failure(CLDError.error(code: statusCode, userInfo: ["message": httpStatusCode.localizedReason])) } return .failure(CLDNError.responseSerializationFailed(reason: .inputDataNil)) } return .success(validData) } } extension CLDNDataRequest { /// Creates a response serializer that returns the associated data as-is. /// /// - returns: A data response serializer. internal static func dataResponseSerializer() -> CLDNDataResponseSerializer { return CLDNDataResponseSerializer { _, response, data, error in return CLDNRequest.serializeResponseData(response: response, data: data, error: error) } } /// Adds a handler to be called once the request has finished. /// /// - parameter completionHandler: The code to be executed once the request has finished. /// /// - returns: The request. @discardableResult internal func responseData( queue: DispatchQueue? = nil, completionHandler: @escaping (CLDNDataResponse) -> Void) -> Self { return response( queue: queue, responseSerializer: CLDNDataRequest.dataResponseSerializer(), completionHandler: completionHandler ) } } // MARK: - String extension CLDNRequest { /// Returns a result string type initialized from the response data with the specified string encoding. /// /// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the server /// response, falling back to the default HTTP default character set, ISO-8859-1. /// - parameter response: The response from the server. /// - parameter data: The data returned from the server. /// - parameter error: The error already encountered if it exists. /// /// - returns: The result data type. internal static func serializeResponseString( encoding: String.Encoding?, response: HTTPURLResponse?, data: Data?, error: Error?) -> CLDNResult { guard error == nil else { return .failure(error!) } if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success("") } guard let validData = data else { return .failure(CLDNError.responseSerializationFailed(reason: .inputDataNil)) } var convertedEncoding = encoding if let encodingName = response?.textEncodingName as CFString?, convertedEncoding == nil { convertedEncoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding( CFStringConvertIANACharSetNameToEncoding(encodingName)) ) } let actualEncoding = convertedEncoding ?? .isoLatin1 if let string = String(data: validData, encoding: actualEncoding) { return .success(string) } else { if let statusCode = response?.statusCode, let httpStatusCode = HTTPStatusCode(rawValue: statusCode), httpStatusCode.isError { return .failure(CLDError.error(code: statusCode, userInfo: ["message": httpStatusCode.localizedReason])) } return .failure(CLDNError.responseSerializationFailed(reason: .stringSerializationFailed(encoding: actualEncoding))) } } } extension CLDNDataRequest { /// Creates a response serializer that returns a result string type initialized from the response data with /// the specified string encoding. /// /// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the server /// response, falling back to the default HTTP default character set, ISO-8859-1. /// /// - returns: A string response serializer. internal static func stringResponseSerializer(encoding: String.Encoding? = nil) -> CLDNDataResponseSerializer { return CLDNDataResponseSerializer { _, response, data, error in return CLDNRequest.serializeResponseString(encoding: encoding, response: response, data: data, error: error) } } /// Adds a handler to be called once the request has finished. /// /// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the /// server response, falling back to the default HTTP default character set, /// ISO-8859-1. /// - parameter completionHandler: A closure to be executed once the request has finished. /// /// - returns: The request. @discardableResult internal func responseString( queue: DispatchQueue? = nil, encoding: String.Encoding? = nil, completionHandler: @escaping (CLDNDataResponse) -> Void) -> Self { return response( queue: queue, responseSerializer: CLDNDataRequest.stringResponseSerializer(encoding: encoding), completionHandler: completionHandler ) } } // MARK: - JSON extension CLDNRequest { /// Returns a JSON object contained in a result type constructed from the response data using `JSONSerialization` /// with the specified reading options. /// /// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`. /// - parameter response: The response from the server. /// - parameter data: The data returned from the server. /// - parameter error: The error already encountered if it exists. /// /// - returns: The result data type. internal static func serializeResponseJSON( options: JSONSerialization.ReadingOptions, response: HTTPURLResponse?, data: Data?, error: Error?) -> CLDNResult { guard error == nil else { return .failure(error!) } if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(NSNull()) } guard let validData = data, validData.count > 0 else { return .failure(CLDNError.responseSerializationFailed(reason: .inputDataNilOrZeroLength)) } do { let json = try JSONSerialization.jsonObject(with: validData, options: options) return .success(json) } catch { if let statusCode = response?.statusCode, let httpStatusCode = HTTPStatusCode(rawValue: statusCode), httpStatusCode.isError { return .failure(CLDError.error(code: statusCode, userInfo: ["message": httpStatusCode.localizedReason])) } return .failure(CLDNError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error))) } } } extension CLDNDataRequest { /// Creates a response serializer that returns a JSON object result type constructed from the response data using /// `JSONSerialization` with the specified reading options. /// /// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`. /// /// - returns: A JSON object response serializer. internal static func jsonResponseSerializer( options: JSONSerialization.ReadingOptions = .allowFragments) -> CLDNDataResponseSerializer { return CLDNDataResponseSerializer { _, response, data, error in return CLDNRequest.serializeResponseJSON(options: options, response: response, data: data, error: error) } } /// Adds a handler to be called once the request has finished. /// /// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`. /// - parameter completionHandler: A closure to be executed once the request has finished. /// /// - returns: The request. @discardableResult internal func responseJSON( queue: DispatchQueue? = nil, options: JSONSerialization.ReadingOptions = .allowFragments, completionHandler: @escaping (CLDNDataResponse) -> Void) -> Self { return response( queue: queue, responseSerializer: CLDNDataRequest.jsonResponseSerializer(options: options), completionHandler: completionHandler ) } } /// A set of HTTP response status code that do not contain response data. private let emptyDataStatusCodes: Set = [204, 205] ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNResult.swift ================================================ // // CLDNResult.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation /// Used to represent whether a request was successful or encountered an error. /// /// - success: The request and all post processing operations were successful resulting in the serialization of the /// provided associated value. /// /// - failure: The request encountered an error resulting in a failure. The associated values are the original data /// provided by the server as well as the error that caused the failure. internal enum CLDNResult { case success(Value) case failure(Error) /// Returns `true` if the result is a success, `false` otherwise. internal var isSuccess: Bool { switch self { case .success: return true case .failure: return false } } /// Returns `true` if the result is a failure, `false` otherwise. internal var isFailure: Bool { return !isSuccess } /// Returns the associated value if the result is a success, `nil` otherwise. internal var value: Value? { switch self { case .success(let value): return value case .failure: return nil } } /// Returns the associated error value if the result is a failure, `nil` otherwise. internal var error: Error? { switch self { case .success: return nil case .failure(let error): return error } } } // MARK: - CustomStringConvertible extension CLDNResult: CustomStringConvertible { /// The textual representation used when written to an output stream, which includes whether the result was a /// success or failure. internal var description: String { switch self { case .success: return "SUCCESS" case .failure: return "FAILURE" } } } // MARK: - CustomDebugStringConvertible extension CLDNResult: CustomDebugStringConvertible { /// The debug textual representation used when written to an output stream, which includes whether the result was a /// success or failure in addition to the value or error. internal var debugDescription: String { switch self { case .success(let value): return "SUCCESS: \(value)" case .failure(let error): return "FAILURE: \(error)" } } } // MARK: - Functional APIs extension CLDNResult { /// Creates a `CLDNResult` instance from the result of a closure. /// /// A failure result is created when the closure throws, and a success result is created when the closure /// succeeds without throwing an error. /// /// func someString() throws -> String { ... } /// /// let result = CLDNResult(value: { /// return try someString() /// }) /// /// // The type of result is CLDNResult /// /// The trailing closure syntax is also supported: /// /// let result = CLDNResult { try someString() } /// /// - parameter value: The closure to execute and create the result for. internal init(value: () throws -> Value) { do { self = try .success(value()) } catch { self = .failure(error) } } /// Returns the success value, or throws the failure error. /// /// let possibleString: Result = .success("success") /// try print(possibleString.unwrap()) /// // Prints "success" /// /// let noString: CLDNResult = .failure(error) /// try print(noString.unwrap()) /// // Throws error internal func unwrap() throws -> Value { switch self { case .success(let value): return value case .failure(let error): throw error } } /// Evaluates the specified closure when the `CLDNResult` is a success, passing the unwrapped value as a parameter. /// /// Use the `map` method with a closure that does not throw. For example: /// /// let possibleData: CLDNResult = .success(Data()) /// let possibleInt = possibleData.map { $0.count } /// try print(possibleInt.unwrap()) /// // Prints "0" /// /// let noData: CLDNResult = .failure(error) /// let noInt = noData.map { $0.count } /// try print(noInt.unwrap()) /// // Throws error /// /// - parameter transform: A closure that takes the success value of the `CLDNResult` instance. /// /// - returns: A `CLDNResult` containing the result of the given closure. If this instance is a failure, returns the /// same failure. internal func map(_ transform: (Value) -> T) -> CLDNResult { switch self { case .success(let value): return .success(transform(value)) case .failure(let error): return .failure(error) } } /// Evaluates the specified closure when the `CLDNResult` is a success, passing the unwrapped value as a parameter. /// /// Use the `flatMap` method with a closure that may throw an error. For example: /// /// let possibleData: CLDNResult = .success(Data(...)) /// let possibleObject = possibleData.flatMap { /// try JSONSerialization.jsonObject(with: $0) /// } /// /// - parameter transform: A closure that takes the success value of the instance. /// /// - returns: A `CLDNResult` containing the result of the given closure. If this instance is a failure, returns the /// same failure. internal func flatMap(_ transform: (Value) throws -> T) -> CLDNResult { switch self { case .success(let value): do { return try .success(transform(value)) } catch { return .failure(error) } case .failure(let error): return .failure(error) } } /// Evaluates the specified closure when the `CLDNResult` is a failure, passing the unwrapped error as a parameter. /// /// Use the `mapError` function with a closure that does not throw. For example: /// /// let possibleData: CLDNResult = .failure(someError) /// let withMyError: CLDNResult = possibleData.mapError { MyError.error($0) } /// /// - Parameter transform: A closure that takes the error of the instance. /// - Returns: A `CLDNResult` instance containing the result of the transform. If this instance is a success, returns /// the same instance. internal func mapError(_ transform: (Error) -> T) -> CLDNResult { switch self { case .failure(let error): return .failure(transform(error)) case .success: return self } } /// Evaluates the specified closure when the `CLDNResult` is a failure, passing the unwrapped error as a parameter. /// /// Use the `flatMapError` function with a closure that may throw an error. For example: /// /// let possibleData: CLDNResult = .success(Data(...)) /// let possibleObject = possibleData.flatMapError { /// try someFailableFunction(taking: $0) /// } /// /// - Parameter transform: A throwing closure that takes the error of the instance. /// /// - Returns: A `CLDNResult` instance containing the result of the transform. If this instance is a success, returns /// the same instance. internal func flatMapError(_ transform: (Error) throws -> T) -> CLDNResult { switch self { case .failure(let error): do { return try .failure(transform(error)) } catch { return .failure(error) } case .success: return self } } /// Evaluates the specified closure when the `CLDNResult` is a success, passing the unwrapped value as a parameter. /// /// Use the `withValue` function to evaluate the passed closure without modifying the `CLDNResult` instance. /// /// - Parameter closure: A closure that takes the success value of this instance. /// - Returns: This `CLDNResult` instance, unmodified. @discardableResult internal func withValue(_ closure: (Value) throws -> Void) rethrows -> CLDNResult { if case let .success(value) = self { try closure(value) } return self } /// Evaluates the specified closure when the `CLDNResult` is a failure, passing the unwrapped error as a parameter. /// /// Use the `withError` function to evaluate the passed closure without modifying the `CLDNResult` instance. /// /// - Parameter closure: A closure that takes the success value of this instance. /// - Returns: This `CLDNResult` instance, unmodified. @discardableResult internal func withError(_ closure: (Error) throws -> Void) rethrows -> CLDNResult { if case let .failure(error) = self { try closure(error) } return self } /// Evaluates the specified closure when the `CLDNResult` is a success. /// /// Use the `ifSuccess` function to evaluate the passed closure without modifying the `CLDNResult` instance. /// /// - Parameter closure: A `Void` closure. /// - Returns: This `CLDNResult` instance, unmodified. @discardableResult internal func ifSuccess(_ closure: () throws -> Void) rethrows -> CLDNResult { if isSuccess { try closure() } return self } /// Evaluates the specified closure when the `CLDNResult` is a failure. /// /// Use the `ifFailure` function to evaluate the passed closure without modifying the `CLDNResult` instance. /// /// - Parameter closure: A `Void` closure. /// - Returns: This `CLDNResult` instance, unmodified. @discardableResult internal func ifFailure(_ closure: () throws -> Void) rethrows -> CLDNResult { if isFailure { try closure() } return self } } ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNSessionDelegate.swift ================================================ // // CLDNSessionDelegate.swift // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation /// Responsible for handling all delegate callbacks for the underlying session. internal class CLDNSessionDelegate: NSObject { // MARK: URLSessionDelegate Overrides /// Overrides default behavior for URLSessionDelegate method `urlSession(_:didBecomeInvalidWithError:)`. internal var sessionDidBecomeInvalidWithError: ((URLSession, Error?) -> Void)? /// Overrides default behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)`. internal var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))? /// Overrides all behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)` and requires the caller to call the `completionHandler`. internal var sessionDidReceiveChallengeWithCompletion: ((URLSession, URLAuthenticationChallenge, @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)? /// Overrides default behavior for URLSessionDelegate method `urlSessionDidFinishEvents(forBackgroundURLSession:)`. internal var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)? // MARK: URLSessionTaskDelegate Overrides /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`. internal var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)? /// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` and /// requires the caller to call the `completionHandler`. internal var taskWillPerformHTTPRedirectionWithCompletion: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void)? /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didReceive:completionHandler:)`. internal var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))? /// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:didReceive:completionHandler:)` and /// requires the caller to call the `completionHandler`. internal var taskDidReceiveChallengeWithCompletion: ((URLSession, URLSessionTask, URLAuthenticationChallenge, @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)? /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:needNewBodyStream:)`. internal var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)? /// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:needNewBodyStream:)` and /// requires the caller to call the `completionHandler`. internal var taskNeedNewBodyStreamWithCompletion: ((URLSession, URLSessionTask, @escaping (InputStream?) -> Void) -> Void)? /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)`. internal var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)? /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didCompleteWithError:)`. internal var taskDidComplete: ((URLSession, URLSessionTask, Error?) -> Void)? // MARK: URLSessionDataDelegate Overrides /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:completionHandler:)`. internal var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)? /// Overrides all behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:completionHandler:)` and /// requires caller to call the `completionHandler`. internal var dataTaskDidReceiveResponseWithCompletion: ((URLSession, URLSessionDataTask, URLResponse, @escaping (URLSession.ResponseDisposition) -> Void) -> Void)? /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didBecome:)`. internal var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)? /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:)`. internal var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)? /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)`. internal var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)? /// Overrides all behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)` and /// requires caller to call the `completionHandler`. internal var dataTaskWillCacheResponseWithCompletion: ((URLSession, URLSessionDataTask, CachedURLResponse, @escaping (CachedURLResponse?) -> Void) -> Void)? // MARK: URLSessionDownloadDelegate Overrides /// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didFinishDownloadingTo:)`. internal var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> Void)? /// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)`. internal var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? /// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)`. internal var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)? // MARK: URLSessionStreamDelegate Overrides // MARK: Properties var retrier: CLDNRequestRetrier? weak var sessionManager: CLDNSessionManager? var requests: [Int: CLDNRequest] = [:] private let lock = NSLock() /// Access the task delegate for the specified task in a thread-safe manner. internal subscript(task: URLSessionTask) -> CLDNRequest? { get { lock.lock() ; defer { lock.unlock() } return requests[task.taskIdentifier] } set { lock.lock() ; defer { lock.unlock() } requests[task.taskIdentifier] = newValue } } // MARK: Lifecycle /// Initializes the `CLDNSessionDelegate` instance. /// /// - returns: The new `CLDNSessionDelegate` instance. internal override init() { super.init() } // MARK: NSObject Overrides /// Returns a `Bool` indicating whether the `CLDNSessionDelegate` implements or inherits a method that can respond /// to a specified message. /// /// - parameter selector: A selector that identifies a message. /// /// - returns: `true` if the receiver implements or inherits a method that can respond to selector, otherwise `false`. internal override func responds(to selector: Selector) -> Bool { #if !os(macOS) if selector == #selector(URLSessionDelegate.urlSessionDidFinishEvents(forBackgroundURLSession:)) { return sessionDidFinishEventsForBackgroundURLSession != nil } #endif switch selector { case #selector(URLSessionDelegate.urlSession(_:didBecomeInvalidWithError:)): return sessionDidBecomeInvalidWithError != nil case #selector(URLSessionDelegate.urlSession(_:didReceive:completionHandler:)): return (sessionDidReceiveChallenge != nil || sessionDidReceiveChallengeWithCompletion != nil) case #selector(URLSessionTaskDelegate.urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)): return (taskWillPerformHTTPRedirection != nil || taskWillPerformHTTPRedirectionWithCompletion != nil) case #selector(URLSessionDataDelegate.urlSession(_:dataTask:didReceive:completionHandler:)): return (dataTaskDidReceiveResponse != nil || dataTaskDidReceiveResponseWithCompletion != nil) default: return type(of: self).instancesRespond(to: selector) } } } // MARK: - URLSessionDelegate extension CLDNSessionDelegate: URLSessionDelegate { /// Tells the delegate that the session has been invalidated. /// /// - parameter session: The session object that was invalidated. /// - parameter error: The error that caused invalidation, or nil if the invalidation was explicit. internal func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { sessionDidBecomeInvalidWithError?(session, error) } /// Requests credentials from the delegate in response to a session-level authentication request from the /// remote server. /// /// - parameter session: The session containing the task that requested authentication. /// - parameter challenge: An object that contains the request for authentication. /// - parameter completionHandler: A handler that your delegate method must call providing the disposition /// and credential. internal func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { guard sessionDidReceiveChallengeWithCompletion == nil else { sessionDidReceiveChallengeWithCompletion?(session, challenge, completionHandler) return } var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling var credential: URLCredential? if let sessionDidReceiveChallenge = sessionDidReceiveChallenge { (disposition, credential) = sessionDidReceiveChallenge(session, challenge) } completionHandler(disposition, credential) } #if !os(macOS) /// Tells the delegate that all messages enqueued for a session have been delivered. /// /// - parameter session: The session that no longer has any outstanding requests. internal func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { sessionDidFinishEventsForBackgroundURLSession?(session) } #endif } // MARK: - URLSessionTaskDelegate extension CLDNSessionDelegate: URLSessionTaskDelegate { /// Tells the delegate that the remote server requested an HTTP redirect. /// /// - parameter session: The session containing the task whose request resulted in a redirect. /// - parameter task: The task whose request resulted in a redirect. /// - parameter response: An object containing the server’s response to the original request. /// - parameter request: A URL request object filled out with the new location. /// - parameter completionHandler: A closure that your handler should call with either the value of the request /// parameter, a modified URL request object, or NULL to refuse the redirect and /// return the body of the redirect response. internal func urlSession( _ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { guard taskWillPerformHTTPRedirectionWithCompletion == nil else { taskWillPerformHTTPRedirectionWithCompletion?(session, task, response, request, completionHandler) return } var redirectRequest: URLRequest? = request if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection { redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request) } completionHandler(redirectRequest) } /// Requests credentials from the delegate in response to an authentication request from the remote server. /// /// - parameter session: The session containing the task whose request requires authentication. /// - parameter task: The task whose request requires authentication. /// - parameter challenge: An object that contains the request for authentication. /// - parameter completionHandler: A handler that your delegate method must call providing the disposition /// and credential. internal func urlSession( _ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { guard taskDidReceiveChallengeWithCompletion == nil else { taskDidReceiveChallengeWithCompletion?(session, task, challenge, completionHandler) return } if let taskDidReceiveChallenge = taskDidReceiveChallenge { let result = taskDidReceiveChallenge(session, task, challenge) completionHandler(result.0, result.1) } else if let delegate = self[task]?.delegate { delegate.urlSession( session, task: task, didReceive: challenge, completionHandler: completionHandler ) } else { urlSession(session, didReceive: challenge, completionHandler: completionHandler) } } /// Tells the delegate when a task requires a new request body stream to send to the remote server. /// /// - parameter session: The session containing the task that needs a new body stream. /// - parameter task: The task that needs a new body stream. /// - parameter completionHandler: A completion handler that your delegate method should call with the new body stream. internal func urlSession( _ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) { guard taskNeedNewBodyStreamWithCompletion == nil else { taskNeedNewBodyStreamWithCompletion?(session, task, completionHandler) return } if let taskNeedNewBodyStream = taskNeedNewBodyStream { completionHandler(taskNeedNewBodyStream(session, task)) } else if let delegate = self[task]?.delegate { delegate.urlSession(session, task: task, needNewBodyStream: completionHandler) } } /// Periodically informs the delegate of the progress of sending body content to the server. /// /// - parameter session: The session containing the data task. /// - parameter task: The data task. /// - parameter bytesSent: The number of bytes sent since the last time this delegate method was called. /// - parameter totalBytesSent: The total number of bytes sent so far. /// - parameter totalBytesExpectedToSend: The expected length of the body data. internal func urlSession( _ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { if let taskDidSendBodyData = taskDidSendBodyData { taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend) } else if let delegate = self[task]?.delegate as? CLDNUploadTaskDelegate { delegate.URLSession( session, task: task, didSendBodyData: bytesSent, totalBytesSent: totalBytesSent, totalBytesExpectedToSend: totalBytesExpectedToSend ) } } #if !os(watchOS) /// Tells the delegate that the session finished collecting metrics for the task. /// /// - parameter session: The session collecting the metrics. /// - parameter task: The task whose metrics have been collected. /// - parameter metrics: The collected metrics. @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) @objc(URLSession:task:didFinishCollectingMetrics:) internal func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { self[task]?.delegate.metrics = metrics } #endif /// Tells the delegate that the task finished transferring data. /// /// - parameter session: The session containing the task whose request finished transferring data. /// - parameter task: The task whose request finished transferring data. /// - parameter error: If an error occurred, an error object indicating how the transfer failed, otherwise nil. internal func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { /// Executed after it is determined that the request is not going to be retried let completeTask: (URLSession, URLSessionTask, Error?) -> Void = { [weak self] session, task, error in guard let strongSelf = self else { return } strongSelf.taskDidComplete?(session, task, error) strongSelf[task]?.delegate.urlSession(session, task: task, didCompleteWithError: error) strongSelf[task] = nil } guard let request = self[task], let sessionManager = sessionManager else { completeTask(session, task, error) return } // Run all validations on the request before checking if an error occurred request.validations.forEach { $0() } // Determine whether an error has occurred var error: Error? = error if request.delegate.error != nil { error = request.delegate.error } /// If an error occurred and the retrier is set, asynchronously ask the retrier if the request /// should be retried. Otherwise, complete the task by notifying the task delegate. if let retrier = retrier, let error = error { retrier.CLDN_Should(sessionManager, retry: request, with: error) { [weak self] shouldRetry, timeDelay in guard shouldRetry else { completeTask(session, task, error) ; return } DispatchQueue.CLDNUtility.CLDN_after(timeDelay) { [weak self] in guard let strongSelf = self else { return } let retrySucceeded = strongSelf.sessionManager?.retry(request) ?? false if retrySucceeded, let task = request.task { strongSelf[task] = request return } else { completeTask(session, task, error) } } } } else { completeTask(session, task, error) } } } // MARK: - URLSessionDataDelegate extension CLDNSessionDelegate: URLSessionDataDelegate { /// Tells the delegate that the data task received the initial reply (headers) from the server. /// /// - parameter session: The session containing the data task that received an initial reply. /// - parameter dataTask: The data task that received an initial reply. /// - parameter response: A URL response object populated with headers. /// - parameter completionHandler: A completion handler that your code calls to continue the transfer, passing a /// constant to indicate whether the transfer should continue as a data task or /// should become a download task. internal func urlSession( _ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { guard dataTaskDidReceiveResponseWithCompletion == nil else { dataTaskDidReceiveResponseWithCompletion?(session, dataTask, response, completionHandler) return } var disposition: URLSession.ResponseDisposition = .allow if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse { disposition = dataTaskDidReceiveResponse(session, dataTask, response) } completionHandler(disposition) } /// Tells the delegate that the data task has received some of the expected data. /// /// - parameter session: The session containing the data task that provided data. /// - parameter dataTask: The data task that provided data. /// - parameter data: A data object containing the transferred data. internal func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { if let dataTaskDidReceiveData = dataTaskDidReceiveData { dataTaskDidReceiveData(session, dataTask, data) } else if let delegate = self[dataTask]?.delegate as? CLDNDataTaskDelegate { delegate.urlSession(session, dataTask: dataTask, didReceive: data) } } /// Asks the delegate whether the data (or upload) task should store the response in the cache. /// /// - parameter session: The session containing the data (or upload) task. /// - parameter dataTask: The data (or upload) task. /// - parameter proposedResponse: The default caching behavior. This behavior is determined based on the current /// caching policy and the values of certain received headers, such as the Pragma /// and Cache-Control headers. /// - parameter completionHandler: A block that your handler must call, providing either the original proposed /// response, a modified version of that response, or NULL to prevent caching the /// response. If your delegate implements this method, it must call this completion /// handler; otherwise, your app leaks memory. internal func urlSession( _ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) { guard dataTaskWillCacheResponseWithCompletion == nil else { dataTaskWillCacheResponseWithCompletion?(session, dataTask, proposedResponse, completionHandler) return } if let dataTaskWillCacheResponse = dataTaskWillCacheResponse { completionHandler(dataTaskWillCacheResponse(session, dataTask, proposedResponse)) } else if let delegate = self[dataTask]?.delegate as? CLDNDataTaskDelegate { delegate.urlSession( session, dataTask: dataTask, willCacheResponse: proposedResponse, completionHandler: completionHandler ) } else { completionHandler(proposedResponse) } } } // MARK: - URLSessionDownloadDelegate extension CLDNSessionDelegate: URLSessionDownloadDelegate { /// Tells the delegate that a download task has finished downloading. /// /// - parameter session: The session containing the download task that finished. /// - parameter downloadTask: The download task that finished. /// - parameter location: A file URL for the temporary file. Because the file is temporary, you must either /// internal the file for reading or move it to a permanent location in your app’s sandbox /// container directory before returning from this delegate method. internal func urlSession( _ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { if let downloadTaskDidFinishDownloadingToURL = downloadTaskDidFinishDownloadingToURL { downloadTaskDidFinishDownloadingToURL(session, downloadTask, location) } } /// Periodically informs the delegate about the download’s progress. /// /// - parameter session: The session containing the download task. /// - parameter downloadTask: The download task. /// - parameter bytesWritten: The number of bytes transferred since the last time this delegate /// method was called. /// - parameter totalBytesWritten: The total number of bytes transferred so far. /// - parameter totalBytesExpectedToWrite: The expected length of the file, as provided by the Content-Length /// header. If this header was not provided, the value is /// `NSURLSessionTransferSizeUnknown`. internal func urlSession( _ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { if let downloadTaskDidWriteData = downloadTaskDidWriteData { downloadTaskDidWriteData(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) } } /// Tells the delegate that the download task has resumed downloading. /// /// - parameter session: The session containing the download task that finished. /// - parameter downloadTask: The download task that resumed. See explanation in the discussion. /// - parameter fileOffset: If the file's cache policy or last modified date prevents reuse of the /// existing content, then this value is zero. Otherwise, this value is an /// integer representing the number of bytes on disk that do not need to be /// retrieved again. /// - parameter expectedTotalBytes: The expected length of the file, as provided by the Content-Length header. /// If this header was not provided, the value is NSURLSessionTransferSizeUnknown. internal func urlSession( _ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) { if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset { downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes) } } } ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNSessionManager.swift ================================================ // // CLDNSessionManager.swift // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation /// Responsible for creating and managing `CLDNRequest` objects, as well as their underlying `NSURLSession`. internal class CLDNSessionManager { // MARK: - Helper Types /// Defines whether the `CLDNMultipartFormData` encoding was successful and contains result of the encoding as /// associated values. /// /// - Success: Represents a successful `CLDNMultipartFormData` encoding and contains the new `CLDNUploadRequest` along with /// streaming information. /// - Failure: Used to represent a failure in the `CLDNMultipartFormData` encoding and also contains the encoding /// error. internal enum MultipartFormDataEncodingResult { case success(request: CLDNUploadRequest, streamingFromDisk: Bool, streamFileURL: URL?) case failure(Error) } // MARK: - Properties /// A default instance of `SessionManager`, used by top-level Cloudinary request methods, and suitable for use /// directly for any ad hoc requests. internal static let `default`: CLDNSessionManager = { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = CLDNSessionManager.defaultHTTPHeaders return CLDNSessionManager(configuration: configuration) }() /// Creates default values for the "Accept-Encoding", "Accept-Language" and "User-Agent" headers. internal static let defaultHTTPHeaders: CLDNHTTPHeaders = { // Accept-Encoding HTTP Header; see https://tools.ietf.org/html/rfc7230#section-4.2.3 let acceptEncoding: String = "gzip;q=1.0, compress;q=0.5" // Accept-Language HTTP Header; see https://tools.ietf.org/html/rfc7231#section-5.3.5 let acceptLanguage = Locale.preferredLanguages.prefix(6).enumerated().map { index, languageCode in let quality = 1.0 - (Double(index) * 0.1) return "\(languageCode);q=\(quality)" }.joined(separator: ", ") // User-Agent Header; see https://tools.ietf.org/html/rfc7231#section-5.5.3 // Example: `iOS Example/1.0 (org.cloudinary.iOS-Example; build:1; iOS 10.0.0) cloudinary/4.0.0` let userAgent: String = { if let info = Bundle.main.infoDictionary { let executable = info[kCFBundleExecutableKey as String] as? String ?? "Unknown" let bundle = info[kCFBundleIdentifierKey as String] as? String ?? "Unknown" let appVersion = info["CFBundleShortVersionString"] as? String ?? "Unknown" let appBuild = info[kCFBundleVersionKey as String] as? String ?? "Unknown" let osNameVersion: String = { let version = ProcessInfo.processInfo.operatingSystemVersion let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" let osName: String = { #if os(iOS) return "iOS" #elseif os(watchOS) return "watchOS" #elseif os(tvOS) return "tvOS" #elseif os(macOS) return "OS X" #elseif os(Linux) return "Linux" #else return "Unknown" #endif }() return "\(osName) \(versionString)" }() let cloudinaryVersion: String = { guard let afInfo = Bundle(for: CLDNSessionManager.self).infoDictionary, let build = afInfo["CFBundleShortVersionString"] else { return "Unknown" } return "Cloudinary/\(build)" }() return "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(cloudinaryVersion)" } return "Cloudinary" }() return [ "Accept-Encoding": acceptEncoding, "Accept-Language": acceptLanguage, "User-Agent": userAgent ] }() /// Default memory threshold used when encoding `MultipartFormData` in bytes. internal static let multipartFormDataEncodingMemoryThreshold: UInt64 = 10_000_000 /// The underlying session. internal let session: URLSession /// The session delegate handling all the task and session delegate callbacks. internal let delegate: CLDNSessionDelegate /// Whether to start requests immediately after being constructed. `true` by default. internal var startRequestsImmediately: Bool = true /// The request adapter called each time a new request is created. internal var adapter: CLDNRequestAdapter? /// The request retrier called each time a request encounters an error to determine whether to retry the request. internal var retrier: CLDNRequestRetrier? { get { return delegate.retrier } set { delegate.retrier = newValue } } /// The background completion handler closure provided by the UIApplicationDelegate /// `application:handleEventsForBackgroundURLSession:completionHandler:` method. By setting the background /// completion handler, the SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` closure implementation /// will automatically call the handler. /// /// If you need to handle your own events before the handler is called, then you need to override the /// SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` and manually call the handler when finished. /// /// `nil` by default. internal var backgroundCompletionHandler: (() -> Void)? let queue = DispatchQueue(label: "com.cloudinary.session-manager." + UUID().uuidString) // MARK: - Lifecycle /// Creates an instance with the specified `configuration`, `delegate` and `serverTrustPolicyManager`. /// /// - parameter configuration: The configuration used to construct the managed session. /// `URLSessionConfiguration.default` by default. /// - parameter delegate: The delegate used when initializing the session. `CLDNSessionDelegate()` by /// default. /// - parameter serverTrustPolicyManager: The server trust policy manager to use for evaluating all server trust /// challenges. `nil` by default. /// /// - returns: The new `CLDNSessionManager` instance. internal init( configuration: URLSessionConfiguration = URLSessionConfiguration.default, delegate: CLDNSessionDelegate = CLDNSessionDelegate()) { configuration.urlCredentialStorage = nil; self.delegate = delegate self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) commonInit() } /// Creates an instance with the specified `session`, `delegate` and `serverTrustPolicyManager`. /// /// - parameter session: The URL session. /// - parameter delegate: The delegate of the URL session. Must equal the URL session's delegate. /// - parameter serverTrustPolicyManager: The server trust policy manager to use for evaluating all server trust /// challenges. `nil` by default. /// /// - returns: The new `CLDNSessionManager` instance if the URL session's delegate matches; `nil` otherwise. internal init?( session: URLSession, delegate: CLDNSessionDelegate) { guard delegate === session.delegate else { return nil } self.delegate = delegate self.session = session commonInit() } private func commonInit() { delegate.sessionManager = self delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in guard let strongSelf = self else { return } DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() } } } deinit { session.invalidateAndCancel() } // MARK: - Data Request /// Creates a `CLDNDataRequest` to retrieve the contents of the specified `url`, `method`, `parameters`, `encoding` /// and `headers`. /// /// - parameter url: The URL. /// - parameter method: The HTTP method. `.get` by default. /// - parameter parameters: The parameters. `nil` by default. /// - parameter encoding: The parameter encoding. `CLDNURLEncoding.default` by default. /// - parameter headers: The HTTP headers. `nil` by default. /// /// - returns: The created `CLDNDataRequest`. @discardableResult internal func request( _ url: CLDNURLConvertible, method: CLDNHTTPMethod = .get, parameters: CLDNParameters? = nil, encoding: CLDNParameterEncoding = CLDNURLEncoding.default, headers: CLDNHTTPHeaders? = nil) -> CLDNDataRequest { var originalRequest: URLRequest? do { var parameters = parameters let timeoutParamater = parameters?.removeValue(forKey: CLDConfiguration.ConfigParam.Timeout.description) originalRequest = try URLRequest(url: url, method: method, headers: headers) var encodedURLRequest = try encoding.CLDN_Encode(originalRequest!, with: parameters) if let timeout = timeoutParamater as? NSNumber { encodedURLRequest.timeoutInterval = timeout.doubleValue } return request(encodedURLRequest) } catch { return request(originalRequest, failedWith: error) } } /// Creates a `CLDNDataRequest` to retrieve the contents of a URL based on the specified `urlRequest`. /// /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. /// /// - parameter urlRequest: The URL request. /// /// - returns: The created `CLDNDataRequest`. @discardableResult internal func request(_ urlRequest: CLDNURLRequestConvertible) -> CLDNDataRequest { var originalRequest: URLRequest? do { originalRequest = try urlRequest.CLDN_AsURLRequest() let originalTask = CLDNDataRequest.Requestable(urlRequest: originalRequest!) let task = try originalTask.CLDN_Task(session: session, adapter: adapter, queue: queue) let request = CLDNDataRequest(session: session, requestTask: .data(originalTask, task)) delegate[task] = request if startRequestsImmediately { request.resume() } return request } catch { return request(originalRequest, failedWith: error) } } // MARK: Private - Request Implementation private func request(_ urlRequest: URLRequest?, failedWith error: Error) -> CLDNDataRequest { var requestTask: CLDNRequest.RequestTask = .data(nil, nil) if let urlRequest = urlRequest { let originalTask = CLDNDataRequest.Requestable(urlRequest: urlRequest) requestTask = .data(originalTask, nil) } let underlyingError = error.underlyingAdaptError ?? error let request = CLDNDataRequest(session: session, requestTask: requestTask, error: underlyingError) if let retrier = retrier, error is AdaptError { allowRetrier(retrier, toRetry: request, with: underlyingError) } else { if startRequestsImmediately { request.resume() } } return request } // MARK: - Upload Request // MARK: File /// Creates an `CLDNUploadRequest` from the specified `url`, `method` and `headers` for uploading the `file`. /// /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. /// /// - parameter file: The file to upload. /// - parameter url: The URL. /// - parameter method: The HTTP method. `.post` by default. /// - parameter headers: The HTTP headers. `nil` by default. /// /// - returns: The created `CLDNUploadRequest`. @discardableResult internal func upload( _ fileURL: URL, to url: CLDNURLConvertible, method: CLDNHTTPMethod = .post, headers: CLDNHTTPHeaders? = nil) -> CLDNUploadRequest { do { let urlRequest = try URLRequest(url: url, method: method, headers: headers) return upload(fileURL, with: urlRequest) } catch { return upload(nil, failedWith: error) } } /// Creates a `CLDNUploadRequest` from the specified `urlRequest` for uploading the `file`. /// /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. /// /// - parameter file: The file to upload. /// - parameter urlRequest: The URL request. /// /// - returns: The created `CLDNUploadRequest`. @discardableResult internal func upload(_ fileURL: URL, with urlRequest: CLDNURLRequestConvertible) -> CLDNUploadRequest { do { let urlRequest = try urlRequest.CLDN_AsURLRequest() return upload(.file(fileURL, urlRequest)) } catch { return upload(nil, failedWith: error) } } // MARK: Data /// Creates an `CLDNUploadRequest` from the specified `url`, `method` and `headers` for uploading the `data`. /// /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. /// /// - parameter data: The data to upload. /// - parameter url: The URL. /// - parameter method: The HTTP method. `.post` by default. /// - parameter headers: The HTTP headers. `nil` by default. /// /// - returns: The created `CLDNUploadRequest`. @discardableResult internal func upload( _ data: Data, to url: CLDNURLConvertible, method: CLDNHTTPMethod = .post, headers: CLDNHTTPHeaders? = nil) -> CLDNUploadRequest { do { let urlRequest = try URLRequest(url: url, method: method, headers: headers) return upload(data, with: urlRequest) } catch { return upload(nil, failedWith: error) } } /// Creates an `CLDNUploadRequest` from the specified `urlRequest` for uploading the `data`. /// /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. /// /// - parameter data: The data to upload. /// - parameter urlRequest: The URL request. /// /// - returns: The created `CLDNUploadRequest`. @discardableResult internal func upload(_ data: Data, with urlRequest: CLDNURLRequestConvertible) -> CLDNUploadRequest { do { let urlRequest = try urlRequest.CLDN_AsURLRequest() return upload(.data(data, urlRequest)) } catch { return upload(nil, failedWith: error) } } // MARK: InputStream /// Creates an `CLDNUploadRequest` from the specified `url`, `method` and `headers` for uploading the `stream`. /// /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. /// /// - parameter stream: The stream to upload. /// - parameter url: The URL. /// - parameter method: The HTTP method. `.post` by default. /// - parameter headers: The HTTP headers. `nil` by default. /// /// - returns: The created `CLDNUploadRequest`. @discardableResult internal func upload( _ stream: InputStream, to url: CLDNURLConvertible, method: CLDNHTTPMethod = .post, headers: CLDNHTTPHeaders? = nil) -> CLDNUploadRequest { do { let urlRequest = try URLRequest(url: url, method: method, headers: headers) return upload(stream, with: urlRequest) } catch { return upload(nil, failedWith: error) } } /// Creates an `CLDNUploadRequest` from the specified `urlRequest` for uploading the `stream`. /// /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. /// /// - parameter stream: The stream to upload. /// - parameter urlRequest: The URL request. /// /// - returns: The created `CLDNUploadRequest`. @discardableResult internal func upload(_ stream: InputStream, with urlRequest: CLDNURLRequestConvertible) -> CLDNUploadRequest { do { let urlRequest = try urlRequest.CLDN_AsURLRequest() return upload(.stream(stream, urlRequest)) } catch { return upload(nil, failedWith: error) } } // MARK: CLDNMultipartFormData /// Encodes `multipartFormData` using `encodingMemoryThreshold` and calls `encodingCompletion` with new /// `CLDNUploadRequest` using the `url`, `method` and `headers`. /// /// It is important to understand the memory implications of uploading `CLDNMultipartFormData`. If the cummulative /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be /// used for larger payloads such as video content. /// /// The `encodingMemoryThreshold` parameter allows Cloudinary to automatically determine whether to encode in-memory /// or stream from disk. If the content length of the `CLDNMultipartFormData` is below the `encodingMemoryThreshold`, /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding /// technique was used. /// /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. /// /// - parameter multipartFormData: The closure used to append body parts to the `CLDNMultipartFormData`. /// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes. /// `multipartFormDataEncodingMemoryThreshold` by default. /// - parameter url: The URL. /// - parameter method: The HTTP method. `.post` by default. /// - parameter headers: The HTTP headers. `nil` by default. /// - parameter encodingCompletion: The closure called when the `CLDNMultipartFormData` encoding is complete. internal func upload( multipartFormData: @escaping (CLDNMultipartFormData) -> Void, usingThreshold encodingMemoryThreshold: UInt64 = CLDNSessionManager.multipartFormDataEncodingMemoryThreshold, to url: CLDNURLConvertible, method: CLDNHTTPMethod = .post, headers: CLDNHTTPHeaders? = nil, queue: DispatchQueue? = nil, timeout: NSNumber? = nil, encodingCompletion: ((MultipartFormDataEncodingResult) -> Void)?) { do { var urlRequest = try URLRequest(url: url, method: method, headers: headers) if let timeout = timeout { urlRequest.timeoutInterval = timeout.doubleValue } return upload( multipartFormData: multipartFormData, usingThreshold: encodingMemoryThreshold, with: urlRequest, queue: queue, encodingCompletion: encodingCompletion ) } catch { (queue ?? DispatchQueue.main).async { encodingCompletion?(.failure(error)) } } } /// Encodes `multipartFormData` using `encodingMemoryThreshold` and calls `encodingCompletion` with new /// `CLDNUploadRequest` using the `urlRequest`. /// /// It is important to understand the memory implications of uploading `CLDNMultipartFormData`. If the cummulative /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be /// used for larger payloads such as video content. /// /// The `encodingMemoryThreshold` parameter allows Cloudinary to automatically determine whether to encode in-memory /// or stream from disk. If the content length of the `CLDNMultipartFormData` is below the `encodingMemoryThreshold`, /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding /// technique was used. /// /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. /// /// - parameter multipartFormData: The closure used to append body parts to the `CLDNMultipartFormData`. /// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes. /// `multipartFormDataEncodingMemoryThreshold` by default. /// - parameter urlRequest: The URL request. /// - parameter encodingCompletion: The closure called when the `CLDNMultipartFormData` encoding is complete. internal func upload( multipartFormData: @escaping (CLDNMultipartFormData) -> Void, usingThreshold encodingMemoryThreshold: UInt64 = CLDNSessionManager.multipartFormDataEncodingMemoryThreshold, with urlRequest: CLDNURLRequestConvertible, queue: DispatchQueue? = nil, encodingCompletion: ((MultipartFormDataEncodingResult) -> Void)?) { DispatchQueue.global(qos: .utility).async { let formData = CLDNMultipartFormData() multipartFormData(formData) var tempFileURL: URL? do { var urlRequestWithContentType = try urlRequest.CLDN_AsURLRequest() urlRequestWithContentType.setValue(formData.contentType, forHTTPHeaderField: "Content-Type") let isBackgroundSession = self.session.configuration.identifier != nil if formData.contentLength < encodingMemoryThreshold && !isBackgroundSession { let data = try formData.encode() let encodingResult = MultipartFormDataEncodingResult.success( request: self.upload(data, with: urlRequestWithContentType), streamingFromDisk: false, streamFileURL: nil ) (queue ?? DispatchQueue.main).async { encodingCompletion?(encodingResult) } } else { let fileManager = FileManager.default let tempDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()) let directoryURL = tempDirectoryURL.appendingPathComponent("org.cloudinary.manager/multipart.form.data") let fileName = UUID().uuidString let fileURL = directoryURL.appendingPathComponent(fileName) tempFileURL = fileURL var directoryError: Error? // Create directory inside serial queue to ensure two threads don't do this in parallel self.queue.sync { do { try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) } catch { directoryError = error } } if let directoryError = directoryError { throw directoryError } try formData.writeEncodedData(to: fileURL) let upload = self.upload(fileURL, with: urlRequestWithContentType) // Cleanup the temp file once the upload is complete upload.delegate.queue.addOperation { do { try FileManager.default.removeItem(at: fileURL) } catch { // No-op } } (queue ?? DispatchQueue.main).async { let encodingResult = MultipartFormDataEncodingResult.success( request: upload, streamingFromDisk: true, streamFileURL: fileURL ) encodingCompletion?(encodingResult) } } } catch { // Cleanup the temp file in the event that the multipart form data encoding failed if let tempFileURL = tempFileURL { do { try FileManager.default.removeItem(at: tempFileURL) } catch { // No-op } } (queue ?? DispatchQueue.main).async { encodingCompletion?(.failure(error)) } } } } // MARK: Private - Upload Implementation private func upload(_ uploadable: CLDNUploadRequest.Uploadable) -> CLDNUploadRequest { do { let task = try uploadable.CLDN_Task(session: session, adapter: adapter, queue: queue) let upload = CLDNUploadRequest(session: session, requestTask: .upload(uploadable, task)) if case let .stream(inputStream, _) = uploadable { upload.delegate.taskNeedNewBodyStream = { _, _ in inputStream } } delegate[task] = upload if startRequestsImmediately { upload.resume() } return upload } catch { return upload(uploadable, failedWith: error) } } private func upload(_ uploadable: CLDNUploadRequest.Uploadable?, failedWith error: Error) -> CLDNUploadRequest { var uploadTask: CLDNRequest.RequestTask = .upload(nil, nil) if let uploadable = uploadable { uploadTask = .upload(uploadable, nil) } let underlyingError = error.underlyingAdaptError ?? error let upload = CLDNUploadRequest(session: session, requestTask: uploadTask, error: underlyingError) if let retrier = retrier, error is AdaptError { allowRetrier(retrier, toRetry: upload, with: underlyingError) } else { if startRequestsImmediately { upload.resume() } } return upload } // MARK: - Internal - Retry Request func retry(_ request: CLDNRequest) -> Bool { guard let originalTask = request.originalTask else { return false } do { let task = try originalTask.CLDN_Task(session: session, adapter: adapter, queue: queue) if let originalTask = request.task { delegate[originalTask] = nil // removes the old request to avoid endless growth } request.delegate.task = task // resets all task delegate data request.retryCount += 1 request.startTime = CFAbsoluteTimeGetCurrent() request.endTime = nil task.resume() return true } catch { request.delegate.error = error.underlyingAdaptError ?? error return false } } private func allowRetrier(_ retrier: CLDNRequestRetrier, toRetry request: CLDNRequest, with error: Error) { DispatchQueue.CLDNUtility.async { [weak self] in guard let strongSelf = self else { return } retrier.CLDN_Should(strongSelf, retry: request, with: error) { shouldRetry, timeDelay in guard let strongSelf = self else { return } guard shouldRetry else { if strongSelf.startRequestsImmediately { request.resume() } return } DispatchQueue.CLDNUtility.CLDN_after(timeDelay) { guard let strongSelf = self else { return } let retrySucceeded = strongSelf.retry(request) if retrySucceeded, let task = request.task { strongSelf.delegate[task] = request } else { if strongSelf.startRequestsImmediately { request.resume() } } } } } } } ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNTaskDelegate.swift ================================================ // // CLDNTaskDelegate.swift // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation /// The task delegate is responsible for handling all delegate callbacks for the underlying task as well as /// executing all operations attached to the serial operation queue upon task completion. internal class CLDNTaskDelegate: NSObject { // MARK: Properties /// The serial operation queue used to execute all operations after the task completes. internal let queue: OperationQueue /// The data returned by the server. internal var data: Data? { return nil } /// The error generated throughout the lifecyle of the task. internal var error: Error? var task: URLSessionTask? { set { taskLock.lock(); defer { taskLock.unlock() } _task = newValue } get { taskLock.lock(); defer { taskLock.unlock() } return _task } } var initialResponseTime: CFAbsoluteTime? var credential: URLCredential? var metrics: AnyObject? // URLSessionTaskMetrics private var _task: URLSessionTask? { didSet { reset() } } private let taskLock = NSLock() // MARK: Lifecycle init(task: URLSessionTask?) { _task = task self.queue = { let operationQueue = OperationQueue() operationQueue.name = "com.cloudinary.CLDNTaskDelegateOperationQueue" operationQueue.maxConcurrentOperationCount = 1 operationQueue.isSuspended = true operationQueue.qualityOfService = .utility return operationQueue }() } func reset() { error = nil initialResponseTime = nil } // MARK: URLSessionTaskDelegate var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)? var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))? var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)? var taskDidCompleteWithError: ((URLSession, URLSessionTask, Error?) -> Void)? @objc(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:) func urlSession( _ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { var redirectRequest: URLRequest? = request if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection { redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request) } completionHandler(redirectRequest) } @objc(URLSession:task:didReceiveChallenge:completionHandler:) func urlSession( _ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling var credential: URLCredential? if let taskDidReceiveChallenge = taskDidReceiveChallenge { (disposition, credential) = taskDidReceiveChallenge(session, task, challenge) } else { if challenge.previousFailureCount > 0 { disposition = .rejectProtectionSpace } else { credential = self.credential ?? session.configuration.urlCredentialStorage?.defaultCredential(for: challenge.protectionSpace) if credential != nil { disposition = .useCredential } } } completionHandler(disposition, credential) } @objc(URLSession:task:needNewBodyStream:) func urlSession( _ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) { var bodyStream: InputStream? if let taskNeedNewBodyStream = taskNeedNewBodyStream { bodyStream = taskNeedNewBodyStream(session, task) } completionHandler(bodyStream) } @objc(URLSession:task:didCompleteWithError:) func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let taskDidCompleteWithError = taskDidCompleteWithError { taskDidCompleteWithError(session, task, error) } else { if let error = error { if self.error == nil { self.error = error } } queue.isSuspended = false } } } // MARK: - class CLDNDataTaskDelegate: CLDNTaskDelegate, URLSessionDataDelegate { // MARK: Properties var dataTask: URLSessionDataTask { return task as! URLSessionDataTask } override var data: Data? { if dataStream != nil { return nil } else { return mutableData } } var progress: Progress var progressHandler: (closure: CLDNRequest.ProgressHandler, queue: DispatchQueue)? var dataStream: ((_ data: Data) -> Void)? private var totalBytesReceived: Int64 = 0 private var mutableData: Data private var expectedContentLength: Int64? // MARK: Lifecycle override init(task: URLSessionTask?) { mutableData = Data() progress = Progress(totalUnitCount: 0) super.init(task: task) } override func reset() { super.reset() progress = Progress(totalUnitCount: 0) totalBytesReceived = 0 mutableData = Data() expectedContentLength = nil } // MARK: URLSessionDataDelegate var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)? var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)? var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)? var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)? func urlSession( _ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { var disposition: URLSession.ResponseDisposition = .allow expectedContentLength = response.expectedContentLength if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse { disposition = dataTaskDidReceiveResponse(session, dataTask, response) } completionHandler(disposition) } func urlSession( _ session: URLSession, dataTask: URLSessionDataTask, didBecome downloadTask: URLSessionDownloadTask) { dataTaskDidBecomeDownloadTask?(session, dataTask, downloadTask) } func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() } if let dataTaskDidReceiveData = dataTaskDidReceiveData { dataTaskDidReceiveData(session, dataTask, data) } else { if let dataStream = dataStream { dataStream(data) } else { mutableData.append(data) } let bytesReceived = Int64(data.count) totalBytesReceived += bytesReceived let totalBytesExpected = dataTask.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown progress.totalUnitCount = totalBytesExpected progress.completedUnitCount = totalBytesReceived if let progressHandler = progressHandler { progressHandler.queue.async { progressHandler.closure(self.progress) } } } } func urlSession( _ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) { var cachedResponse: CachedURLResponse? = proposedResponse if let dataTaskWillCacheResponse = dataTaskWillCacheResponse { cachedResponse = dataTaskWillCacheResponse(session, dataTask, proposedResponse) } completionHandler(cachedResponse) } } // MARK: - class CLDNUploadTaskDelegate: CLDNDataTaskDelegate { // MARK: Properties var uploadTask: URLSessionUploadTask { return task as! URLSessionUploadTask } var uploadProgress: Progress var uploadProgressHandler: (closure: CLDNRequest.ProgressHandler, queue: DispatchQueue)? // MARK: Lifecycle override init(task: URLSessionTask?) { uploadProgress = Progress(totalUnitCount: 0) super.init(task: task) } override func reset() { super.reset() uploadProgress = Progress(totalUnitCount: 0) } // MARK: URLSessionTaskDelegate var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)? func URLSession( _ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() } if let taskDidSendBodyData = taskDidSendBodyData { taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend) } else { uploadProgress.totalUnitCount = totalBytesExpectedToSend uploadProgress.completedUnitCount = totalBytesSent if let uploadProgressHandler = uploadProgressHandler { uploadProgressHandler.queue.async { uploadProgressHandler.closure(self.uploadProgress) } } } } } ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNTimeline.swift ================================================ // // CLDNTimeline.swift // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation /// Responsible for computing the timing metrics for the complete lifecycle of a `CLDNRequest`. internal struct CLDNTimeline { /// The time the request was initialized. internal let requestStartTime: CFAbsoluteTime /// The time the first bytes were received from or sent to the server. internal let initialResponseTime: CFAbsoluteTime /// The time when the request was completed. internal let requestCompletedTime: CFAbsoluteTime /// The time when the response serialization was completed. internal let serializationCompletedTime: CFAbsoluteTime /// The time interval in seconds from the time the request started to the initial response from the server. internal let latency: TimeInterval /// The time interval in seconds from the time the request started to the time the request completed. internal let requestDuration: TimeInterval /// The time interval in seconds from the time the request completed to the time response serialization completed. internal let serializationDuration: TimeInterval /// The time interval in seconds from the time the request started to the time response serialization completed. internal let totalDuration: TimeInterval /// Creates a new `CLDNTimeline` instance with the specified request times. /// /// - parameter requestStartTime: The time the request was initialized. Defaults to `0.0`. /// - parameter initialResponseTime: The time the first bytes were received from or sent to the server. /// Defaults to `0.0`. /// - parameter requestCompletedTime: The time when the request was completed. Defaults to `0.0`. /// - parameter serializationCompletedTime: The time when the response serialization was completed. Defaults /// to `0.0`. /// /// - returns: The new `CLDNTimeline` instance. internal init( requestStartTime: CFAbsoluteTime = 0.0, initialResponseTime: CFAbsoluteTime = 0.0, requestCompletedTime: CFAbsoluteTime = 0.0, serializationCompletedTime: CFAbsoluteTime = 0.0) { self.requestStartTime = requestStartTime self.initialResponseTime = initialResponseTime self.requestCompletedTime = requestCompletedTime self.serializationCompletedTime = serializationCompletedTime self.latency = initialResponseTime - requestStartTime self.requestDuration = requestCompletedTime - requestStartTime self.serializationDuration = serializationCompletedTime - requestCompletedTime self.totalDuration = serializationCompletedTime - requestStartTime } } // MARK: - CustomStringConvertible extension CLDNTimeline: CustomStringConvertible { /// The textual representation used when written to an output stream, which includes the latency, the request /// duration and the total duration. internal var description: String { let latency = String(format: "%.3f", self.latency) let requestDuration = String(format: "%.3f", self.requestDuration) let serializationDuration = String(format: "%.3f", self.serializationDuration) let totalDuration = String(format: "%.3f", self.totalDuration) // NOTE: Had to move to string concatenation due to memory leak filed as rdar://26761490. Once memory leak is // fixed, we should move back to string interpolation by reverting commit 7d4a43b1. let timings = [ "\"Latency\": " + latency + " secs", "\"Request Duration\": " + requestDuration + " secs", "\"Serialization Duration\": " + serializationDuration + " secs", "\"Total Duration\": " + totalDuration + " secs" ] return "Timeline: { " + timings.joined(separator: ", ") + " }" } } // MARK: - CustomDebugStringConvertible extension CLDNTimeline: CustomDebugStringConvertible { /// The textual representation used when written to an output stream, which includes the request start time, the /// initial response time, the request completed time, the serialization completed time, the latency, the request /// duration and the total duration. internal var debugDescription: String { let requestStartTime = String(format: "%.3f", self.requestStartTime) let initialResponseTime = String(format: "%.3f", self.initialResponseTime) let requestCompletedTime = String(format: "%.3f", self.requestCompletedTime) let serializationCompletedTime = String(format: "%.3f", self.serializationCompletedTime) let latency = String(format: "%.3f", self.latency) let requestDuration = String(format: "%.3f", self.requestDuration) let serializationDuration = String(format: "%.3f", self.serializationDuration) let totalDuration = String(format: "%.3f", self.totalDuration) // NOTE: Had to move to string concatenation due to memory leak filed as rdar://26761490. Once memory leak is // fixed, we should move back to string interpolation by reverting commit 7d4a43b1. let timings = [ "\"Request Start Time\": " + requestStartTime, "\"Initial Response Time\": " + initialResponseTime, "\"Request Completed Time\": " + requestCompletedTime, "\"Serialization Completed Time\": " + serializationCompletedTime, "\"Latency\": " + latency + " secs", "\"Request Duration\": " + requestDuration + " secs", "\"Serialization Duration\": " + serializationDuration + " secs", "\"Total Duration\": " + totalDuration + " secs" ] return "Timeline: { " + timings.joined(separator: ", ") + " }" } } ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/CLDNValidation.swift ================================================ // // CLDNValidation.swift // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation extension CLDNRequest { // MARK: Helper Types fileprivate typealias ErrorReason = CLDNError.ResponseValidationFailureReason /// Used to represent whether validation was successful or encountered an error resulting in a failure. /// /// - success: The validation was successful. /// - failure: The validation failed encountering the provided error. internal enum ValidationResult { case success case failure(Error) } fileprivate struct MIMEType { let type: String let subtype: String var isWildcard: Bool { return type == "*" && subtype == "*" } init?(_ string: String) { let components: [String] = { let stripped = string.trimmingCharacters(in: .whitespacesAndNewlines) #if swift(>=3.2) let split = stripped[..<(stripped.range(of: ";")?.lowerBound ?? stripped.endIndex)] #else let split = stripped.substring(to: stripped.range(of: ";")?.lowerBound ?? stripped.endIndex) #endif return split.components(separatedBy: "/") }() if let type = components.first, let subtype = components.last { self.type = type self.subtype = subtype } else { return nil } } func matches(_ mime: MIMEType) -> Bool { switch (type, subtype) { case (mime.type, mime.subtype), (mime.type, "*"), ("*", mime.subtype), ("*", "*"): return true default: return false } } } // MARK: Properties internal var acceptableStatusCodes: [Int] { return Array(200..<300) } fileprivate var acceptableContentTypes: [String] { if let accept = request?.value(forHTTPHeaderField: "Accept") { return accept.components(separatedBy: ",") } return ["*/*"] } // MARK: Status Code fileprivate func validate( statusCode acceptableStatusCodes: S, response: HTTPURLResponse) -> ValidationResult where S.Iterator.Element == Int { if acceptableStatusCodes.contains(response.statusCode) { return .success } else { let reason: ErrorReason = .unacceptableStatusCode(code: response.statusCode) return .failure(CLDNError.responseValidationFailed(reason: reason)) } } // MARK: Content Type fileprivate func validate( contentType acceptableContentTypes: S, response: HTTPURLResponse, data: Data?) -> ValidationResult where S.Iterator.Element == String { guard let data = data, data.count > 0 else { return .success } guard let responseContentType = response.mimeType, let responseMIMEType = MIMEType(responseContentType) else { for contentType in acceptableContentTypes { if let mimeType = MIMEType(contentType), mimeType.isWildcard { return .success } } let error: CLDNError = { let reason: ErrorReason = .missingContentType(acceptableContentTypes: Array(acceptableContentTypes)) return CLDNError.responseValidationFailed(reason: reason) }() return .failure(error) } for contentType in acceptableContentTypes { if let acceptableMIMEType = MIMEType(contentType), acceptableMIMEType.matches(responseMIMEType) { return .success } } let error: CLDNError = { let reason: ErrorReason = .unacceptableContentType( acceptableContentTypes: Array(acceptableContentTypes), responseContentType: responseContentType ) return CLDNError.responseValidationFailed(reason: reason) }() return .failure(error) } } // MARK: - extension CLDNDataRequest { /// A closure used to validate a request that takes a URL request, a URL response and data, and returns whether the /// request was valid. internal typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult /// Validates the request, using the specified closure. /// /// If validation fails, subsequent calls to response handlers will have an associated error. /// /// - parameter validation: A closure to validate the request. /// /// - returns: The request. @discardableResult internal func validate(_ validation: @escaping Validation) -> Self { let validationExecution: () -> Void = { [unowned self] in if let response = self.response, self.delegate.error == nil, case let .failure(error) = validation(self.request, response, self.delegate.data) { self.delegate.error = error } } validations.append(validationExecution) return self } /// Validates that the response has a status code in the specified sequence. /// /// If validation fails, subsequent calls to response handlers will have an associated error. /// /// - parameter range: The range of acceptable status codes. /// /// - returns: The request. @discardableResult internal func validate(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int { return validate { [unowned self] _, response, _ in return self.validate(statusCode: acceptableStatusCodes, response: response) } } /// Validates that the response has a content type in the specified sequence. /// /// If validation fails, subsequent calls to response handlers will have an associated error. /// /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes. /// /// - returns: The request. @discardableResult internal func validate(contentType acceptableContentTypes: S) -> Self where S.Iterator.Element == String { return validate { [unowned self] _, response, data in return self.validate(contentType: acceptableContentTypes, response: response, data: data) } } /// Validates that the response has a status code in the default acceptable range of 200...299, and that the content /// type matches any specified in the Accept HTTP header field. /// /// If validation fails, subsequent calls to response handlers will have an associated error. /// /// - returns: The request. @discardableResult internal func validate() -> Self { let contentTypes = { [unowned self] in self.acceptableContentTypes } return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes()) } } ================================================ FILE: Cloudinary/Classes/Core/BaseNetwork/DispatchQueue+Cloudinary.swift ================================================ // // DispatchQueue+Cloudinary.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Dispatch import Foundation extension DispatchQueue { static var CLDNUserInteractive: DispatchQueue { return DispatchQueue.global(qos: .userInteractive) } static var CLDNUserInitiated: DispatchQueue { return DispatchQueue.global(qos: .userInitiated) } static var CLDNUtility: DispatchQueue { return DispatchQueue.global(qos: .utility) } static var CLDNBackground: DispatchQueue { return DispatchQueue.global(qos: .background) } func CLDN_after(_ delay: TimeInterval, execute closure: @escaping () -> Void) { asyncAfter(deadline: .now() + delay, execute: closure) } } ================================================ FILE: Cloudinary/Classes/Core/CLDAnalytics.swift ================================================ // // CLDAnalytics.swift // Cloudinary // // Created by Adi Mizrahi on 13/07/2022. // import Foundation import UIKit @objcMembers open class CLDAnalytics: NSObject { private final let ALGO_VERSION = "D" private final let PRODUCT = "A" private final let SDK = "E" private final let OS_TYPE = "B" private final let ERROR_SIGNATURE = "E" private var sdkVersion: String? = nil private var techVersion: String? = nil private var osVersion: String? = nil private var osType: String? = nil private var featureFlag = "0" public init(sdkVersion: String? = nil, techVersion: String? = nil, osType: String? = "B", osVersion: String? = nil, featureFlag: String? = nil) { super.init() self.sdkVersion = sdkVersion ?? CLDNetworkCoordinator.getVersion() self.techVersion = techVersion ?? getiOSVersion() self.osType = osType self.osVersion = osVersion ?? getiOSVersion() self.featureFlag = featureFlag ?? "0" } public func generateAnalyticsSignature(sdkVersion: String? = nil, techVersion: String? = nil, osType: String? = "B", osVersion: String? = nil, featureFlag: String? = nil) -> String { let sdkVersion = sdkVersion ?? self.sdkVersion let techVersion = techVersion ?? self.techVersion let osType = osType ?? self.osType let osVersion = osVersion ?? self.osVersion let featureFlag = featureFlag ?? self.featureFlag let swiftVersionArray = techVersion!.split(usingRegex: "\\.|\\-") guard swiftVersionArray.count > 1, let techVersionString = generateVersionString(major: String(swiftVersionArray[0]), minor: String(swiftVersionArray[1]), patch: "") else { return ERROR_SIGNATURE } let osVersionArray = osVersion!.split(usingRegex: "\\.|\\-") guard let osVersionString = generateOSVersionString(major: String(osVersionArray[0]), minor: String(osVersionArray[1])) else { return ERROR_SIGNATURE } let sdkVersionArray = sdkVersion!.split(usingRegex: "\\.|\\-") guard sdkVersionArray.count > 1, let sdkVersionString = generateVersionString(major: String(sdkVersionArray[0]), minor: String(sdkVersionArray[1]), patch: String(sdkVersionArray[2])) else { return ERROR_SIGNATURE } return "\(ALGO_VERSION)\(PRODUCT)\(SDK)\(sdkVersionString)\(techVersionString)\(osType ?? OS_TYPE)\(osVersionString)\(featureFlag)" } public func setSDKVersion(version: String) { sdkVersion = version } public func setTechVersion(version: String) { techVersion = version } public func setOsVersion(version: String) { osVersion = version } public func setFeatureFlag(flag: String? = nil) { guard let flag = flag else { featureFlag = "0" return } featureFlag = flag } private func generateOSVersionString(major: String, minor: String) -> String? { let majorVersionString = major.leftPadding(toLength: 2, withPad: "0") let minorVersionString = minor.leftPadding(toLength: 2, withPad: "0") guard let majorDoubleValue = Int(majorVersionString), let minorDoubleValue = Int(minorVersionString) else { return nil } let majorString = String(majorDoubleValue, radix: 2) let minorString = String(minorDoubleValue, radix: 2) let majorStr = majorString.toAnalyticsVersionStr() let minorStr = minorString.toAnalyticsVersionStr() return "\(majorStr)\(minorStr)" } private func generateVersionString(major: String, minor: String, patch: String) -> String? { let versionString = (patch.leftPadding(toLength: 2, withPad: "0") + minor.leftPadding(toLength: 2, withPad: "0") + major.leftPadding(toLength: 2, withPad: "0")) guard let doubleValue = Int(versionString) else { return nil } let hexString = String(doubleValue, radix: 2).leftPadding(toLength: 18, withPad: "0") var patchStr = "" if (!patch.isEmpty) { patchStr = String(hexString[hexString.startIndex.. String { return UIDevice.current.systemVersion } } extension String { func split(usingRegex pattern: String) -> [String] { //### Crashes when you pass invalid `pattern` let regex = try! NSRegularExpression(pattern: pattern) let matches = regex.matches(in: self, range: NSRange(0.. () public typealias CLDAssetCompletionHandler = (_ responseAsset: Data?, _ error: NSError?) -> () public typealias CLDUploadCompletionHandler = (_ response: CLDUploadResult?, _ error: NSError?) -> () @objcMembers open class CLDCloudinary: NSObject { /** Holds the configuration parameters to be used by the `CLDCloudinary` instance. */ open fileprivate(set) var config: CLDConfiguration /** The network coordinator coordinates between the SDK's API level classes to its network adapter layer. */ fileprivate var networkCoordinator: CLDNetworkCoordinator /** The network download coordinator coordinates between the SDK's API level classes to its network adapter layer. */ fileprivate var downloadCoordinator: CLDDownloadCoordinator // MARK: - SDK Configurations // MARK: Log Level /** Sets Cloudinary SDK's log level, default level is set to **None**. */ public static var logLevel: CLDLogLevel { get { return CLDLogManager.minimumLogLevel } set { CLDLogManager.minimumLogLevel = newValue } } // MARK: Image Cache open var enableUrlCache: Bool { get { return CLDDownloadCoordinator.enableCache } set { CLDDownloadCoordinator.enableCache = newValue } } /** Sets Cloudinary SDK's image cache maximum disk capacity. default is 150 MB. */ open var cacheMaxDiskCapacity: Int { get { return CLDDownloadCoordinator.urlCache.diskCapacity } set { CLDDownloadCoordinator.urlCache.diskCapacity = newValue } } /** Sets Cloudinary SDK's image cache maximum memory total cost. default is 30 MB. */ open var cacheMaxMemoryTotalCost: Int { get { return CLDDownloadCoordinator.urlCache.memoryCapacity } set { CLDDownloadCoordinator.urlCache.memoryCapacity = newValue } } // MARK: - Init /** Initializes the `CLDCloudinary` instance with the specified configuration and network adapter. - parameter configuration: The configuration used by this CLDCloudinary instance. - parameter networkAdapter: A network adapter that implements `CLDNetworkAdapter`. CLDNetworkDelegate() by default. - returns: The new `CLDCloudinary` instance. */ public convenience init(configuration: CLDConfiguration, networkAdapter: CLDNetworkAdapter? = nil, sessionConfiguration: URLSessionConfiguration? = nil) { self.init(configuration: configuration, networkAdapter: networkAdapter, downloadAdapter: nil, sessionConfiguration: sessionConfiguration, downloadSessionConfiguration: nil) } /** Initializes the `CLDCloudinary` instance with the specified configuration and network adapter. - parameter configuration: The configuration used by this CLDCloudinary instance. - parameter networkAdapter: A network adapter that implements `CLDNetworkAdapter`. - parameter downloadAdapter: A download adapter that implements `CLDNetworkAdapter`. - parameter sessionConfiguration: A session configuration that implements `URLSessionConfiguration`. - parameter downloadSessionConfiguration: A download session configuration that implements `URLSessionConfiguration`. CLDNetworkDelegate() by default. - returns: The new `CLDCloudinary` instance. */ public init(configuration: CLDConfiguration, networkAdapter: CLDNetworkAdapter? = nil, downloadAdapter: CLDNetworkAdapter? = nil, sessionConfiguration: URLSessionConfiguration? = nil, downloadSessionConfiguration: URLSessionConfiguration? = nil) { config = configuration if let customNetworkAdapter = networkAdapter { networkCoordinator = CLDNetworkCoordinator(configuration: config, networkAdapter: customNetworkAdapter) } else { if let sessionConfiguration = sessionConfiguration { networkCoordinator = CLDNetworkCoordinator(configuration: config, sessionConfiguration: sessionConfiguration) } else { networkCoordinator = CLDNetworkCoordinator(configuration: config) } } if let customDownloadAdapter = downloadAdapter { downloadCoordinator = CLDDownloadCoordinator(configuration: config, networkAdapter: customDownloadAdapter) } else { if let downloadSessionConfiguration = downloadSessionConfiguration { downloadCoordinator = CLDDownloadCoordinator(configuration: config, sessionConfiguration: downloadSessionConfiguration) } else { downloadCoordinator = CLDDownloadCoordinator(configuration: config) } } super.init() } // MARK: Factory Methods /** A factory method to create a new `CLDUrl` instance - returns: A new `CLDUrl` instance. */ open func createUrl() -> CLDUrl { return CLDUrl(configuration: config) } /** A factory method to create a new `CLDUploader` instance - returns: A new `CLDUploader` instance. */ open func createUploader() -> CLDUploader { return CLDUploader(networkCoordinator: networkCoordinator) } /** A factory method to create a new `CLDDownloader` instance - returns: A new `CLDDownloader` instance. */ open func createDownloader() -> CLDDownloader { return CLDDownloader(downloadCoordinator: downloadCoordinator) } /** A factory method to create a new `CLDAdminApi` instance - returns: A new `CLDAdminApi` instance. */ open func createManagementApi() -> CLDManagementApi { return CLDManagementApi(networkCoordinator: networkCoordinator) } // MARK: - Network Adapter /** The maximum number of queued downloads that can execute at the same time. default is NSOperationQueueDefaultMaxConcurrentOperationCount. - parameter maxConcurrentDownloads: The maximum concurrent downloads to allow. */ @available(iOS 8.0, *) open func setMaxConcurrentDownloads(_ maxConcurrentDownloads: Int) { networkCoordinator.setMaxConcurrentDownloads(maxConcurrentDownloads) } // MARK: Background Session /** Set a completion handler provided by the UIApplicationDelegate `application:handleEventsForBackgroundURLSession:completionHandler:` method. The handler will be called automatically once the session finishes its events for background URL session. default is `nil`. */ @available(iOS 8.0, *) open func setBackgroundCompletionHandler(_ newValue: (() -> ())?) { networkCoordinator.setBackgroundCompletionHandler(newValue) } // MARK: - Advanced /** Sets the "USER-AGENT" HTTP header on all network requests to be **"PlatformName/ver CloudinaryiOS/ver"** By default the header is set to **"CloudinaryiOS/ver"** */ open func setUserPlatform(_ platformName: String, version: String) { config.setUserPlatform(platformName, version: version) } open func setExtraHeaderes(_ extraHeaders: [String: String]) { networkCoordinator.setExtraHeaders(extraHeaders) } } ================================================ FILE: Cloudinary/Classes/Core/CLDCompatibility.swift ================================================ // // Created by Amir on 05/11/2017. // Copyright (c) 2017 Cloudinary. All rights reserved. // import Foundation #if swift(>=4.0) internal extension NSTextCheckingResult { func rangeAt(_ idx:Int) -> NSRange { return range(at: idx) } } #endif ================================================ FILE: Cloudinary/Classes/Core/CLDConfiguration.swift ================================================ // // CLDConfiguration.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The CLDConfiguration class holds the configuration parameters to be used by the `CLDCloudinary` instance. */ @objcMembers open class CLDConfiguration: NSObject { fileprivate struct Defines { fileprivate static let ENV_VAR_CLOUDINARY_URL = "CLOUDINARY_URL" } /** Your account's cloud name on Cloudinary. */ open fileprivate(set) var cloudName: String! /** Your account's API key, can be found in your account's dashboard on Cloudinary as part of the account details. */ open fileprivate(set) var apiKey: String? /** Your account's API secret, can be found in your account's dashboard on Cloudinary as part of the account details. */ open fileprivate(set) var apiSecret: String? /** A boolean value specifying whether or not to use a private CDN. false by default. */ open fileprivate(set) var privateCdn: Bool = false /** A boolean value specifying whether or not to use a secure CDN connection. false by default. */ open fileprivate(set) var secure: Bool = true /** A boolean value specifying whether or not to use a CDN subdomain. false by default. */ open fileprivate(set) var cdnSubdomain: Bool = false /** A boolean value specifying whether or not to use a secure connection with a CDN subdomain. false by default. */ open fileprivate(set) var secureCdnSubdomain: Bool = false /** A boolean value specifying whether or not to use long encryption. false by default. */ open fileprivate(set) var longUrlSignature: Bool = false /** An enum value specifying the desired hash algorithm. sha1 by default. */ open fileprivate(set) var signatureAlgorithm: SignatureAlgorithm = .sha1 /** A int value specifying the signature version to use. */ open fileprivate(set) var signatureVesion : Int = 2 /** Your secure distribution domain to be set when using a secure distribution (advanced plan only). nil by default. */ open fileprivate(set) var secureDistribution: String? /** Your custom domain. nil by default. */ open fileprivate(set) var cname: String? /** A custom upload prefix to be used instead of Cloudinary's default API prefix. nil by default. */ open fileprivate(set) var uploadPrefix: String? /** A custom timeout in milliseconds to be used instead of Cloudinary's default timeout. nil by default. */ open fileprivate(set) var timeout: NSNumber? /** A boolean value specifying whether or not to use analytics. true by default. */ open var analytics: Bool = true /** An analytics object */ open var analyticsObject: CLDAnalytics = CLDAnalytics() internal var userPlatform: CLDUserPlatform? // MARK: - Init fileprivate override init() { super.init() } /** Initializes a CLDConfiguration instance, using the URL specified in the environment parameters under `CLOUDINARY_URL`. The URL should be in this form: `cloudinary://:@`. Extra parameters may be added to the url: `secure` (boolean), `cdn_subdomain` (boolean), `secure_cdn_distribution` (boolean), `long_url_signature`(boolean), `cname`, `upload_prefix`, `signature_algorithm` - returns: A new `CLDConfiguration` instance if the environment parameter URL exists and is valid, otherwise returns nil. */ public static func initWithEnvParams() -> CLDConfiguration? { let dict = ProcessInfo.processInfo.environment if let url = dict[Defines.ENV_VAR_CLOUDINARY_URL], validateUrl(url: url) { return CLDConfiguration(cloudinaryUrl: url) } return nil } internal static func validateUrl(url: String) -> Bool{ return url.starts(with: "cloudinary://") } public init?(options: [String : AnyObject]) { guard let cloudName = options[ConfigParam.CloudName.rawValue] as? String else { return nil } self.cloudName = cloudName for key in options.keys { if let option = ConfigParam(rawValue: key) { switch option { case .Secure: if let value = options[ConfigParam.Secure.rawValue] as? Bool { secure = value } else if let value = options[ConfigParam.Secure.rawValue] as? String { secure = value.cldAsBool() } break case .CdnSubdomain: if let value = options[ConfigParam.CdnSubdomain.rawValue] as? Bool { cdnSubdomain = value } else if let value = options[ConfigParam.CdnSubdomain.rawValue] as? String { cdnSubdomain = value.cldAsBool() } break case .SecureCdnSubdomain: if let value = options[ConfigParam.SecureCdnSubdomain.rawValue] as? Bool { secureCdnSubdomain = value } else if let value = options[ConfigParam.SecureCdnSubdomain.rawValue] as? String { secureCdnSubdomain = value.cldAsBool() } break case .LongUrlSignature: if let value = options[ConfigParam.LongUrlSignature.rawValue] as? Bool { longUrlSignature = value } else if let value = options[ConfigParam.LongUrlSignature.rawValue] as? String { longUrlSignature = value.cldAsBool() } break case .SignatureAlgorithm: if let value = options[ConfigParam.SignatureAlgorithm.rawValue] as? SignatureAlgorithm { signatureAlgorithm = value } else if let value = options[ConfigParam.SignatureAlgorithm.rawValue] as? String { signatureAlgorithm = SignatureAlgorithm(stringValue: value) } break case .SignatureVersion: if let value = options[ConfigParam.SignatureVersion.rawValue] as? Int { signatureVesion = value } case .CName: if let value = options[ConfigParam.CName.rawValue] as? String { cname = value } break case .UploadPrefix: if let value = options[ConfigParam.UploadPrefix.rawValue] as? String { uploadPrefix = value } break case .APISecret: if let value = options[ConfigParam.APISecret.rawValue] as? String { apiSecret = value } break case .APIKey: if let value = options[ConfigParam.APIKey.rawValue] as? String { apiKey = value } break case .PrivateCdn: if let value = options[ConfigParam.PrivateCdn.rawValue] as? Bool { privateCdn = value } else if let value = options[ConfigParam.PrivateCdn.rawValue] as? String { privateCdn = value.cldAsBool() } break case .SecureDistribution: if let value = options[ConfigParam.SecureDistribution.rawValue] as? String { secureDistribution = value } break case .Timeout: if let value = options[ConfigParam.Timeout.rawValue] as? NSNumber { timeout = value } else if let value = options[ConfigParam.Timeout.rawValue] as? String { timeout = value.cldAsNSNumber() } break default: break } } } } /** Initializes a CLDConfiguration instance with the specified parameters. - parameter cloudName: Your account's cloud name on Cloudinary. - parameter apiKey: Your account's API key, can be found in your account's dashboard on Cloudinary as part of the account details. - parameter apiSecret: Your account's API secret, can be found in your account's dashboard on Cloudinary as part of the account details. - parameter privateCdn: A boolean value specifying whether or not to use a private CDN. false by default. - parameter secure: A boolean value specifying whether or not to use a secure CDN connection. false by default. - parameter cdnSubdomain: A boolean value specifying whether or not to use a CDN subdomain. false by default. - parameter secureCdnSubdomain: A boolean value specifying whether or not to use a secure connection with a CDN subdomain. false by default. - parameter longUrlSignature: A boolean value specifying whether or not to use long encryption. false by default. - parameter signatureAlgorithm: An enum value specifying the desired hash algorithm. sha1 by default. - parameter secureDistribution: Set your secure distribution domain to be set when using a secure distribution (advanced plan only). nil by default. - parameter cname: Set your custom domain. nil by default. - parameter uploadPrefix: Set a custom upload prefix to be used instead of Cloudinary's default API prefix. nil by default. - parameter timeout: A custom timeout in milliseconds to be used instead of Cloudinary's default timeout. nil by default. - returns: A new `CLDConfiguration` instance. */ public init( cloudName: String, apiKey: String? = nil, apiSecret: String? = nil, privateCdn: Bool = false, secure: Bool = false, cdnSubdomain: Bool = false, secureCdnSubdomain: Bool = false, longUrlSignature: Bool = false, signatureAlgorithm: SignatureAlgorithm = .sha1, signatureVersion: Int = 2, secureDistribution: String? = nil, cname: String? = nil, uploadPrefix: String? = nil, timeout: NSNumber? = nil, analytics: Bool = true ) { self.cloudName = cloudName self.apiKey = apiKey self.apiSecret = apiSecret self.privateCdn = privateCdn self.secure = secure self.cdnSubdomain = cdnSubdomain self.secureCdnSubdomain = secureCdnSubdomain self.longUrlSignature = longUrlSignature self.signatureAlgorithm = signatureAlgorithm self.signatureVesion = signatureVersion self.secureDistribution = secureDistribution self.cname = cname self.uploadPrefix = uploadPrefix self.timeout = timeout self.analytics = analytics super.init() } /** Initializes a CLDConfiguration instance, using a given URL. The URL should be in this form: `cloudinary://:@`. Extra parameters may be added to the url: `secure` (boolean), `cdn_subdomain` (boolean), `secure_cdn_distribution` (boolean), `long_url_signature`(boolean), `cname`, `upload_prefix`, `signature_algorithm` - returns: A new `CLDConfiguration` instance if the URL is valid, otherwise returns nil. */ public init?(cloudinaryUrl: String) { super.init() guard let uri = URL(string: cloudinaryUrl), CLDConfiguration.validateUrl(url: cloudinaryUrl), let cloudName = uri.host else { return nil } self.cloudName = cloudName if let apiKey = uri.user { self.apiKey = apiKey } if let apiSecret = uri.password { self.apiSecret = apiSecret } if !uri.path.isEmpty { privateCdn = true let index1 = uri.path.index(uri.path.startIndex, offsetBy: 1) secureDistribution = String(uri.path[index1...]) } if let queryItems = URLComponents(url: uri, resolvingAgainstBaseURL: false)?.queryItems { for item in queryItems { guard let value = item.value else { continue } switch ConfigParam(rawValue: item.name) { case .Secure: secure = value.cldAsBool() case .CdnSubdomain: cdnSubdomain = value.cldAsBool() case .SecureCdnSubdomain: secureCdnSubdomain = value.cldAsBool() case .LongUrlSignature: longUrlSignature = value.cldAsBool() case .SignatureAlgorithm: signatureAlgorithm = SignatureAlgorithm(stringValue: value) case .SignatureVersion: signatureVesion = Int(value) ?? 2 case .CName: cname = value case .UploadPrefix: uploadPrefix = value case .Timeout: timeout = value.cldAsNSNumber() case .Analytics: analytics = value.cldAsBool() default: continue } } } } // MARK: Optional Url Params internal enum ConfigParam: String, CustomStringConvertible { case Secure = "secure" case CdnSubdomain = "cdn_subdomain" case SecureCdnSubdomain = "secure_cdn_subdomain" case LongUrlSignature = "long_url_signature" case SignatureAlgorithm = "signature_algorithm" case SignatureVersion = "signature_version" case CName = "cname" case UploadPrefix = "upload_prefix" case APIKey = "api_key" case APISecret = "api_secret" case CloudName = "cloud_name" case PrivateCdn = "private_cdn" case SecureDistribution = "secure_distribution" case Timeout = "timeout" case Analytics = "analytics" internal var description: String { switch self { case .Secure: return "secure" case .CdnSubdomain: return "cdn_subdomain" case .SecureCdnSubdomain: return "secure_cdn_subdomain" case .LongUrlSignature: return "long_url_signature" case .SignatureAlgorithm: return "signature_algorithm" case .SignatureVersion: return "signature_version" case .CName: return "cname" case .UploadPrefix: return "upload_prefix" case .APIKey: return "api_key" case .APISecret: return "api_secret" case .CloudName: return "cloud_name" case .PrivateCdn: return "private_cdn" case .SecureDistribution: return "secure_distribution" case .Timeout: return "timeout" case .Analytics: return "analytics" } } } // MARK: signatureAlgorithm @objc public enum SignatureAlgorithm: Int, CustomStringConvertible { case sha1 = 0 case sha256 = 1 public init(stringValue: String) { switch stringValue { case "sha1": self = .sha1 case "sha256": self = .sha256 default: self = .sha1 } } public var description: String { switch self { case .sha1: return "sha1" case .sha256: return "sha256" } } } // MARK: User Platform internal func setUserPlatform(_ platformName: String, version: String) { userPlatform = CLDUserPlatform(platform: platformName, version: version) } internal func clearUserPlatform() { userPlatform = nil } internal struct CLDUserPlatform { var platform: String var version: String init(platform: String, version: String) { self.platform = platform self.version = version } } } ================================================ FILE: Cloudinary/Classes/Core/CLDEagerTransformation.swift ================================================ // // Created by Nitzan Jaitman on 09/05/2018. // Copyright (c) 2018 Cloudinary. All rights reserved. // import Foundation open class CLDEagerTransformation: CLDTransformation { /** Set the format for the eager transformation */ fileprivate var eagerFormat: String? open func setFormat(_ format: String?) -> Self{ self.eagerFormat = format return self; } override func getStringRepresentationFromParams(_ params: [String: String]) -> String? { // return the original transformation string with a /format if set. var result = "" if let baseTransformation = super.getStringRepresentationFromParams(params) { result += baseTransformation } if let format = eagerFormat { result += "/\(format)" } return result } } ================================================ FILE: Cloudinary/Classes/Core/CLDResponsiveParams.swift ================================================ // // CLDResponsiveParams.swift // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDResponsiveParams: NSObject { public static let defaultStepSize = 50 public static let defaultMaxDimension = 350 public static let defaultMinDimension = 50 public static let defaultReloadOnSizeChange = false internal let autoWidth: Bool internal let autoHeight: Bool internal let cropMode: CLDTransformation.CLDCrop? internal let gravity: CLDTransformation.CLDGravity? internal var shouldReloadOnSizeChange = defaultReloadOnSizeChange internal var stepSizePoints = defaultStepSize internal var maxDimensionPoints = defaultMaxDimension internal var minDimensionPoints = defaultMinDimension // MARK: - Init public init(autoWidth: Bool, autoHeight: Bool, cropMode: CLDTransformation.CLDCrop?, gravity: CLDTransformation.CLDGravity?) { self.autoWidth = autoWidth self.autoHeight = autoHeight self.cropMode = cropMode self.gravity = gravity } // MARK: - Presets public static func fit () -> CLDResponsiveParams { return CLDResponsiveParams(autoWidth: true, autoHeight: true, cropMode: CLDTransformation.CLDCrop.fit, gravity: nil) } public static func autoFill () -> CLDResponsiveParams { return CLDResponsiveParams(autoWidth: true, autoHeight: true, cropMode: CLDTransformation.CLDCrop.fill, gravity: CLDTransformation.CLDGravity.auto) } // MARK: - Setters public func setStepSize(_ stepSize:Int) -> Self { self.stepSizePoints = stepSize return self } public func setMaxDimension(_ maxDimension:Int) -> Self { self.maxDimensionPoints = maxDimension return self } public func setMinDimension(_ minDimension:Int) -> Self { self.minDimensionPoints = minDimension return self } public func setReloadOnSizeChange(_ reload: Bool) -> Self { self.shouldReloadOnSizeChange = reload return self } } ================================================ FILE: Cloudinary/Classes/Core/Cloudinary.h ================================================ // // Cloudinary.h // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import //! Project version number for Cloudinary. FOUNDATION_EXPORT double CloudinaryVersionNumber; //! Project version string for Cloudinary. FOUNDATION_EXPORT const unsigned char CloudinaryVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import ================================================ FILE: Cloudinary/Classes/Core/Features/CacheSystem/Enum/HTTPStatusCode.swift ================================================ // // HTTPStatusCode.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal enum HTTPStatusCode : Int { case `continue` = 100 case switchingProtocols = 101 case processing = 102 case checkpoint = 103 case ok = 200 case created = 201 case accepted = 202 case nonAuthoritativeInformation = 203 case noContent = 204 case resetContent = 205 case partialContent = 206 case multiStatus = 207 case alreadyReported = 208 case imUsed = 226 case multipleChoices = 300 case movedPermenantly = 301 case found = 302 case seeOther = 303 case notModified = 304 case useProxy = 305 case temporaryRedirect = 307 case permanentRedirect = 308 case badRequest = 400 case unauthorized = 401 case paymentRequired = 402 case forbidden = 403 case notFound = 404 case methodNotAllowed = 405 case notAcceptable = 406 case proxyAuthenticationRequired = 407 case requestTimeout = 408 case conflict = 409 case gone = 410 case lengthRequired = 411 case preconditionFailed = 412 case payloadTooLarge = 413 case uriTooLong = 414 case unsupportedMediaType = 415 case rangeNotSatisfiable = 416 case expectationFailed = 417 case imATeapot = 418 case misdirectedRequest = 421 case unprocesssableEntity = 422 case locked = 423 case failedDependency = 424 case urpgradeRequired = 426 case preconditionRequired = 428 case tooManyRequests = 429 case requestHeadersFieldTooLarge = 431 case iisLoginTimeout = 440 case nginxNoResponse = 444 case iisRetryWith = 449 case blockedByWindowsParentalControls = 450 case unavailableForLegalReasons = 451 case nginxSSLCertificateError = 495 case nginxHTTPToHTTPS = 497 case tokenExpired = 498 case nginxClientClosedRequest = 499 case internalServerError = 500 case notImplemented = 501 case badGateway = 502 case serviceUnavailable = 503 case gatewayTimeout = 504 case httpVersionNotSupported = 505 case variantAlsoNegotiates = 506 case insufficientStorage = 507 case loopDetected = 508 case bandwidthLimitExceeded = 509 case notExtended = 510 case networkAuthenticationRequired = 511 case siteIsFrozen = 530 } extension HTTPStatusCode { internal var isInformational : Bool { switch rawValue { case 100..<200: return true default: return false } } internal var isSuccess : Bool { switch rawValue { case 200..<300: return true default: return false } } internal var isRedirection : Bool { switch rawValue { case 300..<400: return true default: return false } } internal var isClientError : Bool { switch rawValue { case 400..<500: return true default: return false } } internal var isServerError : Bool { switch rawValue { case 500..<600: return true default: return false } } internal var isError : Bool { return self.isClientError || isServerError } internal var localizedReason : String { return HTTPURLResponse.localizedString(forStatusCode: rawValue) } } // MARK: - CustomStringConvertible extension HTTPStatusCode : CustomStringConvertible { internal var description : String { return "\(rawValue) \(localizedReason)" } } ================================================ FILE: Cloudinary/Classes/Core/Features/Downloader/CLDDownloader.swift ================================================ // // CLDDownloader.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The CLDDownloader class is used to asynchronously fetch images either from the image cache if they exist or download them from a remote source. */ @objcMembers open class CLDDownloader: CLDBaseNetworkObject { // MARK: - Init internal fileprivate(set) var downloadCoordinator: CLDDownloadCoordinator! fileprivate override init() { super.init() } internal init(downloadCoordinator: CLDDownloadCoordinator) { self.downloadCoordinator = downloadCoordinator super.init(networkCoordinator: downloadCoordinator) } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/CLDBaseNetworkObject.swift ================================================ // // CLDBaseNetworkObject.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDBaseNetworkObject: NSObject { internal fileprivate(set) var networkCoordinator: CLDNetworkCoordinator! internal override init() { super.init() } internal init(networkCoordinator: CLDNetworkCoordinator) { self.networkCoordinator = networkCoordinator super.init() } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/CLDConditionExpression.swift ================================================ // // CLDConditionExpression.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDConditionExpression : CLDExpression { var relatedTransformation: CLDTransformation // MARK: - Init private init(_ expression: CLDExpression) { self.relatedTransformation = CLDTransformation() super.init(value: expression.asInternalString()) } public override init() { self.relatedTransformation = CLDTransformation() super.init() } public override init(value: String) { self.relatedTransformation = CLDTransformation() super.init(value: value) } // MARK: - Public methods @discardableResult public func and() -> Self { appendOperatorToCurrentValue(.and) return self } @objc(andString:) @discardableResult public func and(_ value: String) -> Self { appendingStringToCurrentValue(cldoperator: .and, inputValue: value) return self } @objc(andExpression:) @discardableResult public func and(_ value: CLDExpression) -> Self { return and(value.asInternalString()) } @discardableResult public func or() -> Self { appendOperatorToCurrentValue(.or) return self } @objc(orString:) @discardableResult public func or(_ value: String) -> Self { appendingStringToCurrentValue(cldoperator: .or, inputValue: value) return self } @objc(orExpression:) @discardableResult public func or(_ value: CLDExpression) -> Self { return or(value.asInternalString()) } @objc(equalToInt:) @discardableResult public func equal(to value: Int) -> Self { return equal(to: String(value)) } @objc(equalToFloat:) @discardableResult public func equal(to value: Float) -> Self { return equal(to: value.cldFloatFormat()) } @objc(equalToString:) @discardableResult public func equal(to value: String) -> Self { appendingStringToCurrentValue(cldoperator: .equal, inputValue: value) return self } @objc(equalToExpression:) @discardableResult public func equal(to value: CLDExpression) -> Self { return equal(to: value.asInternalString()) } @objc(notEqualToInt:) @discardableResult public func notEqual(to value: Int) -> Self { return notEqual(to: String(value)) } @objc(notEqualToFloat:) @discardableResult public func notEqual(to value: Float) -> Self { return notEqual(to: value.cldFloatFormat()) } @objc(notEqualToString:) @discardableResult public func notEqual(to value: String) -> Self { appendingStringToCurrentValue(cldoperator: .notEqual, inputValue: value) return self } @objc(notEqualToExpression:) @discardableResult public func notEqual(to value: CLDExpression) -> Self { return notEqual(to: value.asInternalString()) } @objc(lessThenInt:) @discardableResult public func less(then value: Int) -> Self { return less(then: String(value)) } @objc(lessThenFloat:) @discardableResult public func less(then value: Float) -> Self { return less(then: value.cldFloatFormat()) } @objc(lessThenString:) @discardableResult public func less(then value: String) -> Self { appendingStringToCurrentValue(cldoperator: .lessThen, inputValue: value) return self } @objc(lessThenExpression:) @discardableResult public func less(then value: CLDExpression) -> Self { return less(then: value.asInternalString()) } @objc(greaterThenInt:) @discardableResult public func greater(then value: Int) -> Self { return greater(then: String(value)) } @objc(greaterThenFloat:) @discardableResult public func greater(then value: Float) -> Self { return greater(then: value.cldFloatFormat()) } @objc(greaterThenString:) @discardableResult public func greater(then value: String) -> Self { appendingStringToCurrentValue(cldoperator: .greaterThen, inputValue: value) return self } @objc(greaterThenExpression:) @discardableResult public func greater(then value: CLDExpression) -> Self { return greater(then: value.asInternalString()) } @objc(lessOrEqualToInt:) @discardableResult public func lessOrEqual(to value: Int) -> Self { return lessOrEqual(to: String(value)) } @objc(lessOrEqualToFloat:) @discardableResult public func lessOrEqual(to value: Float) -> Self { return lessOrEqual(to: value.cldFloatFormat()) } @objc(lessOrEqualToString:) @discardableResult public func lessOrEqual(to value: String) -> Self { appendingStringToCurrentValue(cldoperator: .lessOrEqual, inputValue: value) return self } @objc(lessOrEqualToExpression:) @discardableResult public func lessOrEqual(to value: CLDExpression) -> Self { return lessOrEqual(to: value.asInternalString()) } @objc(greaterOrEqualToInt:) @discardableResult public func greaterOrEqual(to value: Int) -> Self { return greaterOrEqual(to: String(value)) } @objc(greaterOrEqualToFloat:) @discardableResult public func greaterOrEqual(to value: Float) -> Self { return greaterOrEqual(to: value.cldFloatFormat()) } @objc(greaterOrEqualToString:) @discardableResult public func greaterOrEqual(to value: String) -> Self { appendingStringToCurrentValue(cldoperator: .greaterOrEqual, inputValue: value) return self } @objc(greaterOrEqualToExpression:) @discardableResult public func greaterOrEqual(to value: CLDExpression) -> Self { return greaterOrEqual(to: value.asInternalString()) } @objc(insideString:) @discardableResult public func inside(_ expression: String) -> Self { guard !expression.isEmpty else { return self } appendingStringToCurrentValue(cldoperator: .inside, inputValue: expression) return self } @objc(insideExpression:) @discardableResult public func inside(_ expression: CLDExpression) -> Self { return inside(expression.asInternalString()) } @objc(notInsideString:) @discardableResult public func notInside(_ expression: String) -> Self { guard !expression.isEmpty else { return self } appendingStringToCurrentValue(cldoperator: .notInside, inputValue: expression) return self } @objc(notInsideExpression:) @discardableResult public func notInside(_ expression: CLDExpression) -> Self { return notInside(expression.asInternalString()) } @objc(valueFromString:) @discardableResult public func value(_ expression: String) -> Self { let expressionObject = CLDExpression.init(value: expression) let value: String if expressionObject.currentValue.isEmpty { value = expression } else { value = expressionObject.asString() } if !expressionObject.currentValue.isEmpty && currentKey.isEmpty { currentKey = expressionObject.currentKey currentValue = expressionObject.currentValue } else if expressionObject.currentValue.isEmpty && currentKey.isEmpty { currentKey = value } else if currentValue.isEmpty { currentValue.append(value) } else { currentValue.append(CLDVariable.elementsSeparator + value) } return self } @objc(valueFromExpression:) @discardableResult public func value(_ expression: CLDExpression) -> Self { return value(expression.asInternalString()) } // MARK: - then @objc(then) public func then() -> CLDTransformation { return relatedTransformation.ifCondition(self) } // MARK: - Class Func public override class func width() -> Self { return CLDConditionExpression(super.width()) as! Self } public override class func height() -> Self { return CLDConditionExpression(super.height()) as! Self } public override class func aspectRatio() -> Self { return CLDConditionExpression(super.aspectRatio()) as! Self } public override class func initialWidth() -> Self { return CLDConditionExpression(super.initialWidth()) as! Self } public override class func initialHeight() -> Self { return CLDConditionExpression(super.initialHeight()) as! Self } public override class func initialAspectRatio() -> Self { return CLDConditionExpression(super.initialAspectRatio()) as! Self } public override class func pageCount() -> Self { return CLDConditionExpression(super.pageCount()) as! Self } public override class func faceCount() -> Self { return CLDConditionExpression(super.faceCount()) as! Self } public override class func tags() -> Self { return CLDConditionExpression(super.tags()) as! Self } public override class func pageXOffset() -> Self { return CLDConditionExpression(super.pageXOffset()) as! Self } public override class func pageYOffset() -> Self { return CLDConditionExpression(super.pageYOffset()) as! Self } public override class func illustrationScore() -> Self { return CLDConditionExpression(super.illustrationScore()) as! Self } public override class func currentPageIndex() -> Self { return CLDConditionExpression(super.currentPageIndex()) as! Self } public override class func duration() -> Self { return CLDConditionExpression(super.duration()) as! Self } public override class func initialDuration() -> Self { return CLDConditionExpression(super.initialDuration()) as! Self } // MARK: - helper methods @objc(width:intValue:) @discardableResult public func width(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .width, operatorString: operatorString, inputValue: String(object)) } @objc(width:floatValue:) @discardableResult public func width(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .width, operatorString: operatorString, inputValue: String(object)) } @objc(width:string:) @discardableResult public func width(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .width, operatorString: operatorString, inputValue: object) } @objc(width:expression:) @discardableResult public func width(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .width, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(width:conditionExpression:) @discardableResult public func width(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .width, operatorString: operatorString, inputValue: object.asString()) } @objc(height:intValue:) @discardableResult public func height(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .height, operatorString: operatorString, inputValue: String(object)) } @objc(height:floatValue:) @discardableResult public func height(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .height, operatorString: operatorString, inputValue: String(object)) } @objc(height:string:) @discardableResult public func height(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .height, operatorString: operatorString, inputValue: object) } @objc(height:expression:) @discardableResult public func height(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .height, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(height:conditionExpression:) @discardableResult public func height(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .height, operatorString: operatorString, inputValue: object.asString()) } @objc(aspectRatio:intValue:) @discardableResult public func aspectRatio(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .aspectRatio, operatorString: operatorString, inputValue: String(object)) } @objc(aspectRatio:floatValue:) @discardableResult public func aspectRatio(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .aspectRatio, operatorString: operatorString, inputValue: String(object)) } @objc(aspectRatio:string:) @discardableResult public func aspectRatio(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .aspectRatio, operatorString: operatorString, inputValue: object) } @objc(aspectRatio:expression:) @discardableResult public func aspectRatio(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .aspectRatio, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(aspectRatio:conditionExpression:) @discardableResult public func aspectRatio(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .aspectRatio, operatorString: operatorString, inputValue: object.asString()) } @objc(initialWidth:intValue:) @discardableResult public func initialWidth(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .initialWidth, operatorString: operatorString, inputValue: String(object)) } @objc(initialWidth:floatValue:) @discardableResult public func initialWidth(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .initialWidth, operatorString: operatorString, inputValue: String(object)) } @objc(initialWidth:string:) @discardableResult public func initialWidth(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .initialWidth, operatorString: operatorString, inputValue: object) } @objc(initialWidth:expression:) @discardableResult public func initialWidth(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .initialWidth, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(initialWidth:conditionExpression:) @discardableResult public func initialWidth(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .initialWidth, operatorString: operatorString, inputValue: object.asString()) } @objc(initialHeight:intValue:) @discardableResult public func initialHeight(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .initialHeight, operatorString: operatorString, inputValue: String(object)) } @objc(initialHeight:floatValue:) @discardableResult public func initialHeight(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .initialHeight, operatorString: operatorString, inputValue: String(object)) } @objc(initialHeight:string:) @discardableResult public func initialHeight(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .initialHeight, operatorString: operatorString, inputValue: object) } @objc(initialHeight:expression:) @discardableResult public func initialHeight(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .initialHeight, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(initialHeight:conditionExpression:) @discardableResult public func initialHeight(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .initialHeight, operatorString: operatorString, inputValue: object.asString()) } @objc(initialAspectRatio:intValue:) @discardableResult public func initialAspectRatio(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .initialAspectRatio, operatorString: operatorString, inputValue: String(object)) } @objc(initialAspectRatio:floatValue:) @discardableResult public func initialAspectRatio(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .initialAspectRatio, operatorString: operatorString, inputValue: String(object)) } @objc(initialAspectRatio:string:) @discardableResult public func initialAspectRatio(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .initialAspectRatio, operatorString: operatorString, inputValue: object) } @objc(initialAspectRatio:expression:) @discardableResult public func initialAspectRatio(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .initialAspectRatio, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(initialAspectRatio:conditionExpression:) @discardableResult public func initialAspectRatio(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .initialAspectRatio, operatorString: operatorString, inputValue: object.asString()) } @objc(pageCount:intValue:) @discardableResult public func pageCount(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .pageCount, operatorString: operatorString, inputValue: String(object)) } @objc(pageCount:floatValue:) @discardableResult public func pageCount(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .pageCount, operatorString: operatorString, inputValue: String(object)) } @objc(pageCount:string:) @discardableResult public func pageCount(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .pageCount, operatorString: operatorString, inputValue: object) } @objc(pageCount:expression:) @discardableResult public func pageCount(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .pageCount, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(pageCount:conditionExpression:) @discardableResult public func pageCount(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .pageCount, operatorString: operatorString, inputValue: object.asString()) } @objc(faceCount:intValue:) @discardableResult public func faceCount(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .faceCount, operatorString: operatorString, inputValue: String(object)) } @objc(faceCount:floatValue:) @discardableResult public func faceCount(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .faceCount, operatorString: operatorString, inputValue: String(object)) } @objc(faceCount:string:) @discardableResult public func faceCount(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .faceCount, operatorString: operatorString, inputValue: object) } @objc(faceCount:expression:) @discardableResult public func faceCount(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .faceCount, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(faceCount:conditionExpression:) @discardableResult public func faceCount(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .faceCount, operatorString: operatorString, inputValue: object.asString()) } @objc(tags:intValue:) @discardableResult public func tags(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .tags, operatorString: operatorString, inputValue: String(object)) } @objc(tags:floatValue:) @discardableResult public func tags(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .tags, operatorString: operatorString, inputValue: String(object)) } @objc(tags:string:) @discardableResult public func tags(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .tags, operatorString: operatorString, inputValue: object) } @objc(tags:expression:) @discardableResult public func tags(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .tags, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(tags:conditionExpression:) @discardableResult public func tags(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .tags, operatorString: operatorString, inputValue: object.asString()) } @objc(pageXOffset:intValue:) @discardableResult public func pageXOffset(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .pageX, operatorString: operatorString, inputValue: String(object)) } @objc(pageXOffset:floatValue:) @discardableResult public func pageXOffset(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .pageX, operatorString: operatorString, inputValue: String(object)) } @objc(pageXOffset:string:) @discardableResult public func pageXOffset(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .pageX, operatorString: operatorString, inputValue: object) } @objc(pageXOffset:expression:) @discardableResult public func pageXOffset(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .pageX, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(pageXOffset:conditionExpression:) @discardableResult public func pageXOffset(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .pageX, operatorString: operatorString, inputValue: object.asString()) } @objc(pageYOffset:intValue:) @discardableResult public func pageYOffset(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .pageY, operatorString: operatorString, inputValue: String(object)) } @objc(pageYOffset:floatValue:) @discardableResult public func pageYOffset(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .pageY, operatorString: operatorString, inputValue: String(object)) } @objc(pageYOffset:string:) @discardableResult public func pageYOffset(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .pageY, operatorString: operatorString, inputValue: object) } @objc(pageYOffset:expression:) @discardableResult public func pageYOffset(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .pageY, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(pageYOffset:conditionExpression:) @discardableResult public func pageYOffset(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .pageY, operatorString: operatorString, inputValue: object.asString()) } @objc(illustrationScore:intValue:) @discardableResult public func illustrationScore(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .illustrationScore, operatorString: operatorString, inputValue: String(object)) } @objc(illustrationScore:floatValue:) @discardableResult public func illustrationScore(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .illustrationScore, operatorString: operatorString, inputValue: String(object)) } @objc(illustrationScore:string:) @discardableResult public func illustrationScore(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .illustrationScore, operatorString: operatorString, inputValue: object) } @objc(illustrationScore:expression:) @discardableResult public func illustrationScore(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .illustrationScore, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(illustrationScore:conditionExpression:) @discardableResult public func illustrationScore(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .illustrationScore, operatorString: operatorString, inputValue: object.asString()) } @objc(currentPageIndex:intValue:) @discardableResult public func currentPageIndex(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .currentPage, operatorString: operatorString, inputValue: String(object)) } @objc(currentPageIndex:floatValue:) @discardableResult public func currentPageIndex(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .currentPage, operatorString: operatorString, inputValue: String(object)) } @objc(currentPageIndex:string:) @discardableResult public func currentPageIndex(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .currentPage, operatorString: operatorString, inputValue: object) } @objc(currentPageIndex:expression:) @discardableResult public func currentPageIndex(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .currentPage, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(currentPageIndex:conditionExpression:) @discardableResult public func currentPageIndex(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .currentPage, operatorString: operatorString, inputValue: object.asString()) } @objc(duration:intValue:) @discardableResult public func duration(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .duration, operatorString: operatorString, inputValue: String(object)) } @objc(duration:floatValue:) @discardableResult public func duration(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .duration, operatorString: operatorString, inputValue: String(object)) } @objc(duration:string:) @discardableResult public func duration(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .duration, operatorString: operatorString, inputValue: object) } @objc(duration:expression:) @discardableResult public func duration(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .duration, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(duration:conditionExpression:) @discardableResult public func duration(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .duration, operatorString: operatorString, inputValue: object.asString()) } @objc(initialDuration:intValue:) @discardableResult public func initialDuration(_ operatorString: String, _ object: Int) -> Self { return predicate(expressionKey: .initialDuration, operatorString: operatorString, inputValue: String(object)) } @objc(initialDuration:floatValue:) @discardableResult public func initialDuration(_ operatorString: String, _ object: Float) -> Self { return predicate(expressionKey: .initialDuration, operatorString: operatorString, inputValue: String(object)) } @objc(initialDuration:string:) @discardableResult public func initialDuration(_ operatorString: String, _ object: String) -> Self { return predicate(expressionKey: .initialDuration, operatorString: operatorString, inputValue: object) } @objc(initialDuration:expression:) @discardableResult public func initialDuration(_ operatorString: String, _ object: CLDExpression) -> Self { return predicate(expressionKey: .initialDuration, operatorString: operatorString, inputValue: object.asInternalString()) } @objc(initialDuration:conditionExpression:) @discardableResult public func initialDuration(_ operatorString: String, _ object: CLDConditionExpression) -> Self { return predicate(expressionKey: .initialDuration, operatorString: operatorString, inputValue: object.asString()) } // MARK: - Private Methods private func appendingStringToCurrentValue(cldoperator: CLDOperators, inputValue: String) { let expression = CLDExpression.init(value: inputValue) let value: String if expression.currentValue.isEmpty { value = inputValue } else { value = expression.asString() } appendOperatorToCurrentValue(cldoperator, inputValue: value) } fileprivate func predicate(expressionKey: ExpressionKeys, operatorString: String, inputValue: String) -> Self { let stringPredicate = "\(expressionKey.asString) \(operatorString) \(inputValue)" return value(stringPredicate) } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/CLDDefinitions.swift ================================================ // // CLDDefinitions.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation // MARK: URL Type @objc public enum CLDType: Int, CustomStringConvertible { case upload, fetch, facebook, twitter, twitterName, sprite, `private`, authenticated public var description: String { get { switch self { case .upload: return "upload" case .facebook: return "facebook" case .fetch: return "fetch" case .twitter: return "twitter" case .twitterName: return "twitter_name" case .sprite: return "sprite" case .private: return "private" case .authenticated: return "authenticated" } } } } // MARK: Resource Type @objc public enum CLDUrlResourceType: Int, CustomStringConvertible { case image, raw, video, auto public var description: String { get { switch self { case .image: return "image" case .raw: return "raw" case .video: return "video" case .auto: return "auto" } } } } // MARK: - Upload Params @objc public enum CLDModeration: Int, CustomStringConvertible { case manual, webpurify public var description: String { get { switch self { case .manual: return "manual" case .webpurify: return "webpurify" } } } } // MARK: - Text Params @objc public enum CLDFontWeight: Int, CustomStringConvertible { case normal, bold public var description: String { get { switch self { case .normal: return "normal" case .bold: return "bold" } } } } @objc public enum CLDFontStyle: Int, CustomStringConvertible { case normal, italic public var description: String { get { switch self { case .normal: return "normal" case .italic: return "italic" } } } } @objc public enum CLDTextDecoration: Int, CustomStringConvertible { case none, underline public var description: String { get { switch self { case .none: return "none" case .underline: return "underline" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/CLDExpression.swift ================================================ // // CLDExpression.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDExpression: NSObject { internal enum ExpressionKeys : String, CaseIterable { case width case height case initial_width case initialWidth case initial_height case initialHeight case initial_aspect_ratio case aspect_ratio case aspectRatio case initialAspectRatio case page_count case pageCount case face_count case faceCount case illustration_score case illustrationScore case current_page case currentPage case tags case pageX case pageY case duration case initial_duration case initialDuration var asString: String { switch self { case .width : return "w" case .height: return "h" case .initial_width : return "iw" case .initialWidth : return "iw" case .initial_height: return "ih" case .initialHeight : return "ih" case .initial_aspect_ratio: return "iar" case .aspect_ratio : return "ar" case .aspectRatio : return "ar" case .initialAspectRatio : return "iar" case .page_count: return "pc" case .pageCount : return "pc" case .face_count: return "fc" case .faceCount : return "fc" case .illustration_score: return "ils" case .illustrationScore : return "ils" case .current_page: return "cp" case .currentPage : return "cp" case .tags: return "tags" case .pageX: return "px" case .pageY: return "py" case .duration: return "du" case .initial_duration: return "idu" case .initialDuration : return "idu" } } } internal var currentValue : String internal var currentKey : String private var allSpaceAndOrDash : Bool = false private let consecutiveDashesRegex : String = "[ _]+" private let userVariableRegex : String = "\\$_*[^_]+" // MARK: - Init public override init() { self.currentKey = String() self.currentValue = String() super.init() } public init(value: String) { var components = value.components(separatedBy: .whitespacesAndNewlines) self.currentKey = components.removeFirst() self.currentValue = components.joined(separator: CLDVariable.elementsSeparator) let range = NSRange(location: 0, length: value.utf16.count) let regex = try? NSRegularExpression(pattern: "^" + consecutiveDashesRegex) self.allSpaceAndOrDash = !(regex?.firstMatch(in: value, options: [], range: range) == nil) super.init() } fileprivate init(expressionKey: ExpressionKeys) { self.currentKey = expressionKey.asString self.currentValue = String() super.init() } // MARK: - class func public class func width() -> CLDExpression { return CLDExpression(expressionKey: .width) } public class func height() -> CLDExpression { return CLDExpression(expressionKey: .height) } public class func initialWidth() -> CLDExpression { return CLDExpression(expressionKey: .initialWidth) } public class func initialHeight() -> CLDExpression { return CLDExpression(expressionKey: .initialHeight) } public class func aspectRatio() -> CLDExpression { return CLDExpression(expressionKey: .aspectRatio) } public class func initialAspectRatio() -> CLDExpression { return CLDExpression(expressionKey: .initialAspectRatio) } public class func pageCount() -> CLDExpression { return CLDExpression(expressionKey: .pageCount) } public class func faceCount() -> CLDExpression { return CLDExpression(expressionKey: .faceCount) } public class func tags() -> CLDExpression { return CLDExpression(expressionKey: .tags) } public class func pageXOffset() -> CLDExpression { return CLDExpression(expressionKey: .pageX) } public class func pageYOffset() -> CLDExpression { return CLDExpression(expressionKey: .pageY) } public class func illustrationScore() -> CLDExpression { return CLDExpression(expressionKey: .illustrationScore) } public class func currentPageIndex() -> CLDExpression { return CLDExpression(expressionKey: .currentPage) } public class func duration() -> CLDExpression { return CLDExpression(expressionKey: .duration) } public class func initialDuration() -> CLDExpression { return CLDExpression(expressionKey: .initialDuration) } // MARK: - Public methods @objc(addByInt:) @discardableResult public func add(by number: Int) -> Self { appendOperatorToCurrentValue(.add, inputValue: "\(number)") return self } @objc(addByFloat:) @discardableResult public func add(by number: Float) -> Self { appendOperatorToCurrentValue(.add, inputValue: number.cldFloatFormat()) return self } @objc(addByString:) @discardableResult public func add(by number: String) -> Self { appendOperatorToCurrentValue(.add, inputValue: number) return self } @objc(subtractByInt:) @discardableResult public func subtract(by number: Int) -> Self { appendOperatorToCurrentValue(.subtract, inputValue: "\(number)") return self } @objc(subtractByFloat:) @discardableResult public func subtract(by number: Float) -> Self { appendOperatorToCurrentValue(.subtract, inputValue: number.cldFloatFormat()) return self } @objc(subtractByString:) @discardableResult public func subtract(by number: String) -> Self { appendOperatorToCurrentValue(.subtract, inputValue: number) return self } @objc(multipleByInt:) @discardableResult public func multiple(by number: Int) -> Self { appendOperatorToCurrentValue(.multiple, inputValue: "\(number)") return self } @objc(multipleByFloat:) @discardableResult public func multiple(by number: Float) -> Self { appendOperatorToCurrentValue(.multiple, inputValue: number.cldFloatFormat()) return self } @objc(multipleByString:) @discardableResult public func multiple(by number: String) -> Self { appendOperatorToCurrentValue(.multiple, inputValue: number) return self } @objc(divideByInt:) @discardableResult public func divide(by number: Int) -> Self { appendOperatorToCurrentValue(.divide, inputValue: "\(number)") return self } @objc(divideByFloat:) @discardableResult public func divide(by number: Float) -> Self { appendOperatorToCurrentValue(.divide, inputValue: number.cldFloatFormat()) return self } @objc(divideByString:) @discardableResult public func divide(by number: String) -> Self { appendOperatorToCurrentValue(.divide, inputValue: number) return self } @objc(powerByInt:) @discardableResult public func power(by number: Int) -> Self { appendOperatorToCurrentValue(.power, inputValue: "\(number)") return self } @objc(powerByFloat:) @discardableResult public func power(by number: Float) -> Self { appendOperatorToCurrentValue(.power, inputValue: number.cldFloatFormat()) return self } @objc(powerByString:) @discardableResult public func power(by number: String) -> Self { appendOperatorToCurrentValue(.power, inputValue: number) return self } // MARK: - provide content public func asString() -> String { guard !currentKey.isEmpty else { if allSpaceAndOrDash { return "_" } return String() } let key = removeExtraDashes(from: replaceAllExpressionKeys(in: currentKey)) if currentValue.isEmpty { return "\(key)" } let value = removeExtraDashes(from: replaceAllUnencodeChars(in: currentValue)) return "\(key)_\(value)" } public func asParams() -> [String : String] { guard !currentKey.isEmpty && !currentValue.isEmpty else { return [String : String]() } let key = replaceAllExpressionKeys(in: currentKey) let value = removeExtraDashes(from: replaceAllUnencodeChars(in: currentValue)) return [key:value] } internal func asInternalString() -> String { guard !currentValue.isEmpty else { return "\(currentKey)" } return "\(currentKey) \(currentValue)" } // MARK: - Private methods private func replaceAllUnencodeChars(in string: String) -> String { var wipString = string wipString = replaceAllOperators(in: string) wipString = replaceAllExpressionKeys(in: wipString) return wipString } private func replaceAllOperators(in string: String) -> String { var wipString = string CLDOperators.allCases.forEach { wipString = replace(cldOperator: $0, in: wipString) } return wipString } private func replace(cldOperator: CLDOperators, in string: String) -> String { return string.replacingOccurrences(of: cldOperator.rawValue, with: cldOperator.asString()) } private func replaceAllExpressionKeys(in string: String) -> String { var wipString = string ExpressionKeys.allCases.forEach { wipString = replace(expressionKey: $0, in: wipString) } return wipString } private func replace(expressionKey: ExpressionKeys, in string: String) -> String { var result : String! let string = removeExtraDashes(from: string) if string.contains(CLDVariable.variableNamePrefix) { let range = NSRange(location: 0, length: string.utf16.count) let regex = try? NSRegularExpression(pattern: userVariableRegex) let allRanges = regex?.matches(in: string, options: [], range: range).map({ $0.range }) ?? [] // Replace substring in between user variables. e.x $initial_aspect_ratio_$width, only '_aspect_ratio_' will be addressed. for (index, range) in allRanges.enumerated() { let location = range.length + range.location var length = range.length if index + 1 == allRanges.count { length = string.count } else { let nextRange = allRanges[index + 1] length = nextRange.location } if let stringRange = Range(NSRange(location: location, length: length - location), in: string) { result = string.replacingOccurrences(of: expressionKey.rawValue, with: expressionKey.asString, options: .regularExpression, range: stringRange) } } } else { result = string.replacingOccurrences(of: expressionKey.rawValue, with: expressionKey.asString) } return result } internal func appendOperatorToCurrentValue(_ cldoperator: CLDOperators, inputValue: String = String()) { appendOperatorToCurrentValue(cldoperator.asString(), inputValue: inputValue) } internal func appendOperatorToCurrentValue(_ cldoperator: String, inputValue: String = String()) { var stringValue = String() if !currentValue.isEmpty { stringValue.append(CLDVariable.elementsSeparator) } stringValue.append(cldoperator) if !inputValue.isEmpty { stringValue.append(CLDVariable.elementsSeparator + inputValue) } currentValue.append(stringValue) } private func removeExtraDashes(from string: String) -> String { return string.replacingOccurrences(of: consecutiveDashesRegex, with: CLDVariable.elementsSeparator, options: .regularExpression, range: nil) } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/CLDOperators.swift ================================================ // // CLDOperators.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation public enum CLDOperators: String , CaseIterable { case and = "&&" case or = "||" case multiple = "*" case divide = "/" case add = "+" case subtract = "-" case power = "^" case notInside = "notInside" case inside = "inside" case lessOrEqual = "<=" case greaterOrEqual = ">=" case lessThen = "<" case greaterThen = ">" case notEqual = "!=" case equal = "=" func asString() -> String { switch self { case .equal : return "eq" case .notEqual : return "ne" case .lessThen : return "lt" case .greaterThen : return "gt" case .lessOrEqual : return "lte" case .greaterOrEqual: return "gte" case .inside : return "in" case .notInside : return "nin" case .and: return "and" case .or : return "or" case .multiple: return "mul" case .divide : return "div" case .add : return "add" case .subtract: return "sub" case .power : return "pow" } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/CLDTransformation.swift ================================================ // // CLDTransformation.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import CoreGraphics /** The CLDTransformation class represents a full transformation performed by Cloudinay on-the-fly on a certain asset. */ @objcMembers open class CLDTransformation: NSObject { fileprivate typealias Transformation = [String : String] static fileprivate let transformationContentSeparator: String = "," static fileprivate let elementsSeparator : String = "_" fileprivate var currentTransformationParams: Transformation fileprivate var transformations : [Transformation] fileprivate var currentCondition: CLDConditionExpression? // MARK: - Init public override init() { self.currentTransformationParams = Transformation() self.transformations = [Transformation]() self.currentCondition = nil super.init() } public init(input: [CLDTransformation]) { self.currentTransformationParams = Transformation() self.transformations = [Transformation]() self.currentCondition = nil super.init() input.forEach { $0.chain() transformations.append(contentsOf: $0.transformations) } } // MARK: - Get Values open var width: String? { return getParam(.WIDTH) } open var height: String? { return getParam(.HEIGHT) } open var named: String? { return getParam(.NAMED) } open var crop: String? { return getParam(.CROP) } open var background: String? { return getParam(.BACKGROUND) } open var color: String? { return getParam(.COLOR) } open var effect: String? { return getParam(.EFFECT) } open var angle: String? { return getParam(.ANGLE) } open var opacity: String? { return getParam(.OPACITY) } open var border: String? { return getParam(.BORDER) } open var x: String? { return getParam(.X) } open var y: String? { return getParam(.Y) } open var radius: String? { return getParam(.RADIUS) } open var quality: String? { return getParam(.QUALITY) } open var defaultImage: String? { return getParam(.DEFAULT_IMAGE) } open var gravity: String? { return getParam(.GRAVITY) } open var colorSpace: String? { return getParam(.COLOR_SPACE) } open var prefix: String? { return getParam(.PREFIX) } open var overlay: String? { return getParam(.OVERLAY) } open var underlay: String? { return getParam(.UNDERLAY) } open var fetchFormat: String? { return getParam(.FETCH_FORMAT) } open var density: String? { return getParam(.DENSITY) } open var page: String? { return getParam(.PAGE) } open var delay: String? { return getParam(.DELAY) } open var rawTransformation: String? { return getParam(.RAW_TRANSFORMATION) } open var variables: String? { return getParam(.VARIABLES) } open var ifParam: String? { return getParam(.IF_PARAM) } open var flags: String? { return getParam(.FLAGS) } open var dpr: String? { return getParam(.DPR) } open var zoom: String? { return getParam(.ZOOM) } open var aspectRatio: String? { return getParam(.ASPECT_RATIO) } open var customPreFunction: String? { guard let base = getParam(.CUSTOM_FUNCTION) else { return nil } return base.removePrefix("pre:") } open var customFunction: String? { return getParam(.CUSTOM_FUNCTION) } open var audioCodec: String? { return getParam(.AUDIO_CODEC) } open var audioFrequency: String? { return getParam(.AUDIO_FREQUENCY) } open var bitRate: String? { return getParam(.BIT_RATE) } open var videoSampling: String? { return getParam(.VIDEO_SAMPLING) } open var duration: String? { return getParam(.DURATION) } open var startOffset: String? { return getParam(.START_OFFSET) } open var endOffset: String? { return getParam(.END_OFFSET) } open var offset: [String]? { guard let start = startOffset, let end = endOffset else { return nil } return [start, end] } open var videoCodec: String? { return getParam(.VIDEO_CODEC) } open var fps: String? { return getParam(.FPS) } open var keyframeInterval: String? { return getParam(.KEYFRAME_INTERVAL) } open var streamingProfile: String? { return getParam(.STREAMING_PROFILE) } fileprivate func getParam(_ param: TransformationParam) -> String? { return getParam(param.rawValue) } open func getParam(_ param: String) -> String? { return currentTransformationParams[param] } // MARK: - Set Values - Variable /** Set a variable. - parameter name: The variable's name. - parameter value: The variable's value. - returns: The same instance of CLDTransformation. */ @objc(setVariable:intValue:) @discardableResult public func setVariable(_ name: String, int value: Int) -> Self { let variable = CLDVariable(name: name, value: String(value)) return setVariable(variable) } /** Set a variable. - parameter name: The variable's name. - parameter value: The variable's value. - returns: The same instance of CLDTransformation. */ @objc(setVariable:floatValue:) @discardableResult public func setVariable(_ name: String, float value: Float) -> Self { let variable = CLDVariable(name: name, value: value.cldFloatFormat()) return setVariable(variable) } /** Set a variable. - parameter name: The variable's name. - parameter value: The variable's value. - returns: The same instance of CLDTransformation. */ @objc(setVariable:string:) @discardableResult public func setVariable(_ name: String, string value: String) -> Self { let variable = CLDVariable(name: name, value: value) return setVariable(variable) } /** Set a variable. - parameter name: The variable's name. - parameter values: The variable's value. - returns: The same instance of CLDTransformation. */ @objc(setVariable:valuesArray:) @discardableResult public func setVariable(_ name: String, values: [String]) -> Self { let variable = CLDVariable(name: name, values: values) return setVariable(variable) } /** Set a variable. - parameter variable: The variable to set. - returns: The same instance of CLDTransformation. */ @objc(setVariableWithVariable:) @discardableResult public func setVariable(_ variable: CLDVariable) -> Self { guard variable.isValid else { return self } return setParam(variable.name, value: variable.value) } /** Set an array of variables. - parameter variables: The variables to set. - returns: The same instance of CLDTransformation. */ @objc(setVariablesWithVariablesArray:) @discardableResult public func setVariables(_ variables: [CLDVariable]) -> Self { let joined = variables.filter { $0.isValid }.map { $0.asString() }.joined(separator: CLDTransformation.transformationContentSeparator) setParam(TransformationParam.VARIABLES.rawValue, value: joined) return self } // MARK: - Set Values - Width /** Set the image width. - parameter width: The width to set. - returns: The same instance of CLDTransformation. */ @objc(setWidthWithInt:) @discardableResult open func setWidth(_ width: Int) -> Self { return setWidth(String(width)) } /** Set the image width. - parameter width: The width to set. - returns: The same instance of CLDTransformation. */ @objc(setWidthWithFloat:) @discardableResult open func setWidth(_ width: Float) -> Self { return setWidth(width.cldFloatFormat()) } /** Set the image width. - parameter width: The width to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setWidth(_ width: String) -> Self { return setParam(TransformationParam.WIDTH, expression: CLDExpression(value: width), fallback: width) } // MARK: - Set Values - Height /** Set the image height. - parameter height: The height to set. - returns: The same instance of CLDTransformation. */ @objc(setHeightWithInt:) @discardableResult open func setHeight(_ height: Int) -> Self { return setHeight(String(height)) } /** Set the image height. - parameter height: The height to set. - returns: The same instance of CLDTransformation. */ @objc(setHeightWithFloat:) @discardableResult open func setHeight(_ height: Float) -> Self { return setHeight(height.cldFloatFormat()) } /** Set the image height. - parameter height: The height to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setHeight(_ height: String) -> Self { return setParam(TransformationParam.HEIGHT, expression: CLDExpression(value: height), fallback: height) } // MARK: - Set Values - Named /** A named transformation is a set of image transformations that has been given a custom name for easy reference. It is useful to define a named transformation when you have a set of relatively complex transformations that you use often and that you want to easily reference. - parameter names: The names of the transformations to set. - returns: The same instance of CLDTransformation. */ @objc(setNamedWithArray:) @discardableResult open func setNamed(_ names: [String]) -> Self { return setNamed(names.joined(separator: ".")) } /** A named transformation is a set of image transformations that has been given a custom name for easy reference. It is useful to define a named transformation when you have a set of relatively complex transformations that you use often and that you want to easily reference. - parameter names: The names of the transformations to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setNamed(_ names: String) -> Self { return setParam(TransformationParam.NAMED, value: names) } // MARK: - Set Values - Crop /** Set the image crop. - parameter crop: The crop to set. - returns: The same instance of CLDTransformation. */ @objc(setCropWithCrop:) @discardableResult open func setCrop(_ crop: CLDCrop) -> Self { return setCrop(String(describing: crop)) } /** Set the image crop. - parameter crop: The crop to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setCrop(_ crop: String) -> Self { return setParam(TransformationParam.CROP, value: crop) } // MARK: - Set Values - Background /** Defines the background color to use instead of transparent background areas when converting to JPG format or using the pad crop mode. The background color can be set as an RGB hex triplet (e.g. '#3e2222'), a 3 character RGB hex (e.g. '#777') or a named color (e.g. 'green'). - parameter background: The background color to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setBackground(_ background: String) -> Self { return setParam(TransformationParam.BACKGROUND, value: background.replacingOccurrences(of: "#", with: "rgb:")) } // MARK: - Set Values - Color /** Customize the color to use together with: text captions, the shadow effect and the colorize effect. The color can be set as an RGB hex triplet (e.g. '#3e2222'), a 3 character RGB hex (e.g. '#777') or a named color (e.g. 'green'). - parameter color: The color to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setColor(_ color: String) -> Self { return setParam(TransformationParam.COLOR, value: color.replacingOccurrences(of: "#", with: "rgb:")) } // MARK: - Set Values - Effect /** Apply a filter or an effect on an image. - parameter effect: The effect to apply. - returns: The same instance of CLDTransformation. */ @objc(setEffectWithEffect:) @discardableResult open func setEffect(_ effect: CLDEffect) -> Self { return setEffect(String(describing: effect)) } /** Apply a filter or an effect on an image. The value includes the name of the effect and an additional parameter that controls the behavior of the specific effect. - parameter effect: The effect to apply. - returns: The same instance of CLDTransformation. */ @objc(setEffectWithArtFilter:) @discardableResult open func setEffect(_ effect: CLDArtFilters) -> CLDTransformation { return setEffect(.art, param: String(describing: effect)) } /** Apply a filter or an effect on an image. The value includes the name of the effect and an additional parameter that controls the behavior of the specific effect. - parameter effect: The effect to apply. - parameter param: The effect value to apply. - returns: The same instance of CLDTransformation. */ @objc(setEffectWithEffect:param:) @discardableResult open func setEffect(_ effect: CLDEffect, param: String) -> Self { return setEffect(String(describing: effect), param: param) } /** Apply a filter or an effect on an image. The value includes the name of the effect and an additional parameter that controls the behavior of the specific effect. - parameter effect: The effect to apply. - parameter param: The effect value to apply. - returns: The same instance of CLDTransformation. */ @discardableResult open func setEffect(_ effect: String, param: String) -> Self { return setEffect("\(effect):\(param)") } /** Apply a filter or an effect on an image. - parameter effect: The effect to apply. - returns: The same instance of CLDTransformation. */ @discardableResult open func setEffect(_ effect: String) -> Self { return setParam(TransformationParam.EFFECT, value: effect) } // MARK: - Set Values - Angle /** Rotate or flip an image by the given degrees or automatically according to its orientation or available meta-data. - parameter angle: The angle to rotate. - returns: The same instance of CLDTransformation. */ @objc(setAngleWithInt:) @discardableResult open func setAngle(_ angle: Int) -> Self { return setAngle(String(angle)) } /** Rotate or flip an image by the given degrees or automatically according to its orientation or available meta-data. - parameter angles: The angles to rotate. - returns: The same instance of CLDTransformation. */ @objc(setAngleWithArray:) @discardableResult open func setAngle(_ angles: [String]) -> Self { return setAngle(angles.joined(separator: ".")) } /** Rotate or flip an image by the given degrees or automatically according to its orientation or available meta-data. - parameter angle: The angle to rotate. - returns: The same instance of CLDTransformation. */ @discardableResult open func setAngle(_ angle: String) -> Self { return setParam(TransformationParam.ANGLE, value: angle) } // MARK: - Set Values - Opacity /** Adjust the opacity of the image and make it semi-transparent. 100 means opaque, while 0 is completely transparent. - parameter opacity: The opacity level to apply. - returns: The same instance of CLDTransformation. */ @objc(setOpacityWithInt:) @discardableResult open func setOpacity(_ opacity: Int) -> Self { return setOpacity(String(opacity)) } /** Adjust the opacity of the image and make it semi-transparent. 100 means opaque, while 0 is completely transparent. - parameter opacity: The opacity level to apply. - returns: The same instance of CLDTransformation. */ @discardableResult open func setOpacity(_ opacity: String) -> Self { return setParam(TransformationParam.OPACITY, value: opacity) } // MARK: - Set Values - Border /** Add a solid border around the image. - parameter width: The border width. - parameter color: The border color. - returns: The same instance of CLDTransformation. */ @discardableResult open func setBorder(_ width: Int, color: String) -> Self { return setBorder("\(width)px_solid_\(color)") } /** Add a solid border around the image. Should conform to the form: [width]px_solid_[color], e.g - 5px_solid_#111111 or 5px_solid_red - parameter border: The border to add. - returns: The same instance of CLDTransformation. */ @discardableResult open func setBorder(_ border: String) -> Self { return setParam(TransformationParam.BORDER, value: border.replacingOccurrences(of: "#", with: "rgb:")) } // MARK: - Set Values - X /** Horizontal position for custom-coordinates based cropping, overlay placement and certain region related effects. - parameter x: The x position to add. - returns: The same instance of CLDTransformation. */ @objc(setXFromInt:) @discardableResult open func setX(_ x: Int) -> Self { return setX(String(x)) } /** Horizontal position for custom-coordinates based cropping, overlay placement and certain region related effects. - parameter x: The x position to add. - returns: The same instance of CLDTransformation. */ @objc(setXFromFloat:) @discardableResult open func setX(_ x: Float) -> Self { return setX(x.cldFloatFormat()) } /** Horizontal position for custom-coordinates based cropping, overlay placement and certain region related effects. - parameter x: The x position to add. - returns: The same instance of CLDTransformation. */ @discardableResult open func setX(_ x: String) -> Self { return setParam(TransformationParam.X, expression: CLDExpression(value: x), fallback: x) } // MARK: - Set Values - Y /** Vertical position for custom-coordinates based cropping and overlay placement. - parameter y: The y position to add. - returns: The same instance of CLDTransformation. */ @objc(setYFromInt:) @discardableResult open func setY(_ y: Int) -> Self { return setY(String(y)) } /** Vertical position for custom-coordinates based cropping and overlay placement. - parameter y: The y position to add. - returns: The same instance of CLDTransformation. */ @objc(setYFromFloat:) @discardableResult open func setY(_ y: Float) -> Self { return setY(y.cldFloatFormat()) } /** Vertical position for custom-coordinates based cropping and overlay placement. - parameter y: The y position to add. - returns: The same instance of CLDTransformation. */ @discardableResult open func setY(_ y: String) -> Self { return setParam(TransformationParam.Y, expression: CLDExpression(value: y), fallback: y) } // MARK: - Set Values - Radius /** Round the corners of an image or make it completely circular or oval (ellipse). - parameter radius: The radius to apply. - returns: The same instance of CLDTransformation. */ @objc(setRadiusFromInt:) @discardableResult open func setRadius(_ radius: Int) -> Self { return setRadius(String(radius)) } /** Round the corners of an image or make it completely circular or oval (ellipse). - parameter radius: The radius to apply. - returns: The same instance of CLDTransformation. */ @discardableResult open func setRadius(_ radius: String) -> Self { return setParam(TransformationParam.RADIUS, expression: CLDExpression(value: radius), fallback: radius) } /** Support an array value for radius - parameter radius: - The radius to apply- an array of values - returns: The same instance of CLDTransformation. */ @objc(setRadiusFromArray:) @discardableResult open func setRadius(_ radius: [Any]) -> Self { return setRadius(radius.map{String(describing: $0)}.joined(separator:":")) } // MARK: - Set Values - Quality /** Set the image quality for the transformation, see CLDQuality for options. - parameter quality: A CLDQuality instance containing the quality settings. - returns: The same instance of CLDTransformation. */ @objc(setQualityFromQuality:) @discardableResult open func setQuality(_ quality: CLDQuality) -> Self { return setQuality(quality.description) } /** Control the JPEG, WebP, GIF, JPEG XR and JPEG 2000 compression quality. 1 is the lowest quality and 100 is the highest. Reducing quality generates JPG images much smaller in file size. The default values are: * JPEG: 90 * WebP: 80 (100 quality for WebP is lossless) * GIF: lossless by default. 80 if the `lossy` flag is added * JPEG XR and JPEG 2000: 70 - parameter quality: The quality to apply. - returns: The same instance of CLDTransformation. */ @objc(setQualityFromInt:) @discardableResult @available(*, deprecated, message: "Use setQuality(CLDQuality)") open func setQuality(_ quality: Int) -> Self { return setQuality(String(quality)) } /** Control the JPEG, WebP, GIF, JPEG XR and JPEG 2000 compression quality. 1 is the lowest quality and 100 is the highest. Reducing quality generates JPG images much smaller in file size. The default values are: * JPEG: 90 * WebP: 80 (100 quality for WebP is lossless) * GIF: lossless by default. 80 if the `lossy` flag is added * JPEG XR and JPEG 2000: 70 - parameter quality: The quality to apply. - returns: The same instance of CLDTransformation. */ @discardableResult open func setQuality(_ quality: String) -> Self { return setParam(TransformationParam.QUALITY, value: quality) } // MARK: - Set Values - DefaultImage /** Specify the public ID of a placeholder image to use if the requested image or social network picture does not exist. - parameter defaultImage: The identifier of the default image. - returns: The same instance of CLDTransformation. */ @discardableResult open func setDefaultImage(_ defaultImage: String) -> Self { return setParam(TransformationParam.DEFAULT_IMAGE, value: defaultImage) } // MARK: - Set Values - Gravity /** Decides which part of the image to keep while 'crop', 'pad' and 'fill' crop modes are used. - parameter gravity: The gravity to apply. - returns: The same instance of CLDTransformation. */ @objc(setGravityWithGravity:) @discardableResult open func setGravity(_ gravity: CLDGravity) -> Self { return setGravity(String(describing: gravity)) } /** Decides which part of the image to keep while 'crop', 'pad' and 'fill' crop modes are used. - parameter gravity: The gravity to apply. - returns: The same instance of CLDTransformation. */ @discardableResult open func setGravity(_ gravity: String) -> Self { return setParam(TransformationParam.GRAVITY, value: gravity) } // MARK: - Set Values - ColorSpace /** Set the transformation color space. - parameter colorSpace: The color space to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setColorSpace(_ colorSpace: String) -> Self { return setParam(TransformationParam.COLOR_SPACE, value: colorSpace) } // MARK: - Set Values - Prefix /** Set the transformation prefix. - parameter prefix: The prefix to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setPrefix(_ prefix: String) -> Self { return setParam(TransformationParam.PREFIX, value: prefix) } // MARK: - Set Values - Overlay /** Add an overlay over the base image. You can control the dimension and position of the overlay using the width, height, x, y and gravity parameters. The overlay can take one of the following forms: identifier can be a public ID of an uploaded image or a specific image kind, public ID and settings. **You can use the convenience method `addOverlayWithLayer`** - parameter overlay: The overlay to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setOverlay(_ overlay: String) -> Self { return setParam(TransformationParam.OVERLAY, value: overlay) } // MARK: - Set Values - Underlay /** Add an underlay image below a base partially-transparent image. You can control the dimensions and position of the underlay using the width, height, x, y and gravity parameters. The identifier can be a public ID of an uploaded image or a specific image kind, public ID and settings. The underlay parameter shares the same features as the overlay parameter. **You can use the convenience method `addUnderlayWithLayer`** - parameter underlay: The underlay to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setUnderlay(_ underlay: String) -> Self { return setParam(TransformationParam.UNDERLAY, value: underlay) } // MARK: - Set Values - FetchFormat /** Force format conversion to the given image format for remote 'fetch' URLs and auto uploaded images that already have a different format as part of their URLs. - parameter fetchFormat: The fetchFormat to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setFetchFormat(_ fetchFormat: String) -> Self { return setParam(TransformationParam.FETCH_FORMAT, value: fetchFormat) } // MARK: - Set Values - Density /** Control the density to use while converting a PDF document to images. (range: 50-300, default is 150) - parameter density: The density to use. - returns: The same instance of CLDTransformation. */ @objc(setDensityWithInt:) @discardableResult open func setDensity(_ density: Int) -> Self { return setDensity(String(density)) } /** Control the density to use while converting a PDF document to images. (range: 50-300, default is 150) - parameter density: The density to use. - returns: The same instance of CLDTransformation. */ @discardableResult open func setDensity(_ density: String) -> Self { return setParam(TransformationParam.DENSITY, value: density) } // MARK: - Set Values - Page /** Given a multi-page file (PDF, animated GIF, TIFF), generate an image of a single page using the given index. - parameter page: The index of the page to use to use. - returns: The same instance of CLDTransformation. */ @objc(setPageWithInt:) @discardableResult open func setPage(_ page: Int) -> Self { return setPage(String(page)) } /** Given a multi-page file (PDF, animated GIF, TIFF), generate an image of a single page using the given index. - parameter page: The index of the page to use to use. - returns: The same instance of CLDTransformation. */ @discardableResult open func setPage(_ page: String) -> Self { return setParam(TransformationParam.PAGE, value: page) } // MARK: - Set Values - Delay /** Controls the time delay between the frames of an animated image, in milliseconds. - parameter delay: The delay between the frames of an animated image, in milliseconds. - returns: The same instance of CLDTransformation. */ @objc(setDelayWithInt:) @discardableResult open func setDelay(_ delay: Int) -> Self { return setDelay(String(delay)) } /** Controls the time delay between the frames of an animated image, in milliseconds. - parameter delay: The delay between the frames of an animated image, in milliseconds. - returns: The same instance of CLDTransformation. */ @discardableResult open func setDelay(_ delay: String) -> Self { return setParam(TransformationParam.DELAY, value: delay) } // MARK: - Set Values - RawTransformation /** Add a raw transformation, it will be appended to the other transformation parameter. the transformation must conform to [Cloudinary's transformation documentation](http://cloudinary.com/documentation/image_transformation_reference) - parameter rawTransformation: The raw transformation to add. - returns: The same instance of CLDTransformation. */ @discardableResult open func setRawTransformation(_ rawTransformation: String) -> Self { return setParam(TransformationParam.RAW_TRANSFORMATION, value: rawTransformation) } // MARK: - Set Values - Flags /** Set one or more flags that alter the default transformation behavior. - parameter flags: An array of the flags to apply. - returns: The same instance of CLDTransformation. */ @objc(setFlagsWithArray:) @discardableResult open func setFlags(_ flags: [String]) -> Self { return setFlags(flags.joined(separator: ".")) } /** Set one or more flags that alter the default transformation behavior. - parameter flags: An array of the flags to apply. - returns: The same instance of CLDTransformation. */ @discardableResult open func setFlags(_ flags: String) -> Self { return setParam(TransformationParam.FLAGS, value: flags) } // MARK: - Set Values - DPR /** Deliver the image in the specified device pixel ratio. - parameter dpr: The DPR ot set. - returns: The same instance of CLDTransformation. */ @objc(setDprWithFloat:) @discardableResult open func setDpr(_ dpr: Float) -> Self { return setDpr(dpr.cldFloatFormat()) } /** Deliver the image in the specified device pixel ratio. The parameter accepts any positive float value. - parameter dpr: The DPR ot set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setDpr(_ dpr: String) -> Self { return setParam(TransformationParam.DPR, value: dpr) } // MARK: - Set Values - Zoom /** Control how much of the original image surrounding the face to keep when using either the 'crop' or 'thumb' cropping modes with face detection. default is 1.0. - parameter zoom: The zoom ot set. - returns: The same instance of CLDTransformation. */ @objc(setZoomWithFloat:) @discardableResult open func setZoom(_ zoom: Float) -> Self { return setZoom(zoom.cldFloatFormat()) } /** Control how much of the original image surrounding the face to keep when using either the 'crop' or 'thumb' cropping modes with face detection. default is 1.0. - parameter zoom: The zoom ot set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setZoom(_ zoom: String) -> Self { return setParam(TransformationParam.ZOOM, value: zoom) } // MARK: - Set Values - AspectRatio /** Resize or crop the image to a new aspect ratio using a nominator and dominator (e.g. 16 and 9 for 16:9). This parameter is used together with a specified crop mode that determines how the image is adjusted to the new dimensions. - parameter nominator: The nominator ot use. - parameter denominator: The dominator ot use. - returns: The same instance of CLDTransformation. */ @discardableResult open func setAspectRatio(nominator: Int, denominator: Int) -> Self { return setAspectRatio("\(nominator):\(denominator)") } /** Resize or crop the image to a new aspect ratio. This parameter is used together with a specified crop mode that determines how the image is adjusted to the new dimensions. - parameter aspectRatio: The aspect ratio ot use. - returns: The same instance of CLDTransformation. */ @objc(setAspectRatioWithFloat:) @discardableResult open func setAspectRatio(_ aspectRatio: Float) -> Self { return setAspectRatio(aspectRatio.cldFloatFormat()) } /** Resize or crop the image to a new aspect ratio. This parameter is used together with a specified crop mode that determines how the image is adjusted to the new dimensions. - parameter aspectRatio: The aspect ratio ot use. - returns: The same instance of CLDTransformation. */ @discardableResult open func setAspectRatio(_ aspectRatio: String) -> Self { return setParam(TransformationParam.ASPECT_RATIO, value: aspectRatio) } // MARK: - Set Values - CustomPreFunction /** Set a custom pre-function, such as a call to a lambda function or a web-assembly function. - parameter customPreFunction: The custom pre-function to perform, see CLDCustomFunction. - returns: The same instance of CLDTransformation. */ @discardableResult open func setCustomPreFunction(_ customPreFunction: CLDCustomFunction) -> Self { return setParam(TransformationParam.CUSTOM_FUNCTION, value: "pre:\(customPreFunction.description)") } // MARK: - Set Values - CustomFunction /** Set a custom function, such as a call to a lambda function or a web-assembly function. - parameter customFunction: The custom function to perform, see CLDCustomFunction. - returns: The same instance of CLDTransformation. */ @discardableResult open func setCustomFunction(_ customFunction: CLDCustomFunction) -> Self { return setParam(TransformationParam.CUSTOM_FUNCTION, value: customFunction.description) } // MARK: - Set Values - AudioCodec /** Use the audio_codec parameter to set the audio codec or remove the audio channel completely as follows: * **none** removes the audio channel * **aac** (mp4 or flv only) * **vorbis** (ogv or webm only) * **mp3** (mp4 or flv only) - parameter audioCodec: The audio codec ot set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setAudioCodec(_ audioCodec: String) -> Self { return setParam(TransformationParam.AUDIO_CODEC, value: audioCodec) } // MARK: - Set Values - AudioFrequency /** Use the audio_frequency parameter to control the audio sampling frequency. This parameter represents an integer value in Hz. See the documentation in the [Video transformations reference table](http://cloudinary.com/documentation/video_manipulation_and_delivery#video_transformations_reference) for the possible values. - parameter audioFrequency: The audio frequency ot set. - returns: The same instance of CLDTransformation. */ @objc(setAudioFrequencyWithInt:) @discardableResult open func setAudioFrequency(_ audioFrequency: Int) -> Self { return setAudioFrequency(String(audioFrequency)) } /** Use the audio_frequency parameter to control the audio sampling frequency. This parameter represents an integer value in Hz. See the documentation in the [Video transformations reference table](http://cloudinary.com/documentation/video_manipulation_and_delivery#video_transformations_reference) for the possible values. - parameter audioFrequency: The audio frequency ot set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setAudioFrequency(_ audioFrequency: String) -> Self { return setParam(TransformationParam.AUDIO_FREQUENCY, value: audioFrequency) } // MARK: - Set Values - BitRate /** Use the bit_rate parameter for advanced control of the video bit rate. This parameter controls the number of bits used to represent the video data. The higher the bit rate, the higher the visual quality of the video, but the larger the file size as well. - parameter bitRate: The bit rate ot set. - returns: The same instance of CLDTransformation. */ @objc(setBitRateWithInt:) @discardableResult open func setBitRate(_ bitRate: Int) -> Self { return setBitRate(String(bitRate)) } /** Use the bit_rate parameter for advanced control of the video bit rate. This parameter controls the number of bits used to represent the video data. The higher the bit rate, the higher the visual quality of the video, but the larger the file size as well. - parameter bitRate: The bit rate ot set in kilobytes. - returns: The same instance of CLDTransformation. */ @discardableResult open func setBitRate(kb bitRate: Int) -> Self { return setBitRate("\(bitRate)k") } /** Use the bit_rate parameter for advanced control of the video bit rate. This parameter controls the number of bits used to represent the video data. The higher the bit rate, the higher the visual quality of the video, but the larger the file size as well. Bit rate can take one of the following values: * An integer e.g. 120000. * A string supporting ‘k’ and ‘m’ (kilobits and megabits respectively) e.g. 500k or 2m. - parameter bitRate: The bit rate ot set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setBitRate(_ bitRate: String) -> Self { return setParam(TransformationParam.BIT_RATE, value: bitRate) } // MARK: - Set Values - VideoSampling /** The total number of frames to sample from the original video. The frames are spread out over the length of the video, e.g. 20 takes one frame every 5%. - parameter frames: The video sampling frames ot set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setVideoSampling(frames: Int) -> Self { return setVideoSampling(String(frames)) } /** Controls the time delay between the frames of an animated image, in milliseconds. - parameter delay: The delay ot set in milliseconds. - returns: The same instance of CLDTransformation. */ @discardableResult open func setVideoSampling(delay: Float) -> Self { return setVideoSampling("\(delay.cldFloatFormat())s") } /** Relevant for conversion of video to animated GIF or WebP. If not specified, the resulting GIF or WebP samples the whole video (up to 400 frames, at up to 10 frames per second). By default the duration of the animated image is the same as the duration of the video, no matter how many frames are sampled from the original video (use the delay parameter to adjust the amount of time between frames). - parameter videoSampling: The video sampling ot set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setVideoSampling(_ videoSampling: String) -> Self { return setParam(TransformationParam.VIDEO_SAMPLING, value: videoSampling) } // MARK: - Set Values - Duration /** Offset in seconds or percent of a video, normally used together with the start_offset and end_offset parameters. Used to specify: * The duration the video displays. * The duration an overlay displays. - parameter seconds: The duration to set in seconds. - returns: The same instance of CLDTransformation. */ @discardableResult open func setDuration(seconds: Float) -> Self { return setDuration(seconds.cldFloatFormat()) } /** Offset in seconds or percent of a video, normally used together with the start_offset and end_offset parameters. Used to specify: * The duration the video displays. * The duration an overlay displays. - parameter percent: The duration percent to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setDuration(percent: Int) -> Self { return setDuration("\(percent)p") } /** Offset in seconds or percent of a video, normally used together with the start_offset and end_offset parameters. Used to specify: * The duration the video displays. * The duration an overlay displays. - parameter duration: The duration to set in seconds. - returns: The same instance of CLDTransformation. */ @discardableResult open func setDuration(_ duration: String) -> Self { return setParam(TransformationParam.DURATION, value: duration) } // MARK: - Set Values - StartOffset /** Set an offset in seconds to cut a video at the start. - parameter seconds: The start time to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setStartOffset(seconds: Float) -> Self { return setStartOffset(seconds.cldFloatFormat()) } /** Set an offset in percent to cut a video at the start. - parameter percent: The percent of time to cut. - returns: The same instance of CLDTransformation. */ @discardableResult open func setStartOffset(percent: Int) -> Self { return setStartOffset("\(percent)p") } /** Set an offset in seconds or percent of a video to cut a video at the start. - parameter duration: The start time to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setStartOffset(_ duration: String) -> Self { return setParam(TransformationParam.START_OFFSET, value: duration) } // MARK: - Set Values - EndOffset /** Set an offset in seconds to cut a video at the end. - parameter seconds: The end time to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setEndOffset(seconds: Float) -> Self { return setEndOffset(seconds.cldFloatFormat()) } /** Set an offset in percent to cut a video at the end. - parameter percent: The percent of time to cut. - returns: The same instance of CLDTransformation. */ @discardableResult open func setEndOffset(percent: Int) -> Self { return setEndOffset("\(percent)p") } /** Set an offset in seconds or percent of a video to cut a video at the end. - parameter duration: The end time to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setEndOffset(_ duration: String) -> Self { return setParam(TransformationParam.END_OFFSET, value: duration) } // MARK: - Set Values - VideoCodecAndProfileAndLevel /** Used to determine the video codec, video profile and level to use. You can set this parameter to auto instead. - parameter videoCodec: The video codec to set. - parameter videoProfile: The video profile to set. - parameter level: The level to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setVideoCodecAndProfileAndLevel(_ videoCodec: String, videoProfile: String, level: String? = nil) -> Self { return level == nil ? setVideoCodec("\(videoCodec):\(videoProfile)") : setVideoCodec("\(videoCodec):\(videoProfile):\(level!)") } // MARK: - Set Values - VideoCodecAndProfileAndLevelAndBframes /** Used to determine the video codec, video profile and level to use. You can set this parameter to auto instead. - parameter videoCodec: The video codec to set. - parameter videoProfile: The video profile to set. - parameter level: The level to set. - parameter bframes: Should use B-frames. - returns: The same instance of CLDTransformation. */ @discardableResult open func setVideoCodecAndProfileAndLevelAndBFrames(_ videoCodec: String, videoProfile: String, level: String? = nil, bframes: Bool? = nil) -> Self { var videoCodec = "\(videoCodec):\(videoProfile)" if let level = level { videoCodec.append(":\(level)" + (bframes == false ? ":bframes_no" : "")) } return setVideoCodec(videoCodec) } // MARK: - Set Values - VideoCodec /** Used to determine the video codec to use. You can set this parameter to auto instead. - parameter videoCodec: The video codec to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setVideoCodec(_ videoCodec: String) -> Self { return setParam(TransformationParam.VIDEO_CODEC, value: videoCodec) } // MARK: Setters fileprivate func setParam(_ key: TransformationParam, value: String) -> Self { return setParam(key.rawValue, value: value) } fileprivate func setParam(_ key: TransformationParam, expression: CLDExpression, fallback: String) -> Self { if expression.currentValue.isEmpty { return setParam(key, value: fallback) } else { return setParam(key, value: expression.asString()) } } @discardableResult open func setParam(_ key: String, value: String) -> Self { currentTransformationParams[key] = value return self } // MARK: - Convenience /** A convenience method to set video cutting in seconds. Must provide an array with exactly 2 values: 1. start offset. 2. end offset. - parameter seconds: The array of both start and end offsets to set. - returns: The same instance of CLDTransformation. */ @discardableResult func setOffset(seconds: [Float]) -> Self { guard let start = seconds.first, let end = seconds.last else { return self } return setOffset([start.cldFloatFormat(), end.cldFloatFormat()]) } /** A convenience method to set video cutting in percent. Must provide an array with exactly 2 values: 1. start offset. 2. end offset. - parameter seconds: The array of both start and end offsets to set. - returns: The same instance of CLDTransformation. */ @discardableResult func setOffset(percents: [Int]) -> Self { guard let start = percents.first, let end = percents.last else { return self } return setOffset(["\(start)p", "\(end)p"]) } /** A convenience method to set video cutting in seconds or percent. Should provide an array with 2 values: 1. start offset. 2. end offset. Or one value for both. - parameter seconds: The array of both start and end offsets to set. - returns: The same instance of CLDTransformation. */ @discardableResult func setOffset(_ durations: [String]) -> Self { guard let start = durations.first, let end = durations.last else { return self } setStartOffset(start) setEndOffset(end) return self } /** Shortcut to set video cutting in seconds using a combination of start_offset and end_offset values. - parameter startSeconds: The start time to set. - parameter endSeconds: The end time to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setStartOffsetAndEndOffset(startSeconds: Float, endSeconds: Float) -> Self { return setStartOffset(seconds: startSeconds).setEndOffset(seconds: endSeconds) } /** Shortcut to set video cutting in percent of video using a combination of start_offset and end_offset values. - parameter startPercent: The start time to set. - parameter endPercent: The end time to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setStartOffsetAndEndOffset(startPercent: Int, endPercent: Int) -> Self { return setStartOffset(percent: startPercent).setEndOffset(percent: endPercent) } /** Set the frames-per-second of the video. - parameter fps: A CLDFps configured with the required parameters (see CLDFps for details). - returns: The same instance of CLDTransformation. */ @discardableResult open func setFps(_ fps: CLDFps) -> Self { return setParam(TransformationParam.FPS, value: fps.description) } /** Set the frames-per-second of the video. - parameter fps: Frames per second as string. - returns: The same instance of CLDTransformation. */ @discardableResult @objc(setFpsWithString:) open func setFps(_ fps: String) -> Self { return setFps(CLDFps.fromString(fps)) } /** Set the frames-per-second of the video. - parameter fps: Frames per seconds as float. - returns: The same instance of CLDTransformation. */ @discardableResult @objc(setFpsWithFloat:) open func setFps(_ fps: Float) -> Self { return setFps(CLDFps.fromFloat(fps)) } /** Set an overlay using the helper class CLDLayer. - parameter layer: The layer to add as an overlay. - returns: The same instance of CLDTransformation. */ @discardableResult open func setOverlayWithLayer(_ layer: CLDLayer) -> Self { if let layerString = layer.asString() { return setOverlay(layerString) } else { return setOverlay("") } } /** Set an underlay using the helper class CLDLayer. - parameter layer: The layer to add as an underlay. - returns: The same instance of CLDTransformation. */ @discardableResult open func setUnderlayWithLayer(_ layer: CLDLayer) -> Self { if let layerString = layer.asString() { return setUnderlay(layerString) } else { return setUnderlay("") } } /** A convenience method to set the transformation X and Y parameters. - parameter point: The top left pont to set. - returns: The same instance of CLDTransformation. */ @discardableResult open func setTopLeftPoint(_ point: CGPoint) -> Self { return setX(Float(point.x)).setY(Float(point.y)) } @discardableResult open func setKeyframeInterval(interval: Float) -> Self { return setKeyframeInterval(interval.description) } @discardableResult open func setKeyframeInterval(_ interval: String) -> Self { return setParam(TransformationParam.KEYFRAME_INTERVAL, value: interval) } @discardableResult open func setStreamingProfile(_ streamingProfile: String) -> Self { return setParam(TransformationParam.STREAMING_PROFILE, value: streamingProfile) } // MARK: - Actions /** Cloudinary supports powerful image transformations that are applied on the fly using dynamic URLs, and you can also combine multiple transformations together as part of a single delivery request, e.g. crop an image and then add a border. In certain cases you may want to perform additional transformations on the result of a single transformation request. In order to do that, you can chain the transformations together. In practice, the chain allows you to start seting properties to a new transformation, which will be chained to the transformation you worked on, even though you still use the same CLDTransformation instance. - returns: The same instance of CLDTransformation. */ @discardableResult open func chain() -> Self { guard !currentTransformationParams.isEmpty else { return self } transformations.append(currentTransformationParams) currentTransformationParams = [:] return self } public func asString() -> String? { chain() var components: [String] = [] for params in self.transformations { if let singleTransStringRepresentation = self.getStringRepresentationFromParams(params) { components.append(singleTransStringRepresentation) } else { return nil } } return components.joined(separator: "/") } // MARK: - Private internal func getStringRepresentationFromParams(_ params: [String : String]) -> String? { let emptyParams = params.filter { $0.0.isEmpty || $0.1.isEmpty } if !emptyParams.isEmpty { printLog(.error, text: "An empty string key or value are not allowed.") return nil } let components: [String] = params.sorted { $0.0 < $1.0 } .filter { $0.0 != TransformationParam.RAW_TRANSFORMATION.rawValue && $0.0 != TransformationParam.VARIABLES.rawValue && $0.0 != TransformationParam.IF_PARAM.rawValue && !$0.1.isEmpty } .compactMap { return "\($0)_\($1)" } var finalComponents: [String] = [String]() if let ifConditions = params[TransformationParam.IF_PARAM.rawValue], !ifConditions.isEmpty { finalComponents.append(TransformationParam.IF_PARAM.rawValue + CLDTransformation.elementsSeparator + ifConditions) } if let complexVariables = params[TransformationParam.VARIABLES.rawValue], !complexVariables.isEmpty { finalComponents.append(complexVariables) } finalComponents.append(contentsOf: components) if let rawTrans = params[TransformationParam.RAW_TRANSFORMATION.rawValue], !rawTrans.isEmpty { finalComponents.append(rawTrans) } return finalComponents.joined(separator: CLDTransformation.transformationContentSeparator) } // MARK: - Params internal enum TransformationParam: String { case WIDTH = "w" case HEIGHT = "h" case NAMED = "t" case CROP = "c" case BACKGROUND = "b" case COLOR = "co" case EFFECT = "e" case ANGLE = "a" case OPACITY = "o" case BORDER = "bo" case X = "x" case Y = "y" case RADIUS = "r" case QUALITY = "q" case DEFAULT_IMAGE = "d" case GRAVITY = "g" case COLOR_SPACE = "cs" case PREFIX = "p" case OVERLAY = "l" case UNDERLAY = "u" case FETCH_FORMAT = "f" case DENSITY = "dn" case PAGE = "pg" case DELAY = "dl" case FLAGS = "fl" case DPR = "dpr" case ZOOM = "z" case ASPECT_RATIO = "ar" case CUSTOM_FUNCTION = "fn" case AUDIO_CODEC = "ac" case AUDIO_FREQUENCY = "af" case BIT_RATE = "br" case VIDEO_SAMPLING = "vs" case DURATION = "du" case START_OFFSET = "so" case END_OFFSET = "eo" case VIDEO_CODEC = "vc" case RAW_TRANSFORMATION = "raw_transformation" case VARIABLES = "variables" case IF_PARAM = "if" case KEYFRAME_INTERVAL = "ki" case FPS = "fps" case STREAMING_PROFILE = "sp" } // MARK: CLDBaseParam @objc public class CLDBaseParam: NSObject { fileprivate let param: String fileprivate init(_ components: [String]) { self.param = components.joined(separator: ":") } fileprivate convenience init(_ components: String...) { self.init(components) } override public var description: String { get { return param } } } // MARK: CLDQuality /** Image quality configuration object */ @objc public class CLDQuality: CLDBaseParam { /** Build an instance of CLDQuality configured for fixed quality. - parameter level: Quality level to set. Valid range is 1 through 100. */ public static func fixed(_ level: Int) -> CLDQuality { return CLDQuality(level.description) } /** Build an instance of CLDQuality configured for automatic quality. See CLDAutoQuality enum for details. - parameter level: Auto quality level. */ public static func auto(_ level: CLDQualityAuto? = nil) -> CLDQuality { if let level = level { return CLDQuality("auto", level.description) } else { return CLDQuality("auto") } } /** Build an instance of CLDQuality configured to use jpegmini addon for automatic quality. */ public static func jpegMini() -> CLDQuality { return CLDQuality("jpegmini") } } /** Automatic optimal quality settings: the smallest file size without affecting their perceptual quality. * best: Automatically calculate the optimal quality for images using a less aggressive algorithm * good: Automatically calculate the optimal quality for an image * eco: Automatically calculate the optimal quality for images using a more aggressive algorithm * low: Automatically calculate the optimal quality for images using the most aggressive algorithm */ @objc public enum CLDQualityAuto: Int, CustomStringConvertible { case best, good, eco, low public var description: String { get { switch self { case .best: return "best" case .good: return "good" case .eco: return "eco" case .low: return "low" } } } } // MARK: CLDCustomFunction /** Custom function configuration object */ @objc public class CLDCustomFunction: CLDBaseParam { /** Build an instance of CLDCustomFunction configured for web-assembly custom function. - parameter publicId: Public id of the web assembly file. */ @objc public static func wasm(_ publicId: String) -> CLDCustomFunction { return CLDCustomFunction("wasm", publicId) } /** Build an instance of CLDCustomFunction configured for remote lambda custom function. - parameter url: public url of the aws lambda function */ @objc public static func remote(_ url: String) -> CLDCustomFunction { return CLDCustomFunction("remote", url.cldBase64UrlEncode()) } } // MARK: FPS /** FPS parameters configuration object. For simple cases you can pass a float/string directly to CLDTransformation.setFps(). This class is used for more complex values (e.g. ranges). */ @objc public class CLDFps: CLDBaseParam { /** Build an instance of CLDFps based on a string. - parameter fps: The fps value as string */ public static func fromString(_ fps: String) -> CLDFps { return CLDFps(fps); } /** Build an instance of CLDFps based on a float. - parameter fps: The fps value as float */ public static func fromFloat(_ fps: Float) -> CLDFps { return CLDFps(fps.cldCleanFormat()); } /** Build an instance of CLDFps based on a range of string values. Note: At least one parameter must not be nil. - parameter start: The fps start value as string - parameter start: The fps end value as string */ public static func range(start: String? = nil, end: String? = nil) -> CLDFps { return CLDFps("\(start ?? "")-\(end ?? "")"); } /** Build an instance of CLDFps based on a range of float values. Note: At least one parameter must not be nil. - parameter start: The fps start value as float - parameter start: The fps end value as float */ public static func range(start: Float? = nil, end: Float? = nil) -> CLDFps { return CLDFps("\(start?.cldCleanFormat() ?? "")-\(end?.cldCleanFormat() ?? "")"); } } // MARK: Crop @objc public enum CLDCrop: Int, CustomStringConvertible { case fill, crop, scale, fit, limit, mFit, lFill, pad, lPad, mPad, thumb, imaggaCrop, imaggaScale public var description: String { get { switch self { case .fill: return "fill" case .crop: return "crop" case .scale: return "scale" case .fit: return "fit" case .limit: return "limit" case .mFit: return "mfit" case .lFill: return "lfill" case .pad: return "pad" case .lPad: return "lpad" case .mPad: return "mpad" case .thumb: return "thumb" case .imaggaCrop: return "imagga_crop" case .imaggaScale: return "imagga_scale" } } } } // MARK: Effect @objc public enum CLDEffect: Int, CustomStringConvertible { case hue, red, green, blue, negate, brightness, sepia, grayscale, blackwhite, saturation, colorize, contrast, autoContrast, vibrance, autoColor, improve, autoBrightness, fillLight, viesusCorrect, gamma, screen, multiply, overlay, makeTransparent, trim, shadow, distort, shear, displace, oilPaint, redeye, advRedeye, vignette, gradientFade, pixelate, pixelateRegion, pixelateFaces, blur, blurRegion, blurFaces, sharpen, unsharpMask, orderedDither, art, assistColorblind, preview public var description: String { get { switch self { case .hue: return "hue" case .red: return "red" case .green: return "green" case .blue: return "blue" case .negate: return "negate" case .brightness: return "brightness" case .sepia: return "sepia" case .grayscale: return "grayscale" case .blackwhite: return "blackwhite" case .saturation: return "saturation" case .colorize: return "colorize" case .contrast: return "contrast" case .autoContrast: return "auto_contrast" case .vibrance: return "vibrance" case .autoColor: return "auto_color" case .improve: return "improve" case .autoBrightness: return "auto_brightness" case .fillLight: return "fill_light" case .viesusCorrect: return "viesus_correct" case .gamma: return "gamma" case .screen: return "screen" case .multiply: return "multiply" case .overlay: return "overlay" case .makeTransparent: return "make_transparent" case .trim: return "trim" case .shadow: return "shadow" case .distort: return "distort" case .shear: return "shear" case .displace: return "displace" case .oilPaint: return "oil_paint" case .redeye: return "redeye" case .advRedeye: return "adv_redeye" case .vignette: return "vignette" case .gradientFade: return "gradient_fade" case .pixelate: return "pixelate" case .pixelateRegion: return "pixelate_region" case .pixelateFaces: return "pixelate_faces" case .blur: return "blur" case .blurRegion: return "blur_region" case .blurFaces: return "blur_faces" case .sharpen: return "sharpen" case .unsharpMask: return "unsharp_mask" case .orderedDither: return "ordered_dither" case .art: return "art" case .assistColorblind: return "assist_colorblind" case .preview: return "preview" } } } } // MARK: Artistic Effects @objc public enum CLDArtFilters: Int, CustomStringConvertible { case alDente, athena, audrey, aurora, daguerre, eucalyptus, fes, frost, hairspray, hokusai, incognito, linen, peacock, primavera, quartz, redRock, refresh, sizzle, sonnet, ukulele, zorro public var description: String { get { switch self { case .alDente: return "al_dente" case .athena: return "athena" case .audrey: return "audrey" case .aurora: return "aurora" case .daguerre: return "daguerre" case .eucalyptus: return "eucalyptus" case .fes: return "fes" case .frost: return "frost" case .hairspray: return "hairspray" case .hokusai: return "hokusai" case .incognito: return "incognito" case .linen: return "linen" case .peacock: return "peacock" case .primavera: return "primavera" case .quartz: return "quartz" case .redRock: return "red_rock" case .refresh: return "refresh" case .sizzle: return "sizzle" case .sonnet: return "sonnet" case .ukulele: return "ukulele" case .zorro: return "zorro" } } } } // MARK: Gravity @objc public enum CLDGravity: Int, CustomStringConvertible { case center, auto, face, faceCenter, faces, facesCenter, advFace, advFaces, advEyes, north, northWest, northEast, south, southWest, southEast, east, west, xyCenter, custom, customFace, customFaces, customAdvFace, customAdvFaces, autoOcrText, ocrText, ocrTextAdvOcr public var description: String { get { switch self { case .center: return "center" case .auto: return "auto" case .face: return "face" case .faceCenter: return "face:center" case .faces: return "faces" case .facesCenter: return "faces:center" case .advFace: return "adv_face" case .advFaces: return "adv_faces" case .advEyes: return "adv_eyes" case .north: return "north" case .northWest: return "north_west" case .northEast: return "north_east" case .south: return "south" case .southWest: return "south_west" case .southEast: return "south_east" case .west: return "west" case .east: return "east" case .xyCenter: return "xy_center" case .custom: return "custom" case .customFace: return "custom:face" case .customFaces: return "custom:faces" case .customAdvFace: return "custom:adv_face" case .customAdvFaces: return "custom:adv_faces" case .autoOcrText: return "auto:ocr_text" case .ocrText: return "ocr_text" case .ocrTextAdvOcr: return "ocr_text:adv_ocr" } } } } } // MARK: - Expressions extension CLDTransformation { @objc(setWidthWithExpression:) @discardableResult public func setWidth(_ input: CLDExpression) -> Self { return setParam(TransformationParam.WIDTH, value: input.asString()) } @objc(setHeightWithExpression:) @discardableResult public func setHeight(_ input: CLDExpression) -> Self { return setParam(TransformationParam.HEIGHT, value: input.asString()) } @objc(setXFromExpression:) @discardableResult public func setX(_ input: CLDExpression) -> Self { return setParam(TransformationParam.X, value: input.asString()) } @objc(setYFromExpression:) @discardableResult public func setY(_ input: CLDExpression) -> Self { return setParam(TransformationParam.Y, value: input.asString()) } @objc(setRadiusFromExpression:) @discardableResult public func setRadius(_ input: CLDExpression) -> Self { return setParam(TransformationParam.RADIUS, value: input.asString()) } @objc(setStartOffsetFromExpression:) @discardableResult public func setStartOffset(_ input: CLDExpression) -> Self { return setParam(TransformationParam.START_OFFSET, value: input.asString()) } @objc(setEndOffsetFromExpression:) @discardableResult public func setEndOffset(_ input: CLDExpression) -> Self { return setParam(TransformationParam.END_OFFSET, value: input.asString()) } } // MARK: - Condition Expression extension CLDTransformation { // MARK: - ifCondition with content @objc(ifConditionFromString:) @discardableResult public func ifCondition(_ condition: String) -> Self { let conditionObject = CLDConditionExpression(value: condition) guard !conditionObject.currentValue.isEmpty else { return setParam(TransformationParam.IF_PARAM, value: condition) } return ifCondition(conditionObject) } @discardableResult public func ifCondition(_ condition: CLDConditionExpression) -> Self { return setParam(TransformationParam.IF_PARAM, value: condition.asString()) } @discardableResult public func ifCondition(_ condition: CLDConditionExpression, then transformation: CLDExpression) -> Self { return ifCondition(condition).setParam(transformation.currentKey, value: transformation.currentValue) } // MARK: - ifCondition state @objc(ifCondition) @discardableResult public func ifCondition() -> CLDConditionExpression { let condition = CLDConditionExpression() condition.relatedTransformation = self currentCondition = condition return condition } // MARK: - ifElse @objc(ifElse) @discardableResult public func ifElse() -> Self { chain() return setParam(TransformationParam.IF_PARAM, value: "else") } // MARK: - endIf @objc(endIf) @discardableResult public func endIf() -> Self { chain() guard !transformations.isEmpty else { return self } let transformSize = transformations.count let loopSize = transformSize - 1 for index in (0...loopSize).reversed() { var segment = transformations[index]; // [..., {if: "w_gt_1000",c: "fill", w: 500}, ...] if let value = segment[TransformationParam.IF_PARAM.rawValue] { // if: "w_gt_1000" if value == "end" { break } if segment.keys.count > 1 { segment.removeValue(forKey: TransformationParam.IF_PARAM.rawValue) // {c: fill, w: 500} transformations[index] = segment // [..., {c: fill, w: 500}, ...] transformations.insert([TransformationParam.IF_PARAM.rawValue:value], at: index) // [..., "if_w_gt_1000", {c: fill, w: 500}, ...] } // otherwise keep looking for if_condition if value != "else" { break } } } setParam(TransformationParam.IF_PARAM.rawValue, value: "end") return chain() } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/CLDVariable.swift ================================================ // // CLDVariable.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal func CLDThrowFatalError(with message: String) { // fatalError(message) } @objcMembers open class CLDVariable: NSObject { public var value: String public var name : String { didSet { self.addNamePrefixIfNeeded() } } internal var isValid : Bool { return checkValidName(name) && !value.isEmpty } static public let variableNamePrefix : String = "$" static private let collectionPrefix : String = "!" static private let collectionSuffix : String = "!" static private let collectionSeparator: String = ":" static internal let elementsSeparator: String = "_" static private let separator: String = "," static private let nameRegex: String = "^\\$[a-zA-Z][a-zA-Z0-9]*$" // MARK: - Init public override init() { self.name = String() self.value = String() super.init() self.addNamePrefixIfNeeded() } @objc(initWithName:stringValue:) public init(name variableName: String, value variableValue: String) { self.name = variableName if variableValue.hasPrefix(CLDVariable.variableNamePrefix) { self.value = CLDExpression(value: variableValue).asString() } else { self.value = variableValue } super.init() self.addNamePrefixIfNeeded() } @objc(initWithName:doubleValue:) public init(name variableName: String, value variableValue: Double) { self.name = variableName self.value = String(variableValue) super.init() self.addNamePrefixIfNeeded() } @objc(initWithName:intValue:) public init(name variableName: String, value variableValue: Int) { self.name = variableName self.value = String(variableValue) super.init() self.addNamePrefixIfNeeded() } public init(name variableName: String, values: [String]) { self.name = variableName if values.isEmpty { self.value = String() } else { self.value = CLDVariable.collectionPrefix + values.joined(separator: CLDVariable.collectionSeparator) + CLDVariable.collectionSuffix } super.init() self.addNamePrefixIfNeeded() } // MARK: - Public methods public func asString() -> String { guard checkValidName(name) else { return String() } return name + CLDVariable.elementsSeparator + value } public func asParams() -> [String : String] { guard checkValidName(name) else { return [String:String]() } return [name:value] } // MARK: - Private methods private func checkValidName(_ name: String) -> Bool { let regex = try! NSRegularExpression(pattern: CLDVariable.nameRegex, options: .caseInsensitive) let range = NSRange(location: 0, length: name.count) let isValid = regex.firstMatch(in: name, options: [], range: range) != nil if !isValid { CLDThrowFatalError(with: "\(#function) failed!") } return isValid } private func addNamePrefixIfNeeded() { guard !checkValidName(name) else { return } guard !name.hasPrefix(CLDVariable.variableNamePrefix) else { return } name = addPrefix(to: name) } private func addPrefix(to name: String) -> String { return CLDVariable.variableNamePrefix + name } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Layers/CLDFetchLayer.swift ================================================ // // CLDFetchLayer.swift // Cloudinary // // Created by Nitzan Jaitman on 19/02/2019. // Copyright © 2019 Cloudinary. All rights reserved. // import Foundation @objcMembers open class CLDFetchLayer: CLDLayer { // MARK: - Init /** Initialize a CLDFetchLayer instance. - parameter url: The url of the remote resource to fetch - returns: The new CLDFetchLayer instance. */ public init(url: String) { super.init() setPublicId(publicId: url.cldBase64UrlEncode()) resourceType = String(describing: LayerResourceType.fetch) } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Layers/CLDLayer.swift ================================================ // // CLDLayer.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The CLDLayer is used to help adding an overlay or underlay layer to a transformation. */ @objcMembers open class CLDLayer: NSObject { internal var publicId: String? internal var format: String? internal var resourceType: String? internal var type: String? // MARK: - Init /** Initialize a CLDLayer instance. -returns: The new CLDLayer instance. */ @discardableResult public override init() { super.init() } // MARK: - Set Values /** The identifier of the image to use as a layer. - parameter publicId: The identifier of the image to use as a layer. - returns: The same instance of CLDLayer. */ @discardableResult open func setPublicId(publicId: String) -> CLDLayer { self.publicId = publicId return self } /** The format of the image to use as a layer. - parameter format: The format of the image to use as a layer. - returns: The same instance of CLDLayer. */ @discardableResult open func setFormat(format: String) -> CLDLayer { self.format = format return self } /** Set the layer resource type. - parameter resourceType: The layer resource type. - returns: The same instance of CLDLayer. */ @objc(setResourceTypeFromLayerResourceType:) @discardableResult open func setResourceType(_ resourceType: LayerResourceType) -> CLDLayer { return setResourceType(String(describing: resourceType)) } /** Set the layer resource type. - parameter resourceType: The layer resource type. - returns: The same instance of CLDLayer. */ @objc(setResourceTypeFromString:) @discardableResult open func setResourceType(_ resourceType: String) -> CLDLayer { self.resourceType = resourceType return self } /** Set the layer type. - parameter type: The layer type. - returns: The same instance of CLDLayer. */ @objc(setTypeFromType:) @discardableResult open func setType(_ type: CLDType) -> CLDLayer { return setType(String(describing: type)) } /** Set the layer type. - parameter rawType: The layer type. - returns: The same instance of CLDLayer. */ @objc(setTypeFromString:) @discardableResult open func setType(_ rawType: String) -> CLDLayer { type = rawType return self } // MARK: - Helpers fileprivate func isResourceTypeTextual(_ resourceType: String?) -> Bool { guard let resourceType = resourceType else { return false } return resourceType == String(describing: LayerResourceType.text) || resourceType == String(describing: LayerResourceType.subtitles) } internal func getFinalPublicId() -> String? { var finalPublicId: String? if let pubId = publicId , !pubId.isEmpty, let format = format , !format.isEmpty { finalPublicId = "\(pubId).\(format)" } return finalPublicId ?? publicId } internal func getStringComponents() -> [String]? { var components: [String] = [] if publicId == nil, let resourceType = resourceType , resourceType != String(describing: LayerResourceType.text) { printLog(.error, text: "Must supply publicId for non-text layer") return nil } if let resourceType = resourceType , resourceType != String(describing: LayerResourceType.image) { components.append(resourceType) } if let type = type , type != String(describing: CLDType.upload) { components.append(type) } if !isResourceTypeTextual(resourceType) { if let pubId = getFinalPublicId() , !pubId.isEmpty { components.append(pubId.replacingOccurrences(of: "/", with: ":")) } } return components } // MARK: - Actions internal func asString() -> String? { guard let components = self.getStringComponents() else { return nil } return components.joined(separator: ":") } // MARK: - Params @objc public enum LayerResourceType: Int, CustomStringConvertible { case image, raw, auto, text, subtitles, video, fetch public var description: String { get { switch self { case .image: return "image" case .raw: return "raw" case .auto: return "auto" case .text: return "text" case .subtitles: return "subtitles" case .video: return "video" case .fetch: return "fetch" } } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Layers/CLDSubtitlesLayer.swift ================================================ // // CLDSubtitlesLayer.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDSubtitlesLayer: CLDTextLayer { // MARK: - Init /** Initialize a CLDLayer instance. -returns: The new CLDLayer instance. */ public override init() { super.init() resourceType = String(describing: LayerResourceType.subtitles) } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Layers/CLDTextLayer.swift ================================================ // // CLDTextLayer.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDTextLayer: CLDLayer { internal var text: String? internal var fontFamily: String? internal var fontSize: String? internal var fontStyle: String? internal var fontWeight: String? internal var textDecoration: String? internal var textAlign: String? internal var stroke: String? internal var letterSpacing: String? internal var lineSpacing: String? internal var fontAntialiasing: String? internal var fontHinting: String? // MARK: - Init /** Initialize a CLDTextLayer instance. -returns: The new CLDTextLayer instance. */ public override init() { super.init() resourceType = String(describing: LayerResourceType.text) } // MARK: - Set Values /** Add a text caption layer. - parameter text: The text to use as a caption layer. - returns: The same instance of CLDTextLayer. */ open func setText(text: String) -> CLDTextLayer { self.text = text return self } /** Set the name of a font family. e.g. `arial`. - parameter fontFamily: The layer font family. - returns: The same instance of CLDTextLayer. */ open func setFontFamily(fontFamily: String) -> CLDTextLayer { self.fontFamily = fontFamily return self } /** Set the font size in pixels. e.g. 12. - parameter fontSize: The layer font size. - returns: The same instance of CLDTextLayer. */ @objc(setFontSizeFromInt:) open func setFontSize(_ fontSize: Int) -> CLDTextLayer { return setFontSize(String(fontSize)) } /** Set the font size in pixels. e.g. 12. - parameter fontSize: The layer font size. - returns: The same instance of CLDTextLayer. */ @objc(setFontSizeFromString:) open func setFontSize(_ fontSize: String) -> CLDTextLayer { self.fontSize = fontSize return self } /** Set the font style. - parameter fontStyle: The layer font style. - returns: The same instance of CLDTextLayer. */ @objc(setFontStyleFromLayerFontStyle:) open func setFontStyle(_ fontStyle: CLDFontStyle) -> CLDTextLayer { return setFontStyle(String(describing: fontStyle)) } /** Set the font style. Possible values: normal (default value) or italic. e.g., italic - parameter fontStyle: The layer font style. - returns: The same instance of CLDTextLayer. */ @objc(setFontStyleFromString:) open func setFontStyle(_ fontStyle: String) -> CLDTextLayer { self.fontStyle = fontStyle return self } /** Set the text weight. - parameter fontWeight: The layer font weight. - returns: The same instance of CLDTextLayer. */ @objc(setFontWeightFromLayerFontWeight:) open func setFontWeight(_ fontWeight: CLDFontWeight) -> CLDTextLayer { return setFontWeight(String(describing: fontWeight)) } /** Set the text weight. Possible values: normal (default value) or bold. - parameter fontWeight: The layer font weight. - returns: The same instance of CLDTextLayer. */ @objc(setFontWeightFromString:) open func setFontWeight(_ fontWeight: String) -> CLDTextLayer { self.fontWeight = fontWeight return self } /** Set the text decoration. Possible values: none (default value), underline or strikethrough. - parameter textDecoration: The layer text Decoration. - returns: The same instance of CLDTextLayer. */ @objc(setTextDecorationString:) open func setTextDecoration(_ textDecoration: String) -> CLDTextLayer { self.textDecoration = textDecoration return self } /** Set the text alignment. Possible values: left (default value), center, right, end, start or justify. - parameter textAlign: The layer text alignment. - returns: The same instance of CLDTextLayer. */ @objc(setTextAlignString:) open func setTextAlign(_ textAlign: String) -> CLDTextLayer { self.textAlign = textAlign return self } /** Set the font stroke (border). Possible values: none (default value) or stroke. Set the color and weight of the stroke with the border parameter. - parameter stroke: The layer text stroke. - returns: The same instance of CLDTextLayer. */ @objc(setStrokeString:) open func setStroke(_ stroke: String) -> CLDTextLayer { self.stroke = stroke return self } /** Set the spacing between the letters in pixels. Can be a positive or negative. - parameter letterSpacing: The layer letter Spacing. - returns: The same instance of CLDTextLayer. */ @objc(setLetterSpacingFromInt:) open func setLetterSpacing(_ letterSpacing: Int) -> CLDTextLayer { return setLetterSpacing(String(letterSpacing)) } /** Set the spacing between the letters in pixels. Can be a positive or negative. - parameter letterSpacing: The layer letter Spacing. - returns: The same instance of CLDTextLayer. */ @objc(setLetterSpacingFromFloat:) open func setLetterSpacing(_ letterSpacing: Float) -> CLDTextLayer { return setLetterSpacing(letterSpacing.cldFloatFormat()) } /** Set the spacing between the letters in pixels. Can be a positive or negative. - parameter letterSpacing: The layer letter Spacing. - returns: The same instance of CLDTextLayer. */ @objc(setLetterSpacingString:) open func setLetterSpacing(_ letterSpacing: String) -> CLDTextLayer { self.letterSpacing = letterSpacing return self } /** Set the spacing between the lines in pixels (only relevant for multi-line text). Can be a positive or negative. - parameter lineSpacing: The layer line Spacing. - returns: The same instance of CLDTextLayer. */ @objc(setLineSpacingFromInt:) open func setLineSpacing(_ lineSpacing: Int) -> CLDTextLayer { return setLineSpacing(String(describing: letterSpacing)) } /** Set the spacing between the lines in pixels (only relevant for multi-line text). Can be a positive or negative. - parameter lineSpacing: The layer line Spacing. - returns: The same instance of CLDTextLayer. */ @objc(setLineSpacingFromFloat:) open func setLineSpacing(_ lineSpacing: Float) -> CLDTextLayer { return setLineSpacing(lineSpacing.cldFloatFormat()) } /** Set the spacing between the lines in pixels (only relevant for multi-line text). Can be a positive or negative. - parameter lineSpacing: The layer line Spacing. - returns: The same instance of CLDTextLayer. */ @objc(setLineSpacingString:) open func setLineSpacing(_ lineSpacing: String) -> CLDTextLayer { self.lineSpacing = lineSpacing return self } /** Set the antialiasing of the text layer rendering - parameter antialiasing: The antialiasing method - returns: The same instance of CLDTextLayer. */ @objc(setFontAntialiasingString:) open func setFontAntialiasing(_ antialiasing: String) -> CLDTextLayer { self.fontAntialiasing = antialiasing return self } /** Set the antialiasing of the text layer rendering - parameter antialiasing: The antialiasing method - returns: The same instance of CLDTextLayer. */ open func setFontAntialiasing(_ antialiasing: CLDFontAntialiasing) -> CLDTextLayer { self.fontAntialiasing = antialiasing.rawValue return self } /** Set the text rendering hinting - parameter hinting: The hinting method - returns: The same instance of CLDTextLayer. */ @objc (setFontHintingString:) open func setFontHinting(_ hinting: String) -> CLDTextLayer{ self.fontHinting = hinting return self } /** Set the text rendering hinting - parameter hinting: The hinting method - returns: The same instance of CLDTextLayer. */ open func setFontHinting(_ hinting: CLDFontHinting) -> CLDTextLayer{ self.fontHinting = hinting.rawValue return self } // MARK: - Actions internal override func getStringComponents() -> [String]? { if (text == nil || text!.isEmpty) && (publicId == nil || publicId!.isEmpty) { printLog(.error, text: "Must supply either text or publicId") return nil } guard var components = super.getStringComponents() else { return nil } let optionalTextParams = getOptionalTextPropertiesArray() let mandatoryTextParams = getMandatoryTextPropertiesArray() if optionalTextParams.isEmpty { if !mandatoryTextParams.isEmpty { components.append(mandatoryTextParams.joined(separator: "_")) } } else if !mandatoryTextParams.isEmpty { let textProperties = mandatoryTextParams + optionalTextParams components.append(textProperties.joined(separator: "_")) } else { printLog(.error, text: "Must supply fontSize and fontFamily for text layer") return nil } if let publicId = publicId , !publicId.isEmpty { components.append(publicId.replacingOccurrences(of: "/", with: ":")) } if let text = text , !text.isEmpty { if let text = text.cldSmartEncodeUrl() { var textToAdd = text.replacingOccurrences(of: "%2C", with: "%252C") textToAdd = textToAdd.replacingOccurrences(of: "/", with: "%252F") components.append(textToAdd) } } return components } //MARK: - Private fileprivate func getOptionalTextPropertiesArray() -> [String] { var properties: [String] = [] if let fontWeight = fontWeight , fontWeight != "normal" { properties.append(fontWeight) } if let fontStyle = fontStyle , fontStyle != "normal" { properties.append(fontStyle) } if let antialiasing = fontAntialiasing, !antialiasing.isEmpty { properties.append("antialias_\(antialiasing)") } if let hinting = fontHinting, !hinting.isEmpty { properties.append("hinting_\(hinting)") } if let textDecoration = textDecoration , textDecoration != "none" { properties.append(textDecoration) } if let stroke = stroke , stroke != "none" { properties.append(stroke) } if let textAlign = textAlign , !textAlign.isEmpty { properties.append(textAlign) } if let letterSpacing = letterSpacing , !letterSpacing.isEmpty { properties.append("letter_spacing_\(letterSpacing)") } if let lineSpacing = lineSpacing , !lineSpacing.isEmpty { properties.append("line_spacing_\(lineSpacing)") } return properties } fileprivate func getMandatoryTextPropertiesArray() -> [String] { var properties: [String] = [] if let fontSize = fontSize { properties.insert(fontSize, at: 0) } if let fontFamily = fontFamily { properties.insert(fontFamily, at: 0) } return properties } /// Text layer font antialiasing methods public enum CLDFontAntialiasing: String { /// Use a bi-level alpha mask. case NONE = "none" /// Perform single-color antialiasing. For example, using shades of gray for black text on a white background. case GRAY = "gray" /// Perform antialiasing by taking advantage of the order of subpixel elements on devices such as LCD panels. case SUBPIXEL = "subpixel" /// Some antialiasing is performed, but speed is prioritized over quality. case FAST = "fast" /// Antialiasing that balances quality and performance. case GOOD = "good" /// Renders at the highest quality, sacrificing speed if necessary. case BEST = "best" } /// Text layer font hinting methods public enum CLDFontHinting: String { /// Do not hint outlines. case NONE = "none" /// Hint outlines slightly to improve contrast while retaining good fidelity to the original shapes. case SLIGHT = "slight" /// Hint outlines with medium strength, providing a compromise between fidelity to the original shapes and contrast. case MEDIUM = "medium" /// Hint outlines to the maximize contrast. case FULL = "full" } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/RequestParams/CLDRequestParams.swift ================================================ // // CLDRequestParams.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The CLDRequestParams class is a base class for all different request params object. It holds a dictionary of the actual params, the request's resource type, the request signature for signing a request using an externaly generated signature, and the subclass' representing action. */ @objcMembers open class CLDRequestParams: NSObject { /** A dictionary of the params to be sent as part of the request. */ internal var params: [String : Any] /** The request's resource type, if set it will be part of the request URL. On most cases defaults to "image". */ internal var resourceType: String? /** The request signature for signing a request using an externally generated signature. if no signature is assigned, the SDK will sign the request using the configured API secret. */ internal var signature: CLDSignature? /** Override config apiKey with this value. */ internal var apiKey: String? internal override init() { apiKey = nil signature = nil resourceType = nil params = [:] super.init() } //MARK: - Set Param /** A generic setter to manualy set a param. - parameter key: The key of parameter to set. - parameter value: The parameter value. - returns: The same instance of CLDRequestParams. */ @discardableResult open func setParam(_ key: String, value: Any?) -> Self { params[key] = value return self } @discardableResult @objc(setResourceTypeFromUrlResourceType:) open func setResourceType(_ resourceType: CLDUrlResourceType) -> Self { return setResourceType(String(describing: resourceType)) } @discardableResult @objc(setResourceTypeFromString:) open func setResourceType(_ resourceType: String) -> Self { self.resourceType = resourceType return self } @discardableResult @objc(setSignatureWithSignature:) open func setSignature(_ signature: CLDSignature) -> Self { self.signature = signature return self } @discardableResult @objc(setApiKeyWithKey:) open func setApiKey(_ apiKey: String) -> Self { self.apiKey = apiKey return self } // MARK: - Get Param /** A generic getter to retrieve a param for a given key. - parameter key: The key of the parameter to retrieve. - returns: The same instance of CLDRequestParams. */ open func getParam(_ key: String) -> Any? { return params[key] } internal func merge(_ other: CLDRequestParams?) { if let other = other { self.signature = other.signature self.resourceType = other.resourceType self.params.cldMerge(other.params) self.apiKey = other.apiKey } } internal func setTimeout(from config: CLDConfiguration) { guard let timeout = config.timeout else { return } setParam(CLDConfiguration.ConfigParam.Timeout.description, value: timeout) } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/RequestParams/Helpers/CLDRequestParamsHelpers.swift ================================================ // // CLDRequestParamsHelpers.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import CoreGraphics // MARK: - CLDSignature /** The CLDSignature class represents a signature used to sign a URL request. */ @objcMembers open class CLDSignature: NSObject { public let signature: String public let timestamp: NSNumber public init(signature: String, timestamp: NSNumber) { self.signature = signature self.timestamp = timestamp super.init() } internal enum SignatureParam: String, CustomStringConvertible { case Signature = "signature" case Timestamp = "timestamp" var description: String { get { switch self { case .Signature: return "signature" case .Timestamp: return "timestamp" } } } } } // MARK: - CLDAccessControlRule /** The CLDAccessControlRule class represents a single access control rule for upload/update API */ @objcMembers open class CLDAccessControlRule: NSObject, Codable { // MARK: Properties internal var start: Date? internal var end: Date? internal var accessType: CLDAccessType // MARK: Init /** Initializes the CLDAccessControlRule - parameter accessType: The access type for this rule - parameter start: The start time for the rule - parameter end: The end time for the rule - returns: A new CLDaccessControlRule instance. */ internal init(accessType: CLDAccessType, start: Date? = nil, end: Date? = nil) { self.accessType = accessType self.start = start self.end = end } // MARK: Static builders /** Get a new instance of CLDAccessControlRule with token strategy */ public static func token()-> CLDAccessControlRule { return CLDAccessControlRule(accessType: CLDAccessType.token) } /** Get a new instance of CLDAccessControlRule with anonymous strategy - parameter start: The start date for the rule */ public static func anonymous(start: Date)-> CLDAccessControlRule { return CLDAccessControlRule(accessType: CLDAccessType.anonymous, start: start) } /** Get a new instance of CLDAccessControlRule with anonymous strategy - parameter end: The end date for the rule */ public static func anonymous(end: Date)-> CLDAccessControlRule { return CLDAccessControlRule(accessType: CLDAccessType.anonymous, end: end) } /** Get a new instance of CLDAccessControlRule with anonymous strategy - parameter start: The start date for the rule - parameter end: The end date for the rule */ public static func anonymous(start: Date, end: Date)-> CLDAccessControlRule { return CLDAccessControlRule(accessType: CLDAccessType.anonymous, start: start, end: end) } // MARK: Encodable enum CodingKeys: String, CodingKey { case start case end case accessType = "access_type" } // MARK: Equatable static func == (lhs: CLDAccessControlRule, rhs: CLDAccessControlRule) -> Bool { return lhs.accessType == rhs.accessType && lhs.start == rhs.start && lhs.end == rhs.end } } public enum CLDAccessType : String, Codable { case anonymous, token } // MARK: - CLDCoordinate /** The CLDCoordinate class represents a rectangle area on an asset. */ @objcMembers open class CLDCoordinate: NSObject { let rect: CGRect var x: Float { return Float(rect.origin.x) } var y: Float { return Float(rect.origin.y) } var width: Float { return Float(rect.width) } var height: Float { return Float(rect.height) } // MARK: Init /** Initializes the CLDCoordinate using a CGRect. - parameter rect: The rectangle representing an area on the asset. - returns: A new CLDCoordinate instance. */ public init(rect: CGRect) { self.rect = rect } open override var description: String { get { var components: [String] = [] components.append(x.cldFormat(f: ".0")) components.append(y.cldFormat(f: ".0")) components.append(width.cldFormat(f: ".0")) components.append(height.cldFormat(f: ".0")) return components.joined(separator: ",") } } } // MARK: - CLDResponsiveBreakpoints /** The CLDResponsiveBreakpoints class describe the settings available for configuring responsive breakpoints. Responsive breakpoints is used to request Cloudinary to automatically find the best breakpoints. */ @objcMembers open class CLDResponsiveBreakpoints: NSObject { internal var params: [String: AnyObject] = [:] // MARK - Init /** Initializes a CLDResponsiveBreakpoints instance. - parameter createDerived: If true, create and keep the derived assets of the selected breakpoints during the API call. If false, assets generated during the analysis process are thrown away. - returns: A new CLDResponsiveBreakpoints instance. */ public init(createDerived: Bool) { super.init() setParam(ResponsiveBreakpointsParams.CreateDerived.rawValue, value: createDerived as AnyObject?) } // MARK - Set Param /** Set the base transformation to first apply to the image before finding the best breakpoints. - parameter transformation: The transformation to apply. - returns: The same CLDResponsiveBreakpoints instance. */ open func setTransformations(_ transformation: CLDTransformation) -> Self { return setParam(ResponsiveBreakpointsParams.Transformation.rawValue, value: transformation.asString() as AnyObject) } /** Set the maximum width needed for this asset. If specifying a width bigger than the original asset, the width of the original asset is used instead. default is 1000. - parameter maxWidth: The max width to set. - returns: The same CLDResponsiveBreakpoints instance. */ open func setMaxWidth(_ maxWidth: Int) -> Self { return setParam(ResponsiveBreakpointsParams.MaxWidth.rawValue, value: maxWidth as AnyObject?) } /** Set the minimum width needed for this asset. default is 50. - parameter minWidth: The min width to set. - returns: The same CLDResponsiveBreakpoints instance. */ open func setMinWidth(_ minWidth: Int) -> Self { return setParam(ResponsiveBreakpointsParams.MinWidth.rawValue, value: minWidth as AnyObject?) } /** Set the minimum number of bytes between two consecutive breakpoints (assets). default is 20000. - parameter bytesStep: The bytes step to set. - returns: The same CLDResponsiveBreakpoints instance. */ open func setBytesStep(_ bytesStep: Int) -> Self { return setParam(ResponsiveBreakpointsParams.BytesStep.rawValue, value: bytesStep as AnyObject?) } /** Set the maximum number of breakpoints to find, between 3 and 200. This means that there might be size differences bigger than the given bytes_step value between consecutive assets. default is 20. - parameter maxImages: The max images to set. - returns: The same CLDResponsiveBreakpoints instance. */ open func setMaxImages(_ maxImages: Int) -> Self { return setParam(ResponsiveBreakpointsParams.MaxImages.rawValue, value: maxImages as AnyObject?) } /** Set the format of the resulting images. - parameter format: The format to set. - returns: The same CLDResponsiveBreakpoints instance. */ open func setFormat(_ format: String) -> Self { return setParam(ResponsiveBreakpointsParams.Format.rawValue, value: format as AnyObject) } @discardableResult open func setParam(_ key: String, value: AnyObject?) -> Self { params[key] = value return self } open override var description: String { get { var components: [String] = [] for param in params { if param.value is Int || param.value is Bool { components.append("\"\(param.key)\":\(param.value)") } else { components.append("\"\(param.key)\":\"\(param.value)\"") } } return "{\(components.joined(separator: ","))}" } } fileprivate enum ResponsiveBreakpointsParams: String, CustomStringConvertible { case CreateDerived = "create_derived" case Transformation = "transformation" case MaxWidth = "max_width" case MinWidth = "min_width" case BytesStep = "bytes_step" case MaxImages = "max_images" case Format = "format" var description: String { get { switch self { case .CreateDerived: return "create_derived" case .Transformation: return "transformation" case .MaxWidth: return "max_width" case .MinWidth: return "min_width" case .BytesStep: return "bytes_step" case .MaxImages: return "max_images" case .Format: return "format" } } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Requests/CLDRequest.swift ================================================ // // CLDRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The `CLDRequest` object is returned when creating a network request using one of Cloudinary's API calls. It allows the options to add a response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @objcMembers open class CLDRequest: NSObject { internal var networkRequest: CLDNetworkDataRequest internal init(networkRequest: CLDNetworkDataRequest) { self.networkRequest = networkRequest } // MARK: - Public /** Resume the request. */ open func resume() { networkRequest.resume() } /** Suspend the request. */ open func suspend() { networkRequest.suspend() } /** Cancel the request. */ open func cancel() { networkRequest.cancel() } //MARK: Handlers /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDRequest. */ @discardableResult open func responseRaw(_ completionHandler: @escaping (_ response: Any?, _ error: NSError?) -> ()) -> CLDRequest { networkRequest.response(completionHandler) return self } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/CLDBaseResult.swift ================================================ // // CLDBaseResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDBaseResult: NSObject { open fileprivate(set) var resultJson: [String : AnyObject] // MARK: - Init internal init(json: [String : AnyObject]) { resultJson = json } // MARK: - Private Helpers internal func getParam(_ param: CommonResultKeys) -> AnyObject? { return resultJson[String(describing: param)] } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/AccessibilityAnalysis/CLDAccessibilityAnalysisResult.swift ================================================ // // CLDAccessibilityAnalysisResult.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDAccessibilityAnalysisResult: CLDBaseResult { open var colorblindAccessibilityScore: Double? { guard let accessibilityScore = getParam(.colorblindAccessibilityScore) as? Double else { return nil } return accessibilityScore } open var colorblindAccessibilityAnalysis: CLDColorblindAccessibilityAnalysisResult? { guard let accessibilityAnalysis = getParam(.colorblindAccessibilityAnalysis) as? [String: AnyObject] else { return nil } return CLDColorblindAccessibilityAnalysisResult(json: accessibilityAnalysis) } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDAccessibilityAnalysisResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDAccessibilityAnalysisResultKey: CustomStringConvertible { case colorblindAccessibilityScore, colorblindAccessibilityAnalysis var description: String { switch self { case .colorblindAccessibilityScore : return "colorblind_accessibility_score" case .colorblindAccessibilityAnalysis: return "colorblind_accessibility_analysis" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/AccessibilityAnalysis/CLDColorblindAccessibilityAnalysisResult.swift ================================================ // // CLDColorblindAccessibilityAnalysisResult.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDColorblindAccessibilityAnalysisResult: CLDBaseResult { open var distinctColors: Double? { guard let distinctColors = getParam(.distinctColors) as? Double else { return nil } return distinctColors } open var distinctEdges: Double? { guard let distinctEdges = getParam(.distinctEdges) as? Double else { return nil } return distinctEdges } open var mostIndistinctPair: [String]? { guard let mostIndistinctPair = getParam(.mostIndistinctPair) as? [String] else { return nil } return mostIndistinctPair } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDColorblindAccessibilityAnalysisResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDColorblindAccessibilityAnalysisResultKey: CustomStringConvertible { case distinctColors, distinctEdges, mostIndistinctPair var description: String { switch self { case .distinctColors : return "distinct_colors" case .distinctEdges : return "distinct_edges" case .mostIndistinctPair: return "most_indistinct_pair" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDBoundingBox.swift ================================================ // // CLDBoundingBox.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import CoreGraphics @objcMembers open class CLDBoundingBox: CLDBaseResult { open var topLeft: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDBoundingBoxJsonKey.topLeft)) } open var size: CGSize? { return CLDBoundingBox.parseSize(resultJson, key: String(describing: CLDBoundingBoxJsonKey.size)) } internal static func parsePoint(_ json: [String : AnyObject], key: String) -> CGPoint? { guard let point = json[key] as? [String : Double], let x = point[CommonResultKeys.x.description], let y = point[CommonResultKeys.y.description] else { return nil } return CGPoint(x: CGFloat(x), y: CGFloat(y)) } internal static func parseSize(_ json: [String : AnyObject], key: String) -> CGSize? { guard let point = json[key] as? [String : Double], let width = point[CommonResultKeys.width.description], let height = point[CommonResultKeys.height.description] else { return nil } return CGSize(width: CGFloat(width), height: CGFloat(height)) } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDBoundingBoxJsonKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDBoundingBoxJsonKey: CustomStringConvertible { case topLeft, size var description: String { switch self { case .topLeft: return "tl" case .size: return "size" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDDetection.swift ================================================ // // CLDDetection.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDDetection: CLDBaseResult { open var rekognitionFace: CLDRekognitionFace? { guard let rekognitionFace = getParam(.rekognitionFace) as? [String : AnyObject] else { return nil } return CLDRekognitionFace(json: rekognitionFace) } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDDetectionKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDDetectionKey: CustomStringConvertible { case rekognitionFace var description: String { switch self { case .rekognitionFace: return "rekognition_face" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDFace.swift ================================================ // // CLDFace.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import CoreGraphics @objcMembers open class CLDFace: CLDBaseResult { open var boundingBox: CLDBoundingBox? { guard let bb = getParam(.boundingBox) as? [String : AnyObject] else { return nil } return CLDBoundingBox(json: bb) } open var confidence: Double? { return getParam(.confidence) as? Double } open var age: Double? { return getParam(.age) as? Double } open var smile: Double? { return getParam(.smile) as? Double } open var glasses: Double? { return getParam(.glasses) as? Double } open var sunglasses: Double? { return getParam(.sunglasses) as? Double } open var beard: Double? { return getParam(.beard) as? Double } open var mustache: Double? { return getParam(.mustache) as? Double } open var eyeClosed: Double? { return getParam(.eyeClosed) as? Double } open var mouthOpenWide: Double? { return getParam(.mouthOpenWide) as? Double } open var beauty: Double? { return getParam(.beauty) as? Double } open var gender: Double? { return getParam(.gender) as? Double } open var race: [String : Double]? { return getParam(.race) as? [String : Double] } open var emotion: [String : Double]? { return getParam(.emotion) as? [String : Double] } open var quality: [String : Double]? { return getParam(.quality) as? [String : Double] } open var pose: [String : Double]? { return getParam(.pose) as? [String : Double] } open var eyeLeftPosition: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.eyeLeftPosition)) } open var eyeRightPosition: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.eyeRightPosition)) } open var eyeLeft_Left: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.eyeLeft_Left)) } open var eyeLeft_Right: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.eyeLeft_Right)) } open var eyeLeft_Up: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.eyeLeft_Up)) } open var eyeLeft_Down: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.eyeLeft_Down)) } open var eyeRight_Left: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.eyeRight_Left)) } open var eyeRight_Right: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.eyeRight_Right)) } open var eyeRight_Up: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.eyeRight_Up)) } open var eyeRight_Down: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.eyeRight_Down)) } open var nosePosition: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.nosePosition)) } open var noseLeft: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.noseLeft)) } open var noseRight: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.noseRight)) } open var mouthLeft: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.mouthLeft)) } open var mouthRight: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.mouthRight)) } open var mouthUp: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.mouthUp)) } open var mouthDown: CGPoint? { return CLDBoundingBox.parsePoint(resultJson, key: String(describing: CLDFaceKey.mouthDown)) } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDFaceKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDFaceKey: CustomStringConvertible { case boundingBox, confidence, age, smile, glasses, sunglasses, beard, mustache, eyeClosed, mouthOpenWide, beauty, gender, race, emotion, quality, pose, eyeLeftPosition, eyeRightPosition, eyeLeft_Left, eyeLeft_Right, eyeLeft_Up, eyeLeft_Down, eyeRight_Left, eyeRight_Right, eyeRight_Up, eyeRight_Down, nosePosition, noseLeft, noseRight, mouthLeft, mouthRight, mouthUp, mouthDown var description: String { switch self { case .boundingBox: return "boundingbox" case .confidence: return "confidence" case .age: return "age" case .smile: return "smile" case .glasses: return "glasses" case .sunglasses: return "sunglasses" case .beard: return "beard" case .mustache: return "mustache" case .eyeClosed: return "eye_closed" case .mouthOpenWide: return "mouth_open_wide" case .beauty: return "beauty" case .gender: return "sex" case .race: return "race" case .emotion: return "emotion" case .quality: return "quality" case .pose: return "pose" case .eyeLeftPosition: return "eye_left" case .eyeRightPosition: return "eye_right" case .eyeLeft_Left: return "e_ll" case .eyeLeft_Right: return "e_lr" case .eyeLeft_Up: return "e_lu" case .eyeLeft_Down: return "e_ld" case .eyeRight_Left: return "e_rl" case .eyeRight_Right: return "e_rr" case .eyeRight_Up: return "e_ru" case .eyeRight_Down: return "e_rd" case .nosePosition: return "nose" case .noseLeft: return "n_l" case .noseRight: return "n_r" case .mouthLeft: return "mouth_l" case .mouthRight: return "mouth_r" case .mouthUp: return "m_u" case .mouthDown: return "m_d" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDInfo.swift ================================================ // // CLDInfo.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDInfo: CLDBaseResult { open var detection: CLDDetection? { guard let detection = getParam(.detection) as? [String : AnyObject] else { return nil } return CLDDetection(json: detection) } open var ocr: CLDOcrResult? { guard let ocr = getParam(.ocr) as? [String : AnyObject] else { return nil } return CLDOcrResult(json: ocr) } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDInfoKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDInfoKey: CustomStringConvertible { case detection, ocr var description: String { switch self { case .detection: return "detection" case .ocr : return "ocr" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDAdvOcrResult.swift ================================================ // // CLDAdvOcrResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDAdvOcrResult: CLDBaseResult { open var status: String? { guard let status = getParam(.status) as? String else { return nil } return status } open var data: [CLDOcrDataResult]? { guard let data = getParam(.data) as? [[String : AnyObject]] else { return nil } return data.compactMap{ CLDOcrDataResult(json: $0) } } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDAdvOcrResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDAdvOcrResultKey: CustomStringConvertible { case status, data var description: String { switch self { case .status: return "status" case .data : return "data" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDOcrBlockResult.swift ================================================ // // CLDOcrBlockResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDOcrBlockResult: CLDBaseResult { open var boundingBox: CLDOcrBoundindBlockResult? { guard let boundingBox = getParam(.boundingBox) as? [String : AnyObject] else { return nil } return CLDOcrBoundindBlockResult(json: boundingBox) } open var property: CLDOcrPropertyResult? { guard let property = getParam(.property) as? [String : AnyObject] else { return nil } return CLDOcrPropertyResult(json: property) } open var paragraphs: [CLDOcrParagraphResult]? { guard let paragraphs = getParam(.paragraphs) as? [[String : AnyObject]] else { return nil } return paragraphs.compactMap({ CLDOcrParagraphResult(json: $0) }) } open var blockType: String? { guard let blockType = getParam(.blockType) as? String else { return nil } return blockType } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDOcrBlockResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDOcrBlockResultKey: CustomStringConvertible { case boundingBox, property, paragraphs, blockType var description: String { switch self { case .boundingBox: return "boundingBox" case .property : return "property" case .paragraphs : return "paragraphs" case .blockType : return "blockType" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDOcrBoundindBlockResult.swift ================================================ // // CLDOcrBoundindBlockResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import CoreGraphics @objcMembers open class CLDOcrBoundindBlockResult: CLDBaseResult { open var vertices: [CGPoint]? { guard let vertices = getParam(.vertices) as? [[String : AnyObject]] else { return nil} return vertices.compactMap{ if let x = $0["x"] as? NSNumber, let y = $0["y"] as? NSNumber { return CGPoint(x: x.intValue, y: y.intValue) } else { return nil } } } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDOcrBoundindBlockResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDOcrBoundindBlockResultKey: CustomStringConvertible { case vertices var description: String { switch self { case .vertices : return "vertices" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDOcrDataResult.swift ================================================ // // CLDOcrDataResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDOcrDataResult: CLDBaseResult { open var textAnnotations: [CLDOcrTextAnnotationResult]? { guard let textAnnotations = getParam(.textAnnotations) as? [[String : AnyObject]] else { return nil } return textAnnotations.compactMap{ CLDOcrTextAnnotationResult(json: $0) } } open var fullTextAnnotation: CLDOcrFullTextAnnotationResult? { guard let fullTextAnnotation = getParam(.fullTextAnnotation) as? [String : AnyObject] else { return nil } return CLDOcrFullTextAnnotationResult(json: fullTextAnnotation) } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDOcrDataResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDOcrDataResultKey: CustomStringConvertible { case textAnnotations, fullTextAnnotation var description: String { switch self { case .textAnnotations : return "textAnnotations" case .fullTextAnnotation : return "fullTextAnnotation" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDOcrDetectedLanguagesResult.swift ================================================ // // CLDOcrDetectedLanguagesResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDOcrDetectedLanguagesResult: CLDBaseResult { open var languageCode: String? { guard let languageCode = getParam(.languageCode) as? String else { return nil } return languageCode } open var confidence: Int? { guard let confidence = getParam(.confidence) as? Int else { return nil } return confidence } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDOcrDetectedLanguagesResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDOcrDetectedLanguagesResultKey: CustomStringConvertible { case languageCode, confidence var description: String { switch self { case .languageCode: return "languageCode" case .confidence : return "confidence" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDOcrFullTextAnnotationResult.swift ================================================ // // CLDOcrFullTextAnnotationResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDOcrFullTextAnnotationResult: CLDBaseResult { open var pages: [CLDOcrPageResult]? { guard let pages = getParam(.pages) as? [[String : AnyObject]] else { return nil } return pages.compactMap({ CLDOcrPageResult(json: $0) }) } open var text: String? { guard let text = getParam(.text) as? String else { return nil } return text } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDOcrFullTextAnnotationResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDOcrFullTextAnnotationResultKey: CustomStringConvertible { case pages, text var description: String { switch self { case .pages: return "pages" case .text : return "text" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDOcrPageResult.swift ================================================ // // CLDOcrPageResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDOcrPageResult: CLDBaseResult { open var blocks: [CLDOcrBlockResult]? { guard let blocks = getParam(.blocks) as? [[String : AnyObject]] else { return nil } return blocks.compactMap({ CLDOcrBlockResult(json: $0) }) } open var property: CLDOcrPropertyResult? { guard let property = getParam(.property) as? [String : AnyObject] else { return nil } return CLDOcrPropertyResult(json: property) } open var width: Int? { guard let width = getParam(.pageWidth) as? Int else { return nil } return width } open var height: Int? { guard let height = getParam(.pageHeight) as? Int else { return nil } return height } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDOcrPageResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDOcrPageResultKey: CustomStringConvertible { case blocks, property, pageWidth, pageHeight var description: String { switch self { case .blocks : return "blocks" case .property : return "property" case .pageWidth : return "width" case .pageHeight: return "height" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDOcrParagraphResult.swift ================================================ // // CLDOcrParagraphResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDOcrParagraphResult: CLDBaseResult { open var boundingBox: CLDOcrBoundindBlockResult? { guard let boundingBox = getParam(.boundingBox) as? [String : AnyObject] else { return nil } return CLDOcrBoundindBlockResult(json: boundingBox) } open var property: CLDOcrPropertyResult? { guard let property = getParam(.property) as? [String : AnyObject] else { return nil } return CLDOcrPropertyResult(json: property) } open var words: [CLDOcrWordResult]? { guard let words = getParam(.words) as? [[String : AnyObject]] else { return nil } return words.compactMap({ CLDOcrWordResult(json: $0) }) } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDOcrParagraphResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDOcrParagraphResultKey: CustomStringConvertible { case boundingBox, property, words var description: String { switch self { case .boundingBox: return "boundingBox" case .property : return "property" case .words : return "words" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDOcrPropertyResult.swift ================================================ // // CLDOcrPropertyResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDOcrPropertyResult: CLDBaseResult { open var detectedLanguages: [CLDOcrDetectedLanguagesResult]? { guard let detectedLanguages = getParam(.detectedLanguages) as? [[String : AnyObject]] else { return nil } return detectedLanguages.compactMap({ CLDOcrDetectedLanguagesResult(json: $0) }) } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDOcrPropertyResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDOcrPropertyResultKey: CustomStringConvertible { case detectedLanguages var description: String { switch self { case .detectedLanguages: return "detectedLanguages" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDOcrResult.swift ================================================ // // CLDOcrResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDOcrResult: CLDBaseResult { open var advOcr: CLDAdvOcrResult? { guard let advOcr = getParam(.advOcr) as? [String : AnyObject] else { return nil } return CLDAdvOcrResult(json: advOcr) } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDOcrResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDOcrResultKey: CustomStringConvertible { case advOcr var description: String { switch self { case .advOcr: return "adv_ocr" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDOcrSymbolResult.swift ================================================ // // CLDOcrSymbolResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDOcrSymbolResult: CLDBaseResult { open var boundingBox: CLDOcrBoundindBlockResult? { guard let boundingBox = getParam(.boundingBox) as? [String : AnyObject] else { return nil } return CLDOcrBoundindBlockResult(json: boundingBox) } open var property: CLDOcrPropertyResult? { guard let property = getParam(.property) as? [String : AnyObject] else { return nil } return CLDOcrPropertyResult(json: property) } open var text: String? { guard let text = getParam(.text) as? String else { return nil } return text } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDOcrSymbolResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDOcrSymbolResultKey: CustomStringConvertible { case boundingBox, property, text var description: String { switch self { case .boundingBox: return "boundingBox" case .property : return "property" case .text : return "text" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDOcrTextAnnotationResult.swift ================================================ // // CLDOcrTextAnnotationResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDOcrTextAnnotationResult: CLDBaseResult { open var locale: String? { guard let locale = getParam(.locale) as? String else { return nil } return locale } open var textDescription: String? { guard let textDescription = getParam(.description) as? String else { return nil } return textDescription } open var boundingBlock: CLDOcrBoundindBlockResult? { guard let boundingBlock = getParam(.boundingPoly) as? [String : AnyObject] else { return nil } return CLDOcrBoundindBlockResult(json: boundingBlock) } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDOcrTextAnnotationResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDOcrTextAnnotationResultKey: CustomStringConvertible { case locale, description, boundingPoly var description: String { switch self { case .locale : return "locale" case .description : return "description" case .boundingPoly : return "boundingPoly" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDOcr/CLDOcrWordResult.swift ================================================ // // CLDOcrWordResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDOcrWordResult: CLDBaseResult { open var boundingBox: CLDOcrBoundindBlockResult? { guard let boundingBox = getParam(.boundingBox) as? [String : AnyObject] else { return nil } return CLDOcrBoundindBlockResult(json: boundingBox) } open var property: CLDOcrPropertyResult? { guard let property = getParam(.property) as? [String : AnyObject] else { return nil } return CLDOcrPropertyResult(json: property) } open var symbols: [CLDOcrSymbolResult]? { guard let symbols = getParam(.symbols) as? [[String : AnyObject]] else { return nil } return symbols.compactMap({ CLDOcrSymbolResult(json: $0) }) } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDOcrWordResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDOcrWordResultKey: CustomStringConvertible { case boundingBox, property, symbols var description: String { switch self { case .boundingBox: return "boundingBox" case .property : return "property" case .symbols : return "symbols" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDQualityAnalysis/CLDQualityAnalysis.swift ================================================ // // CLDQualityAnalysis.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDQualityAnalysis: CLDBaseResult { open var blockiness: NSNumber? { guard let blockiness = getParam(.blockiness) as? NSNumber else { return nil } return blockiness } open var chromaSubsampling: NSNumber? { guard let chromaSubsampling = getParam(.chromaSubsampling) as? NSNumber else { return nil } return chromaSubsampling } open var resolution: NSNumber? { guard let resolution = getParam(.resolution) as? NSNumber else { return nil } return resolution } open var noise: NSNumber? { guard let noise = getParam(.noise) as? NSNumber else { return nil } return noise } open var colorScore: NSNumber? { guard let colorScore = getParam(.colorScore) as? NSNumber else { return nil } return colorScore } open var jpegChroma: NSNumber? { guard let jpegChroma = getParam(.jpegChroma) as? NSNumber else { return nil } return jpegChroma } open var dct: NSNumber? { guard let dct = getParam(.dct) as? NSNumber else { return nil } return dct } open var jpegQuality: NSNumber? { guard let jpegQuality = getParam(.jpegQuality) as? NSNumber else { return nil } return jpegQuality } open var focus: NSNumber? { guard let focus = getParam(.focus) as? NSNumber else { return nil } return focus } open var saturation: NSNumber? { guard let saturation = getParam(.saturation) as? NSNumber else { return nil } return saturation } open var contrast: NSNumber? { guard let contrast = getParam(.contrast) as? NSNumber else { return nil } return contrast } open var exposure: NSNumber? { guard let exposure = getParam(.exposure) as? NSNumber else { return nil } return exposure } open var lighting: NSNumber? { guard let lighting = getParam(.lighting) as? NSNumber else { return nil } return lighting } open var pixelScore: NSNumber? { guard let pixelScore = getParam(.pixelScore) as? NSNumber else { return nil } return pixelScore } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDQualityAnalysisKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDQualityAnalysisKey: CustomStringConvertible { case blockiness, chromaSubsampling, resolution, noise, colorScore, jpegChroma, dct, jpegQuality, focus, saturation, contrast, exposure, lighting, pixelScore var description: String { switch self { case .blockiness: return "blockiness" case .chromaSubsampling: return "chroma_subsampling" case .resolution: return "resolution" case .noise: return "noise" case .colorScore: return "color_score" case .jpegChroma: return "jpeg_chroma" case .dct: return "dct" case .jpegQuality: return "jpeg_quality" case .focus: return "focus" case .saturation: return "saturation" case .contrast: return "contrast" case .exposure: return "exposure" case .lighting: return "lighting" case .pixelScore: return "pixel_score" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CLDRekognitionFace.swift ================================================ // // CLDRekognitionFace.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDRekognitionFace: CLDBaseResult { open var status: String? { return getParam(.status) as? String } open var faces: [CLDFace]? { guard let facesArr = getParam(.faces) as? [[String : AnyObject]] else { return nil } var faces: [CLDFace] = [] for face in facesArr { faces.append(CLDFace(json: face)) } return faces } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDRekognitionFaceKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDRekognitionFaceKey: CustomStringConvertible { case status, facesData var description: String { switch self { case .status: return "status" case .facesData: return "data" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Helpers/Results/Helpers/CommonResultKeys.swift ================================================ // // CommonResultKeys.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal enum CommonResultKeys: CustomStringConvertible { case publicId, format, version, resourceType, urlType, createdAt, length, width, height, x, y, url, secureUrl, exif, metadata, faces, colors, tags, moderation, context, phash, info, accessControl, eager, qualityAnalysis, coordinates, accessibilityAnalysis, originalFilename, assetFolder, displayName, playbackUrl, metadata_object var description: String { switch self { case .publicId: return "public_id" case .format: return "format" case .version: return "version" case .resourceType: return "resource_type" case .urlType: return "type" case .createdAt: return "created_at" case .length: return "bytes" case .width: return "width" case .height: return "height" case .x: return "x" case .y: return "y" case .url: return "url" case .secureUrl: return "secure_url" case .exif: return "exif" case .metadata: return "image_metadata" case .faces: return "faces" case .colors: return "colors" case .tags: return "tags" case .moderation: return "moderation" case .context: return "context" case .phash: return "phash" case .info: return "info" case .eager: return "eager" case .accessControl: return "access_control" case .qualityAnalysis: return "quality_analysis" case .accessibilityAnalysis: return "accessibility_analysis" case .coordinates: return "coordinates" case .originalFilename: return "original_filename" case .assetFolder: return "asset_folder" case .displayName: return "display_name" case .playbackUrl: return "playback_url" case .metadata_object: return "metadata" } } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/CLDManagementApi.swift ================================================ // // CLDManagementApi.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The CLDManagementApi class is used to perform the available methods for managing your cloud assets. */ @objcMembers open class CLDManagementApi: CLDBaseNetworkObject { // MARK: - Init fileprivate override init() { super.init() } override internal init(networkCoordinator:CLDNetworkCoordinator) { super.init(networkCoordinator: networkCoordinator) } // MARK: - Features /** Rename an asset on your cloud. - parameter publicId: The current identifier of the uploaded asset. - parameter to: The new identifier to assign to the uploaded asset. - parameter overwrite: A boolean parameter indicating whether or not to overwrite an existing image with the target Public ID. Default: false. - parameter invalidate: A boolean parameter indicating whether to invalidate CDN cached copies of the image (and all its transformed versions). Default: false. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDRenameRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func rename(_ publicId: String, to: String, overwrite: Bool? = nil, invalidate: Bool? = nil, params: CLDRenameRequestParams? = nil, completionHandler: ((_ result: CLDRenameResult?, _ error: Error?) -> ())? = nil) -> CLDRenameRequest { let renameParams = CLDRenameRequestParams(fromPublicId: publicId, toPublicId: to, overwrite: overwrite, invalidate: invalidate) renameParams.merge(params) let request = networkCoordinator.callAction(.Rename, params:renameParams) let renameRequest = CLDRenameRequest(networkRequest: request) renameRequest.response(completionHandler) return renameRequest } /** Immediately and permanently delete assets from your Cloudinary account - parameter publicId: The identifier of the asset to remove. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDDeleteRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func destroy(_ publicId: String, params: CLDDestroyRequestParams? = nil, completionHandler: ((_ result: CLDDeleteResult?, _ error: Error?) -> ())? = nil) -> CLDDeleteRequest { let destroyParams = CLDDestroyRequestParams(publicId: publicId) destroyParams.merge(params) let request = networkCoordinator.callAction(.Destroy, params: destroyParams) let destroyRequest = CLDDeleteRequest(networkRequest: request) destroyRequest.response(completionHandler) return destroyRequest } /** Add a tag to one or more assets in your cloud. Tags are used to categorize and organize your images, and can also be used to apply group actions to images, for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). - parameter tag: The tag to assign. - parameter publicIds: An array of Public IDs of images uploaded to Cloudinary. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDTagRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func addTag(_ tag: String, publicIds: [String], params: CLDTagsRequestParams? = nil, completionHandler: ((_ result: CLDTagResult?, _ error: Error?) -> ())? = nil) -> CLDTagRequest { let addTagParams = CLDTagsRequestParams(tags: [tag], publicIds: publicIds) addTagParams.merge(params) let request = networkCoordinator.callAction(.Tags, params: addTagParams.setCommand(.add)) let tagRequest = CLDTagRequest(networkRequest: request) tagRequest.response(completionHandler) return tagRequest } /** Add the tags to one or more assets in your cloud. Tags are used to categorize and organize your images, and can also be used to apply group actions to images, for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). - parameter tag: An array of tags to assign - parameter publicIds: An array of Public IDs of images uploaded to Cloudinary. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDTagRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult @objc(addTags:publicIds:params:completionHandler:) open func addTag(_ tag: [String], publicIds: [String], params: CLDTagsRequestParams? = nil, completionHandler: ((_ result: CLDTagResult?, _ error: Error?) -> ())? = nil) -> CLDTagRequest { let addTagParams = CLDTagsRequestParams(tags: tag, publicIds: publicIds) addTagParams.merge(params) let request = networkCoordinator.callAction(.Tags, params: addTagParams.setCommand(.add)) let tagRequest = CLDTagRequest(networkRequest: request) tagRequest.response(completionHandler) return tagRequest } /** Remove a tag to one or more assets in your cloud. Tags are used to categorize and organize your images, and can also be used to apply group actions to images, for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). - parameter tag: The tag to remove. - parameter publicIds: An array of Public IDs of images uploaded to Cloudinary. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDTagRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func removeTag(_ tag: String, publicIds: [String], params: CLDTagsRequestParams? = nil, completionHandler: ((_ result: CLDTagResult?, _ error: Error?) -> ())? = nil) -> CLDTagRequest { let removeTagParams = CLDTagsRequestParams(tags: [tag], publicIds: publicIds) removeTagParams.merge(params) let request = networkCoordinator.callAction(.Tags, params: removeTagParams.setCommand(.remove)) let tagRequest = CLDTagRequest(networkRequest: request) tagRequest.response(completionHandler) return tagRequest } /** Remove tags to one or more assets in your cloud. Tags are used to categorize and organize your images, and can also be used to apply group actions to images, for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). - parameter tag: An array of tags to remove - parameter publicIds: An array of Public IDs of images uploaded to Cloudinary. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDTagRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult @objc(removeTags:publicIds:params:completionHandler:) open func removeTag(_ tag: [String], publicIds: [String], params: CLDTagsRequestParams? = nil, completionHandler: ((_ result: CLDTagResult?, _ error: Error?) -> ())? = nil) -> CLDTagRequest { let removeTagParams = CLDTagsRequestParams(tags: tag, publicIds: publicIds) removeTagParams.merge(params) let request = networkCoordinator.callAction(.Tags, params: removeTagParams.setCommand(.remove)) let tagRequest = CLDTagRequest(networkRequest: request) tagRequest.response(completionHandler) return tagRequest } /** Replaces a tag to one or more assets in your cloud. Tags are used to categorize and organize your images, and can also be used to apply group actions to images, for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). - parameter tag: The tag to replace. - parameter publicIds: An array of Public IDs of images uploaded to Cloudinary. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDTagRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func replaceTag(_ tag: String, publicIds: [String], params: CLDTagsRequestParams? = nil, completionHandler: ((_ result: CLDTagResult?, _ error: Error?) -> ())? = nil) -> CLDTagRequest { let replaceTagParams = CLDTagsRequestParams(tags: [tag], publicIds: publicIds) replaceTagParams.merge(params) let request = networkCoordinator.callAction(.Tags, params: replaceTagParams.setCommand(.replace)) let tagRequest = CLDTagRequest(networkRequest: request) tagRequest.response(completionHandler) return tagRequest } /** Replaces tags to one or more assets in your cloud. Tags are used to categorize and organize your images, and can also be used to apply group actions to images, for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). - parameter tag: The array of tags to replace. - parameter publicIds: An array of Public IDs of images uploaded to Cloudinary. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDTagRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult @objc(replaceTags:publicIds:params:completionHandler:) open func replaceTag(_ tag: [String], publicIds: [String], params: CLDTagsRequestParams? = nil, completionHandler: ((_ result: CLDTagResult?, _ error: Error?) -> ())? = nil) -> CLDTagRequest { let replaceTagParams = CLDTagsRequestParams(tags: tag, publicIds: publicIds) replaceTagParams.merge(params) let request = networkCoordinator.callAction(.Tags, params: replaceTagParams.setCommand(.replace)) let tagRequest = CLDTagRequest(networkRequest: request) tagRequest.response(completionHandler) return tagRequest } /** The explicit method is used to apply actions to already uploaded images, i.e., to update images that have already been uploaded. The most common usage of this method is to generate transformations for images that have already been uploaded, either so that they do not need to be generated on the fly when first accessed by users, or because Strict Transformations are enabled for your account and you cannot create transformed images on the fly (for more information, see [Access control to images](http://cloudinary.com/documentation/upload_images#control_access_to_images)). - parameter publicId: The identifier of the uploaded asset. - parameter type: The specific type of the resource. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDExplicitRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult @objc(explicitPublicId:stringType:params:completionHandler:) open func explicit(_ publicId: String, type: String, params: CLDExplicitRequestParams? = nil, completionHandler: ((_ result: CLDExplicitResult?, _ error: Error?) -> ())? = nil) -> CLDExplicitRequest { let explicitParams = CLDExplicitRequestParams(publicId: publicId, type: type) explicitParams.merge(params) let request = networkCoordinator.callAction(.Explicit, params: explicitParams) let explicitRequest = CLDExplicitRequest(networkRequest: request) explicitRequest.response(completionHandler) return explicitRequest } /** The explicit method is used to apply actions to already uploaded images, i.e., to update images that have already been uploaded. The most common usage of this method is to generate transformations for images that have already been uploaded, either so that they do not need to be generated on the fly when first accessed by users, or because Strict Transformations are enabled for your account and you cannot create transformed images on the fly (for more information, see [Access control to images](http://cloudinary.com/documentation/upload_images#control_access_to_images)). - parameter publicId: The identifier of the uploaded asset. - parameter type: The specific type of the resource. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDExplicitRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func explicit(_ publicId: String, type: CLDType, params: CLDExplicitRequestParams? = nil, completionHandler: ((_ result: CLDExplicitResult?, _ error: Error?) -> ())? = nil) -> CLDExplicitRequest { return explicit(publicId, type: String(describing: type), params: params, completionHandler: completionHandler) } /** The explode method creates derived images for all the individual pages in a PDF file. Each derived image created is stored with the same Public ID as the PDF file, and can be accessed using the page parameter for delivering an image of a specific PDF page. This method is useful for pregenerating all the pages of the PDF as individual images so that they do not need to be generated on the fly when first accessed by your users. - parameter publicId: The identifier of the uploaded asset. - parameter transformation: A transformation to run on all the pages before storing them as derived images. This parameter is given as an array (using the SDKs) or comma-separated list (for direct API calls) of transformations, and separated with a slash for chained transformations. At minimum, you must pass the page transformation with the value all. If you supply additional transformations, you must deliver the image using the same relative order of the page and the other transformations. If you use a different order when you deliver, then it is considered a different transformation, and will be generated on-the-fly as a new derived image. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDExplodeRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func explode(_ publicId: String, transformation: CLDTransformation, params: CLDExplodeRequestParams? = nil, completionHandler: ((_ result: CLDExplodeResult?, _ error: Error?) -> ())? = nil) -> CLDExplodeRequest { let explodeParams = CLDExplodeRequestParams(publicId: publicId, transformation: transformation) explodeParams.merge(params) let request = networkCoordinator.callAction(.Explode, params: explodeParams) let explodeRequest = CLDExplodeRequest(networkRequest: request) explodeRequest.response(completionHandler) return explodeRequest } /** Generate sprites by merging multiple images into a single large image for reducing network overhead and bypassing download limitations. This method creates a sprite from all images that have been assigned a specified tag. - parameter tag: The sprite is created from all images with this tag. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDSpriteRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func generateSprite(_ tag: String, params: CLDSpriteRequestParams? = nil, completionHandler: ((_ result: CLDSpriteResult?, _ error: Error?) -> ())? = nil) -> CLDSpriteRequest { let generateSpriteParams = CLDSpriteRequestParams(tag: tag) generateSpriteParams.merge(params) let request = networkCoordinator.callAction(.GenerateSprite, params: generateSpriteParams) let spriteRequest = CLDSpriteRequest(networkRequest: request) spriteRequest.response(completionHandler) return spriteRequest } /** Create a single animated GIF file from all images that have been assigned a specified tag, where each image is included as a single frame of the resulting animating GIF (sorted alphabetically by their Public ID). For a detailed explanation on generating animated GIFs, see the [documentation on creating animated GIFs.](http://cloudinary.com/documentation/image_transformations#creating_animated_gifs) - parameter tag: The sprite is created from all images with this tag. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDMultiRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func multi(_ tag: String, params: CLDMultiRequestParams? = nil, completionHandler: ((_ result: CLDMultiResult?, _ error: Error?) -> ())? = nil) -> CLDMultiRequest { let multiParams = CLDMultiRequestParams(tag: tag) multiParams.merge(params) let request = networkCoordinator.callAction(.Multi, params: multiParams) let multiRequest = CLDMultiRequest(networkRequest: request) multiRequest.response(completionHandler) return multiRequest } /** Dynamically generate an image from a given textual string. You can then use this textual image as any other image, for example, as an overlay for other images. Various font, color and style parameters can be specified to customize the look & feel of the text before converting it to an image. - parameter text: The text string to generate an image for. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDTextRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func text(_ text: String, params: CLDTextRequestParams? = nil, completionHandler: ((_ result: CLDTextResult?, _ error: Error?) -> ())? = nil) -> CLDTextRequest { let textParams = CLDTextRequestParams(text: text) textParams.merge(params) let request = networkCoordinator.callAction(.GenerateText, params: textParams) let textRequest = CLDTextRequest(networkRequest: request) textRequest.response(completionHandler) return textRequest } /** The Cloudinary library supports using a delete token to delete images on the client-side for a limited time of 10 minutes after being uploaded. After 10 minutes has passed, the image cannot be deleted from the client side, only via the Destroy method. In order to also receive a deletion token in the upload response, add the return_delete_token parameter to the upload method and set it to true. - parameter token: The delete token received in the upload response, after uploading the asset using `return_delete_token` set to true. - parameter params: An object holding the available parameters for the request. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance of `CLDDeleteRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func deleteByToken(_ token: String, params: CLDDeleteByTokenRequestParams? = nil, completionHandler: ((_ result: CLDDeleteResult?, _ error: Error?) -> ())? = nil) -> CLDDeleteRequest { let deleteByTokenParams = CLDDeleteByTokenRequestParams(token: token) deleteByTokenParams.merge(params) let request = networkCoordinator.callAction(.DeleteByToken, params: deleteByTokenParams) let deleteByTokenRequest = CLDDeleteRequest(networkRequest: request) deleteByTokenRequest.response(completionHandler) return deleteByTokenRequest } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Requests/CLDDeleteRequest.swift ================================================ // // CLDDeleteRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The `CLDExplodeRequest` object is returned when creating an explode request. It allows the options to add a response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @objcMembers open class CLDDeleteRequest: CLDRequest { /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDDeleteRequest. */ @discardableResult open func response(_ completionHandler: ((_ result: CLDDeleteResult?, _ error: NSError?) -> ())?) -> CLDDeleteRequest { responseRaw { (response, error) in if let res = response as? [String : AnyObject] { completionHandler?(CLDDeleteResult(json: res), nil) } else if let err = error { completionHandler?(nil, err) } else { completionHandler?(nil, CLDError.generalError()) } } return self } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Requests/CLDExplicitRequest.swift ================================================ // // CLDExplicitRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The `CLDExplodeRequest` object is returned when creating an explode request. It allows the options to add a response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @objcMembers open class CLDExplicitRequest: CLDRequest { /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDExplicitRequest. */ @discardableResult open func response(_ completionHandler: ((_ result: CLDExplicitResult?, _ error: NSError?) -> ())?) -> CLDExplicitRequest { responseRaw { (response, error) in if let res = response as? [String : AnyObject] { completionHandler?(CLDExplicitResult(json: res), nil) } else if let err = error { completionHandler?(nil, err) } else { completionHandler?(nil, CLDError.generalError()) } } return self } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Requests/CLDExplodeRequest.swift ================================================ // // CLDExplodeRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The `CLDExplodeRequest` object is returned when creating an explode request. It allows the options to add a response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @objcMembers open class CLDExplodeRequest: CLDRequest { /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDExplodeRequest. */ @discardableResult open func response(_ completionHandler: ((_ result: CLDExplodeResult?, _ error: NSError?) -> ())?) -> CLDExplodeRequest { responseRaw { (response, error) in if let res = response as? [String : AnyObject] { completionHandler?(CLDExplodeResult(json: res), nil) } else if let err = error { completionHandler?(nil, err) } else { completionHandler?(nil, CLDError.generalError()) } } return self } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Requests/CLDMultiRequest.swift ================================================ // // CLDMultiRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The `CLDMultiRequest` object is returned when creating a multi request. It allows the options to add a response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @objcMembers open class CLDMultiRequest: CLDRequest { /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDMultiRequest. */ @discardableResult open func response(_ completionHandler: ((_ result: CLDMultiResult?, _ error: NSError?) -> ())?) -> CLDMultiRequest { responseRaw { (response, error) in if let res = response as? [String : AnyObject] { completionHandler?(CLDMultiResult(json: res), nil) } else if let err = error { completionHandler?(nil, err) } else { completionHandler?(nil, CLDError.generalError()) } } return self } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Requests/CLDRenameRequest.swift ================================================ // // CLDRenameRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The `CLDRenameRequest` object is returned when creating a rename request. It allows the options to add a response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @objcMembers open class CLDRenameRequest: CLDRequest { /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDRenameRequest. */ @discardableResult open func response(_ completionHandler: ((_ result: CLDRenameResult?, _ error: NSError?) -> ())?) -> CLDRenameRequest { responseRaw { (response, error) in if let res = response as? [String : AnyObject] { completionHandler?(CLDRenameResult(json: res), nil) } else if let err = error { completionHandler?(nil, err) } else { completionHandler?(nil, CLDError.generalError()) } } return self } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Requests/CLDSpriteRequest.swift ================================================ // // CLDSpriteRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The `CLDSpriteRequest` object is returned when creating a sprite request. It allows the options to add a response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @objcMembers open class CLDSpriteRequest: CLDRequest { /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDSpriteRequest. */ @discardableResult open func response(_ completionHandler: ((_ result: CLDSpriteResult?, _ error: NSError?) -> ())?) -> CLDSpriteRequest { responseRaw { (response, error) in if let res = response as? [String : AnyObject] { completionHandler?(CLDSpriteResult(json: res), nil) } else if let err = error { completionHandler?(nil, err) } else { completionHandler?(nil, CLDError.generalError()) } } return self } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Requests/CLDTagRequest.swift ================================================ // // CLDTagRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The `CLDTagRequest` object is returned when creating a tag request. It allows the options to add a response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @objcMembers open class CLDTagRequest: CLDRequest { /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDTagRequest. */ @discardableResult open func response(_ completionHandler: ((_ result: CLDTagResult?, _ error: NSError?) -> ())?) -> CLDTagRequest { responseRaw { (response, error) in if let res = response as? [String : AnyObject] { completionHandler?(CLDTagResult(json: res), nil) } else if let err = error { completionHandler?(nil, err) } else { completionHandler?(nil, CLDError.generalError()) } } return self } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Requests/CLDTextRequest.swift ================================================ // // CLDTextRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The `CLDTagRequest` object is returned when creating a tag request. It allows the options to add a response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @objcMembers open class CLDTextRequest: CLDRequest { /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDTextRequest. */ @discardableResult open func response(_ completionHandler: ((_ result: CLDTextResult?, _ error: NSError?) -> ())?) -> CLDTextRequest { responseRaw { (response, error) in if let res = response as? [String : AnyObject] { completionHandler?(CLDTextResult(json: res), nil) } else if let err = error { completionHandler?(nil, err) } else { completionHandler?(nil, CLDError.generalError()) } } return self } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/RequestsParams/CLDDeleteByTokenRequestParams.swift ================================================ // // CLDDeleteByTokenRequestParams.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** This class represents the different parameters that can be passed when performing a `delete by token` request. */ open class CLDDeleteByTokenRequestParams: CLDRequestParams { // MARK: Init fileprivate override init() { super.init() } /** Initializes a CLDDeleteByTokenRequestParams instance. - parameter token: The delete token received in the upload response, after uploading the asset using `return_delete_token` set to true. - returns: A new instance of CLDDeleteByTokenRequestParams. */ internal init(token: String) { super.init() setParam(DeleteByTokenParams.Token.rawValue, value: token) } /** Initializes a CLDDeleteByTokenRequestParams instance. - parameter params: A dictionary of the request parameters. - returns: A new instance of CLDDeleteByTokenRequestParams. */ public init(params: [String : Any]) { super.init() self.params = params } // MARK: Params fileprivate enum DeleteByTokenParams: String { case Token = "token" } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/RequestsParams/CLDDestroyRequestParams.swift ================================================ // // CLDDestroyRequestParams.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** This class represents the different parameters that can be passed when performing a destroy request. */ @objcMembers open class CLDDestroyRequestParams: CLDRequestParams { //MARK: Init public override init() { super.init() } /** Initializes a CLDDestroyRequestParams instance. - parameter publicId: The identifier of the asset to remove. - returns: A new instance of CLDDestroyRequestParams. */ internal init(publicId: String) { super.init() setParam(DestroyParams.PublicId.rawValue, value: publicId) } /** Initializes a CLDDestroyRequestParams instance. - parameter params: A dictionary of the request parameters. - returns: A new instance of CLDDestroyRequestParams. */ public init(params: [String : AnyObject]) { super.init() self.params = params } //MARK: Set Params /** Set the specific file type of the resource using one of the available options from CLDType. - parameter type: The file type to set. - returns: The same instance of CLDDestroyRequestParams. */ @discardableResult @objc(setTypeWithType:) open func setType(_ type: CLDType) -> Self { return setType(String(describing: type)) } /** Set the specific file type of the resource. - parameter type: The specific file type of the resource. - returns: The same instance of CLDExplodeRequestParams. */ @discardableResult open func setType(_ type: String) -> Self { super.setParam(DestroyParams.CloudType.rawValue, value: type) return self } /** Set boolean parameter indicating whether or not the asset should be invalidated through the CDN. default is false. - parameter invalidate: The boolean parameter. - returns: The same instance of CLDDestroyRequestParams. */ @discardableResult open func setInvalidate(_ invalidate: Bool) -> Self { super.setParam(DestroyParams.Invalidate.rawValue, value: invalidate) return self } fileprivate enum DestroyParams: String { case PublicId = "public_id" case CloudType = "type" case Invalidate = "invalidate" } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/RequestsParams/CLDExplicitRequestParams.swift ================================================ // // CLDExplicitRequestParams.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** This class represents the different parameters that can be passed when performing an `explicit` request. */ @objcMembers open class CLDExplicitRequestParams: CLDUploadRequestParams { //MARK: Init public override init() { super.init() } /** Initializes a CLDExplicitRequestParams instance. - parameter publicId: The identifier of the uploaded asset. - parameter type: The specific file type of the resource. - returns: A new instance of CLDExplicitRequestParams. */ internal convenience init(publicId: String, type: CLDType) { self.init(publicId: publicId, type: String(describing: type)) } /** Initializes a CLDExplicitRequestParams instance. - parameter publicId: The identifier of the uploaded asset. - parameter type: The specific file type of the resource. - returns: A new instance of CLDExplicitRequestParams. */ @objc(initWithPublicId:andType:) internal init(publicId: String, type: String) { super.init() setPublicId(publicId) setType(type) } /** Initializes a CLDExplicitRequestParams instance. - parameter params: A dictionary of the request parameters. - returns: A new instance of CLDExplicitRequestParams. */ public override init(params: [String : AnyObject]) { super.init(params: params) } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/RequestsParams/CLDExplodeRequestParams.swift ================================================ // // CLDExplodeRequestParams.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** This class represents the different parameters that can be passed when performing an `explode` request. */ @objcMembers open class CLDExplodeRequestParams: CLDRequestParams { // MARK: Init public override init() { super.init() } /** Initializes a CLDExplodeRequestParams instance. - parameter publicId: The identifier of the uploaded asset. - parameter transformation: A transformation to run on all the pages before storing them as derived images. This parameter is given as an array (using the SDKs) or comma-separated list (for direct API calls) of transformations, and separated with a slash for chained transformations. At minimum, you must pass the page transformation with the value all. If you supply additional transformations, you must deliver the image using the same relative order of the page and the other transformations. If you use a different order when you deliver, then it is considered a different transformation, and will be generated on-the-fly as a new derived image. - returns: A new instance of CLDExplodeRequestParams. */ internal init(publicId: String, transformation: CLDTransformation) { super.init() setParam(ExplodeParams.PublicId.rawValue, value: publicId) setParam(ExplodeParams.Transformation.rawValue, value: transformation.asString()) } /** Initializes a CLDExplodeRequestParams instance. - parameter params: A dictionary of the request parameters. - returns: A new instance of CLDExplodeRequestParams. */ public init(params: [String : AnyObject]) { super.init() self.params = params } // MARK: Set Params /** Set the specific file type of the resource. - parameter type: The specific file type of the resource. - returns: The same instance of CLDExplodeRequestParams. */ @discardableResult @objc(setTypeWithType:) open func setType(_ type: CLDType) -> Self { return setType(String(describing: type)) } /** Set the specific file type of the resource. - parameter type: The specific file type of the resource. - returns: The same instance of CLDExplodeRequestParams. */ @discardableResult open func setType(_ type: String) -> Self { setParam(ExplodeParams.CloudType.rawValue, value: type) return self } /** Set a format to convert the images before storing them in your Cloudinary account. default is jpg. - parameter format: The format to convert to. - returns: The same instance of CLDExplodeRequestParams. */ @discardableResult open func setFormat(_ format: String) -> Self { setParam(ExplodeParams.Format.rawValue, value: format) return self } /** Set a boolean parameter indicating whether to perform the image generation in the background (asynchronously). default is false. - parameter async: The boolean parameter. - returns: The same instance of CLDExplodeRequestParams. */ @discardableResult open func setAsync(_ async: Bool) -> Self { setParam(ExplodeParams.Async.rawValue, value: async) return self } /** Set an HTTP or HTTPS URL to notify your application (a webhook) when the process has completed. - parameter notificationUrl: The URL. - returns: The same instance of CLDExplodeRequestParams. */ @discardableResult open func setNotificationUrl(_ notificationUrl: String) -> Self { setParam(ExplodeParams.NotificationUrl.rawValue, value: notificationUrl) return self } fileprivate enum ExplodeParams: String { case PublicId = "public_id" case CloudType = "type" case Transformation = "transformation" case Format = "format" case Async = "async" case NotificationUrl = "notification_url" } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/RequestsParams/CLDMultiRequestParams.swift ================================================ // // CLDMultiRequestParams.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** This class represents the different parameters that can be passed when performing a request to generate a multi-image. */ open class CLDMultiRequestParams: CLDRequestParams { // MARK: Init public override init() { super.init() } /** Initializes a CLDMultiRequestParams instance. - parameter tag: The sprite is created from all images with this tag. - returns: A new instance of CLDMultiRequestParams. */ internal init(tag: String) { super.init() setParam(MultiParams.Tag.rawValue, value: tag) } /** Initializes a CLDMultiRequestParams instance. - parameter params: A dictionary of the request parameters. - returns: A new instance of CLDMultiRequestParams. */ public init(params: [String : AnyObject]) { super.init() self.params = params } // MARK: Set Params /** Set a transformation to run on all the individual images before creating the sprite. - parameter transformation: The transformation to run. - returns: A new instance of CLDMultiRequestParams. */ @discardableResult open func setTransformation(_ transformation: CLDTransformation) -> Self { if let trans = transformation.asString() { super.setParam(MultiParams.Transformation.rawValue, value: trans) } return self } /** Set a format to convert the images before storing them in your Cloudinary account. default is jpg. - parameter format: The format to convert to. - returns: A new instance of CLDMultiRequestParams. */ @discardableResult open func setFormat(_ format: String) -> Self { super.setParam(MultiParams.Format.rawValue, value: format) return self } /** Set a boolean parameter indicating whether to perform the image generation in the background (asynchronously). default is false. - parameter async: The boolean parameter. - returns: A new instance of CLDMultiRequestParams. */ @discardableResult open func setAsync(_ async: Bool) -> Self { super.setParam(MultiParams.Async.rawValue, value: async) return self } /** Set an HTTP or HTTPS URL to notify your application (a webhook) when the process has completed. - parameter notificationUrl: The URL. - returns: A new instance of CLDMultiRequestParams. */ @discardableResult open func setNotificationUrl(_ notificationUrl: String) -> Self { super.setParam(MultiParams.NotificationUrl.rawValue, value: notificationUrl) return self } fileprivate enum MultiParams: String { case Tag = "tag" case Transformation = "transformation" case Format = "format" case Async = "async" case NotificationUrl = "notification_url" } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/RequestsParams/CLDRenameRequestParams.swift ================================================ // // CLDRenameRequestParams.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** This class represents the different parameters that can be passed when performing a rename request. */ @objcMembers open class CLDRenameRequestParams: CLDRequestParams { // MARK: Init public override init() { super.init() } /** Initializes a CLDRenameRequestParams instance. - parameter fromPublicId: The current identifier of the uploaded asset. - parameter toPublicId: The new identifier to assign to the uploaded asset. - parameter overwrite: A boolean parameter indicating whether or not to overwrite an existing image with the target Public ID. Default: false. - parameter invalidate: A boolean parameter whether to invalidate CDN cached copies of the image (and all its transformed versions). Default: false. - parameter notification_url An HTTP or HTTPS URL to notify your application (a webhook) when the process has completed. If not specified, the response is sent to the Notification URL (if defined) in the Webhook Notifications settings of your Cloudinary Console. - parameter context: A boolean parameter indicating whether to include contextual metadata for the asset in the response. Default: false. - parameter metadata: A boolean parameter indicating whether to include structured metadata for the asset in the response. Default: false. - returns: A new instance of CLDRenameRequestParams. */ internal init(fromPublicId: String, toPublicId: String, overwrite: Bool? = nil, invalidate: Bool? = nil, notificationUrl: String? = nil, context: Bool? = nil, metadata: Bool? = nil) { super.init() setParam(RenameParams.FromPublicId.rawValue, value: fromPublicId) setParam(RenameParams.ToPublicId.rawValue, value: toPublicId) setParam(RenameParams.Overwrite.rawValue, value: overwrite) setParam(RenameParams.Invalidate.rawValue, value: invalidate) setParam(RenameParams.NotificationUrl.rawValue, value: notificationUrl) setParam(RenameParams.Context.rawValue, value: context) setParam(RenameParams.Metadata.rawValue, value: metadata) } /** Initializes a CLDRenameRequestParams instance. - parameter params: A dictionary of the request parameters. - returns: A new instance of CLDRenameRequestParams. */ public init(params: [String : AnyObject]) { super.init() self.params = params } // MARK: - Rename Params fileprivate enum RenameParams: String { case FromPublicId = "from_public_id" case ToPublicId = "to_public_id" case Overwrite = "overwrite" case Invalidate = "invalidate" case NotificationUrl = "notification_url" case Context = "context" case Metadata = "metadata" } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/RequestsParams/CLDSpriteRequestParams.swift ================================================ // // CLDSpriteRequestParams.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** This class represents the different parameters that can be passed when performing a request to generate a sprite. */ @objcMembers open class CLDSpriteRequestParams: CLDRequestParams { // MARK: Init public override init() { super.init() } /** Initializes a CLDSpriteRequestParams instance. - parameter tag: The sprite is created from all images with this tag. - returns: A new instance of CLDSpriteRequestParams. */ internal init(tag: String) { super.init() setParam(SpriteParams.Tag.rawValue, value: tag) } /** Initializes a CLDSpriteRequestParams instance. - parameter params: A dictionary of the request parameters. - returns: A new instance of CLDSpriteRequestParams. */ public init(params: [String : AnyObject]) { super.init() self.params = params } // MARK: Set Params /** Set a transformation to run on all the individual images before creating the sprite. - parameter transformation: The transformation to run. - returns: A new instance of CLDSpriteRequestParams. */ @discardableResult open func setTransformation(_ transformation: CLDTransformation) -> Self { if let stringRep = transformation.asString() { setParam(SpriteParams.Transformation.rawValue, value: stringRep) } return self } /** Set a format to convert the images before storing them in your Cloudinary account. default is jpg. - parameter format: The format to convert to. - returns: A new instance of CLDSpriteRequestParams. */ @discardableResult open func setFormat(_ format: String) -> Self { super.setParam(SpriteParams.Format.rawValue, value: format) return self } /** Set a boolean parameter indicating whether to perform the image generation in the background (asynchronously). default is false. - parameter async: The boolean parameter. - returns: A new instance of CLDSpriteRequestParams. */ @discardableResult open func setAsync(_ async: Bool) -> Self { super.setParam(SpriteParams.Async.rawValue, value: async) return self } /** Set an HTTP or HTTPS URL to notify your application (a webhook) when the process has completed. - parameter notificationUrl: The URL. - returns: A new instance of CLDSpriteRequestParams. */ @discardableResult open func setNotificationUrl(_ notificationUrl: String) -> Self { super.setParam(SpriteParams.NotificationUrl.rawValue, value: notificationUrl) return self } fileprivate enum SpriteParams: String { case Tag = "tag" case Transformation = "transformation" case Format = "format" case Async = "async" case NotificationUrl = "notification_url" } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/RequestsParams/CLDTagsRequestParams.swift ================================================ // // CLDTagsRequestParams.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** This class represents the different parameters that can be passed when performing a tag request. */ @objcMembers open class CLDTagsRequestParams: CLDRequestParams { //MARK: Init public override init() { super.init() } /** Initializes a CLDTagsRequestParams instance. - parameter tag: The tag to assign, remove, or replace. - parameter publicIds: An array of Public IDs of images uploaded to Cloudinary. - returns: A new instance of CLDTagsRequestParams. */ internal init(tags: [String], publicIds: [String]) { super.init() setParam(TagsParams.Tag.rawValue, value: tags.joined(separator: ",")) setParam(TagsParams.PublicIds.rawValue, value: publicIds) } /** Initializes a CLDTagsRequestParams instance. - parameter params: A dictionary of the request parameters. - returns: A new instance of CLDTagsRequestParams. */ public init(params: [String : AnyObject]) { super.init() self.params = params } // MARK: - Set Params /** Sets the desired action to be done. - parameter command: The action to perform on asset resources using the given tag. Either add the given tag, remove the given tag, or replace the given tag, which adds the given tag while removing all other tags assigned. - returns: The same instance of CLDTagsRequestParams. */ internal func setCommand(_ command: TagsCommand) -> Self { setParam(TagsParams.Command.rawValue, value: command.description) return self } // MARK: Private fileprivate enum TagsParams: String { case PublicIds = "public_ids" case Tag = "tag" case Command = "command" } internal enum TagsCommand: CustomStringConvertible { case add, remove, replace var description: String { switch self { case .add: return "add" case .remove: return "remove" case .replace: return "replace" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/RequestsParams/CLDTextRequestParams.swift ================================================ // // CLDTextRequestParams.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** This class represents the different parameters that can be passed when performing a request to generate a text-image. */ open class CLDTextRequestParams: CLDRequestParams { // MARK: Init public override init() { super.init() } /** Initializes a CLDTextRequestParams instance. - parameter text: The text string to generate an image for. - returns: A new instance of CLDTextRequestParams. */ internal init(text: String) { super.init() setParam(TextParams.Text.rawValue, value: text) } /** Initializes a CLDTextRequestParams instance. - parameter params: A dictionary of the request parameters. - returns: A new instance of CLDTextRequestParams. */ public init(params: [String : AnyObject]) { super.init() self.params = params } // MARK: Set Params /** Set an identifier that is used for accessing the generated image. If not specified, a unique identifier is generated by Cloudinary. - parameter publicId: The identifier. - returns: A new instance of CLDTextRequestParams. */ @discardableResult open func setPublicId(_ publicId: String) -> CLDTextRequestParams { setParam(TextParams.PublicId.rawValue, value: publicId) return self } /** Set a font family. - parameter fontFamily: The name of the font family. - returns: A new instance of CLDTextRequestParams. */ @discardableResult open func setFontFamily(_ fontFamily: String) -> CLDTextRequestParams { setParam(TextParams.FontFamily.rawValue, value: fontFamily) return self } /** Set the font size in points. - parameter fontSize: The font size in points. default is 12. - returns: A new instance of CLDTextRequestParams. */ @discardableResult open func setFontSizeFromInt(_ fontSize: Int) -> CLDTextRequestParams { return setFontSize(String(fontSize)) } /** Set the font size in points. - parameter fontSize: The font size in points. default is 12. - returns: A new instance of CLDTextRequestParams. */ @discardableResult open func setFontSize(_ fontSize: String) -> CLDTextRequestParams { setParam(TextParams.FontSize.rawValue, value: fontSize) return self } /** Set the font size in points. - parameter fontColor: A name or RGB representation of the font's color. For example: `red` or #ff0000. default is black. - returns: A new instance of CLDTextRequestParams. */ @discardableResult open func setFontColor(_ fontColor: String) -> CLDTextRequestParams { setParam(TextParams.FontColor.rawValue, value: fontColor) return self } /** Set the font weight. - parameter fontWeight: The font weight to set. default is normal. - returns: A new instance of CLDTextRequestParams. */ @discardableResult @objc(setFontWeightFromFontWeight:) open func setFontWeight(_ fontWeight: CLDFontWeight) -> CLDTextRequestParams { return setFontWeight(String(describing: fontWeight)) } /** Set the font weight. - parameter fontWeight: The font weight to set. default is normal. - returns: A new instance of CLDTextRequestParams. */ @discardableResult open func setFontWeight(_ fontWeight: String) -> CLDTextRequestParams { setParam(TextParams.FontWeight.rawValue, value: fontWeight) return self } /** Set the font style. - parameter fontStyle: The font style to set. default is normal. - returns: A new instance of CLDTextRequestParams. */ @discardableResult @objc(setFontStyleFromFontStyle:) open func setFontStyle(_ fontStyle: CLDFontStyle) -> CLDTextRequestParams { return setFontStyle(String(describing: fontStyle)) } /** Set the font style. - parameter fontStyle: The font style to set. default is normal. - returns: A new instance of CLDTextRequestParams. */ @discardableResult open func setFontStyle(_ fontStyle: String) -> CLDTextRequestParams { setParam(TextParams.FontStyle.rawValue, value: fontStyle) return self } /** Set the background color. - parameter background: A name or RGB representation of the background color. For example: `red` or #ff0000. default is transparent. - returns: A new instance of CLDTextRequestParams. */ @discardableResult open func setBackground(_ background: String) -> CLDTextRequestParams { setParam(TextParams.Background.rawValue, value: background) return self } /** Set the text opacity level from 0 to 100. - parameter opacity: The text opacity value between 0 (invisible) and 100. default is 100. - returns: A new instance of CLDTextRequestParams. */ @discardableResult open func setOpacity(_ opacity: Int) -> CLDTextRequestParams { setParam(TextParams.Opacity.rawValue, value: opacity) return self } /** Set a text decoration to add the the generated text, for example: underline. - parameter textDecoration: The text decoration to set. default is none. - returns: A new instance of CLDTextRequestParams. */ @discardableResult @objc(setTextDecorationFromTextDecoration:) open func setTextDecoration(_ textDecoration: CLDTextDecoration) -> CLDTextRequestParams { return setTextDecoration(String(describing: textDecoration)) } /** Set a text decoration to add the the generated text, for example: underline. - parameter textDecoration: The text decoration to set. default is none. - returns: A new instance of CLDTextRequestParams. */ @discardableResult open func setTextDecoration(_ textDecoration: String) -> CLDTextRequestParams { setParam(TextParams.TextDecoration.rawValue, value: textDecoration) return self } fileprivate enum TextParams: String { case Text = "text" case PublicId = "public_id" case FontFamily = "font_family" case FontSize = "font_size" case FontColor = "font_color" case FontWeight = "font_weight" case FontStyle = "font_style" case Background = "background" case Opacity = "opacity" case TextDecoration = "text_decoration" } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Results/CLDDeleteResult.swift ================================================ // // CLDDeleteResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDDeleteResult: CLDBaseResult { // MARK: - Getters open var result: String? { return getParam(.result) as? String } // MARK: - Private Helpers fileprivate func getParam(_ param: DeleteResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum DeleteResultKey: CustomStringConvertible { case result var description: String { switch self { case .result: return "result" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Results/CLDExplicitResult.swift ================================================ // // CLDExplicitResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objc open class CLDExplicitResult: CLDUploadResult { } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Results/CLDExplodeResult.swift ================================================ // // CLDExplodeResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDExplodeResult: CLDBaseResult { // MARK: - Getters open var status: String? { return getParam(.status) as? String } open var batchId: String? { return getParam(.batchId) as? String } // MARK: - Private Helpers fileprivate func getParam(_ param: ExplodeResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum ExplodeResultKey: CustomStringConvertible { case status, batchId var description: String { switch self { case .status: return "status" case .batchId: return "batch_id" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Results/CLDMultiResult.swift ================================================ // // CLDMultiResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDMultiResult: CLDBaseResult { // MARK: - Getters open var url: String? { return getParam(.url) as? String } open var secureUrl: String? { return getParam(.secureUrl) as? String } open var publicId: String? { return getParam(.publicId) as? String } open var version: String? { guard let version = getParam(.version) else { return nil } return String(describing: version) } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Results/CLDRenameResult.swift ================================================ // // CLDRenameResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDRenameResult: CLDBaseResult { // MARK: - Getters open var publicId: String? { return getParam(.publicId) as? String } open var format: String? { return getParam(.format) as? String } open var version: String? { guard let version = getParam(.version) else { return nil } return String(describing: version) } open var resourceType: String? { return getParam(.resourceType) as? String } open var type: String? { return getParam(.urlType) as? String } open var createdAt: String? { return getParam(.createdAt) as? String } open var length: Double? { return getParam(.length) as? Double } open var width: Int? { return getParam(.width) as? Int } open var height: Int? { return getParam(.height) as? Int } open var url: String? { return getParam(.url) as? String } open var secureUrl: String? { return getParam(.secureUrl) as? String } open var nextCursor: String? { return getParam(.nextCursor) as? String } open var exif: [String : String]? { return getParam(.exif) as? [String : String] } open var metadata: [String : String]? { return getParam(.metadata) as? [String : String] } open var metadataObject: [String: String]? { return getParam(.metadata_object) as? [String: String] } open var faces: AnyObject? { return getParam(.faces) } open var colors: AnyObject? { return getParam(.colors) } open var derived: CLDDerived? { guard let derived = getParam(.derived) as? [String : AnyObject] else { return nil } return CLDDerived(json: derived) } open var tags: [String]? { return getParam(.tags) as? [String] } open var moderation: AnyObject? { return getParam(.moderation) } open var context: AnyObject? { return getParam(.context) } open var phash: String? { return getParam(.phash) as? String } open var predominant: CLDPredominant? { guard let predominant = getParam(.predominant) as? [String : AnyObject] else { return nil } return CLDPredominant(json: predominant) } open var coordinates: CLDCoordinates? { guard let coordinates = getParam(RenameResultKey.coordinates) as? [String : AnyObject] else { return nil } return CLDCoordinates(json: coordinates) } open var info: CLDInfo? { guard let info = getParam(.info) as? [String : AnyObject] else { return nil } return CLDInfo(json: info) } // MARK: - Private Helpers fileprivate func getParam(_ param: RenameResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum RenameResultKey: CustomStringConvertible { case nextCursor, derived, predominant, coordinates var description: String { switch self { case .nextCursor: return "next_cursor" case .derived: return "derived" case .predominant: return "predominant" case .coordinates: return "coordinates" } } } } // MARK: - CLDCoordinates @objcMembers open class CLDCoordinates: CLDBaseResult { open var custom: AnyObject? { return getParam(.custom) } open var faces: AnyObject? { return getParam(.faces) } // MARK: Private Helpers fileprivate func getParam(_ param: CLDCoordinatesKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDCoordinatesKey: CustomStringConvertible { case custom var description: String { switch self { case .custom: return "custom" } } } } // MARK: - CLDPredominant @objcMembers open class CLDPredominant: CLDBaseResult { open var google: AnyObject? { return getParam(.google) } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDPredominantKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDPredominantKey: CustomStringConvertible { case google var description: String { switch self { case .google: return "google" } } } } // MARK: - CLDDerived @objcMembers open class CLDDerived: CLDBaseResult { open var transformation: String? { return getParam(.transformation) as? String } open var format: String? { return getParam(.format) as? String } open var length: Double? { return getParam(.length) as? Double } open var identifier: String? { return getParam(.id) as? String } open var url: String? { return getParam(.url) as? String } open var secureUrl: String? { return getParam(.secureUrl) as? String } // MARK: - Private Helpers fileprivate func getParam(_ param: CLDDerivedKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum CLDDerivedKey: CustomStringConvertible { case transformation, id var description: String { switch self { case .transformation: return "transformation" case .id: return "id" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Results/CLDSpriteResult.swift ================================================ // // CLDSpriteResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDSpriteResult: CLDBaseResult { // MARK: - Getters open var cssUrl: String? { return getParam(.cssUrl) as? String } open var secureCssUrl: String? { return getParam(.secureCssUrl) as? String } open var imageUrl: String? { return getParam(.imageUrl) as? String } open var jsonUrl: String? { return getParam(.jsonUrl) as? String } open var publicId: String? { return getParam(.publicId) as? String } open var version: String? { guard let version = getParam(.version) else { return nil } return String(describing: version) } open var imageInfos: [String : CLDImageInfo]? { guard let imageInfosDic = getParam(.imageInfos) as? [String : AnyObject] else { return nil } var imageInfos: [String : CLDImageInfo] = [:] for key in imageInfosDic.keys { if let imgInfo = imageInfosDic[key] as? [String : AnyObject] { imageInfos[key] = CLDImageInfo(json: imgInfo) } } return imageInfos } // MARK: - Private Helpers fileprivate func getParam(_ param: SpriteResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum SpriteResultKey: CustomStringConvertible { case cssUrl, secureCssUrl, imageUrl, jsonUrl, imageInfos var description: String { switch self { case .cssUrl: return "css_url" case .secureCssUrl: return "secure_css_url" case .imageUrl: return "image_url" case .jsonUrl: return "json_url" case .imageInfos: return "image_infos" } } } } @objcMembers open class CLDImageInfo: CLDBaseResult { open var x: Int? { return getParam(.x) as? Int } open var y: Int? { return getParam(.y) as? Int } open var width: Int? { return getParam(.width) as? Int } open var height: Int? { return getParam(.height) as? Int } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Results/CLDTagResult.swift ================================================ // // CLDTagResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDTagResult: CLDBaseResult { // MARK: - Getters open var publicIds: [String]? { return getParam(.publicIds) as? [String] } // MARK: - Private Helpers fileprivate func getParam(_ param: TagResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum TagResultKey: CustomStringConvertible { case publicIds var description: String { switch self { case .publicIds: return "public_ids" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/ManagementApi/Results/CLDTextResult.swift ================================================ // // CLDTextResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDTextResult: CLDBaseResult { // MARK: - Getters open var width: Int? { return getParam(.width) as? Int } open var height: Int? { return getParam(.height) as? Int } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/CLDUploaderWidget.swift ================================================ // // CLDUploaderWidget.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit import AVKit import MobileCoreServices @objc public protocol CLDUploaderWidgetDelegate: AnyObject { /** Informs the delegate that the upload process will begin for the added requests. - parameter widget: The widget object. - parameter uploadRequests: The `CLDUploadRequest`s to be uploaded. */ func uploadWidget(_ widget: CLDUploaderWidget, willCall uploadRequests: [CLDUploadRequest]) /** Informs the delegate that the widget is canceled. - parameter widget: The widget object. */ func widgetDidCancel(_ widget: CLDUploaderWidget) /** Informs the delegate that the widget view is dismissed. - parameter widget: The widget object. */ func uploadWidgetDidDismiss() } @objcMembers public class CLDUploaderWidget: NSObject { public private(set) weak var rootViewController : UIViewController! public private(set) var cloudinaryObject : CLDCloudinary public private(set) var configuration : CLDWidgetConfiguration? public private(set) var images : [UIImage] public private(set) var videos : [AVPlayerItem] private var widgetViewController : CLDWidgetViewController! private var widgetPresented : Bool private var imagePicker : UIImagePickerController! public weak var delegate : CLDUploaderWidgetDelegate? private var selectAssetFromLibrary: Bool { self.images.count == 0 && self.videos.count == 0 } // MARK - public methods /** Initializes the `CLDUploaderWidget` instance with the specified cloudinary, configuration, images and delegate. - parameter cloudinary: The CLDCloudinary object to be used for uploading the selected assets. - parameter configuration: The configuration used by this CLDUploaderWidget instance. - parameter images: The images to be presented, edited and uploaded. - parameter delegate: The delegate object conforming to `CLDUploaderWidgetDelegate`. - returns: The new `CLDUploaderWidget` instance. */ convenience public init( cloudinary: CLDCloudinary, configuration: CLDWidgetConfiguration?, images: [UIImage]?, delegate: CLDUploaderWidgetDelegate? ) { self.init(cloudinary: cloudinary, configuration: configuration, images: images, videos: nil, delegate: delegate) } /** Initializes the `CLDUploaderWidget` instance with the specified cloudinary, configuration, images, videos and delegate. - parameter cloudinary: The CLDCloudinary object to be used for uploading the selected assets. - parameter configuration: The configuration used by this CLDUploaderWidget instance. - parameter images: The images to be presented, edited and uploaded. - parameter videos: The videos to be presented and uploaded. - parameter delegate: The delegate object conforming to `CLDUploaderWidgetDelegate`. - returns: The new `CLDUploaderWidget` instance. */ public init( cloudinary: CLDCloudinary, configuration: CLDWidgetConfiguration?, images: [UIImage]?, videos: [AVPlayerItem]?, delegate: CLDUploaderWidgetDelegate? ) { self.cloudinaryObject = cloudinary self.configuration = configuration self.images = images != nil ? images! : [UIImage]() self.videos = videos != nil ? videos! : [AVPlayerItem]() self.delegate = delegate self.widgetPresented = false super.init() } /** Sets the CLDCloudinary object to be used for uploading the selected assets - parameter cloudinary: The cloudinary object. - returns: The same instance of CLDUploaderWidget. */ @objc(setCloudinaryFromCloudinary:) @discardableResult public func setCloudinary(_ cloudinary: CLDCloudinary) -> Self { guard widgetPresented == false else { print("cloudinary can not be set while widget is presented") return self } self.cloudinaryObject = cloudinary return self } /** Holds the configuration parameters to be used by the `CLDUpladerWidget` instance. - parameter configuration: The configuration object. - returns: The same instance of CLDUploaderWidget. */ @objc(setConfigurationFromConfiguration:) @discardableResult public func setConfiguration(_ configuration: CLDWidgetConfiguration) -> Self { guard widgetPresented == false else { print("configuration can not be set while widget is presented") return self } self.configuration = configuration return self } /** Sets images to be presented, edited and uploaded. - parameter images: The images array object. - returns: The same instance of CLDUploaderWidget. */ @objc(setImagesFromImages:) @discardableResult public func setImages(_ images: [UIImage]) -> Self { guard widgetPresented == false else { print("images can not be set while widget is presented") return self } self.images = images return self } /** Sets videos to be presented and uploaded. - parameter videos: The videos array object. - returns: The same instance of CLDUploaderWidget. */ @objc(setVideosFromVideoItems:) @discardableResult public func setVideos(_ videoItems: [AVPlayerItem]) -> Self { guard widgetPresented == false else { print("videos can not be set while widget is presented") return self } self.videos = videoItems return self } /** Sets videos to be presented and uploaded. - parameter videos: The videos array object. - returns: The same instance of CLDUploaderWidget. */ @objc(setVideosFromVideoUrls:) @discardableResult public func setVideos(_ videoUrls: [URL]) -> Self { guard widgetPresented == false else { print("videos can not be set while widget is presented") return self } let videoItems = videoUrls.compactMap { AVPlayerItem(url: $0) } self.videos = videoItems return self } /** Sets a delegate object conforming to `CLDUploaderWidgetDelegate` protocol to recieve information via delegate methods. - parameter delegate: The delegate object. - returns: The same instance of CLDUploaderWidget. */ @objc(setDelegateFromDelegate:) @discardableResult public func setDelegate(_ delegate: CLDUploaderWidgetDelegate) -> Self { self.delegate = delegate return self } /** Modally presenting the widget or an `UIImagePickerController` from the given view controller. - parameter viewController: The presenting `UIViewController` object. */ public func presentWidget(from viewController: UIViewController) { let viewControllerToPresent: UIViewController if selectAssetFromLibrary { imagePicker = createImagePicker() viewControllerToPresent = imagePicker } else { widgetViewController = createWidgetViewController() viewControllerToPresent = widgetViewController } if #available(iOS 9.0, *) { viewControllerToPresent.loadViewIfNeeded() } else { _ = viewController.view } viewControllerToPresent.modalPresentationStyle = .fullScreen if #available(iOS 13.0, *) { viewControllerToPresent.isModalInPresentation = true } rootViewController = viewController rootViewController.present(viewControllerToPresent, animated: true, completion: nil) } /** Dismisses the widget. */ public func dismissWidget() { if selectAssetFromLibrary { imagePicker.dismiss(animated: true) { self.delegate?.uploadWidgetDidDismiss() } } else { widgetViewController.dismiss(animated: true) { self.delegate?.uploadWidgetDidDismiss() } } } // MARK - private methods private func createWidgetViewController() -> CLDWidgetViewController { let imageContainers = images.compactMap { CLDWidgetAssetContainer(originalImage: $0, editedImage: $0) } let videoContainers = videos.compactMap { CLDWidgetAssetContainer(videoItem: $0) } let assetContainers = videoContainers + imageContainers return CLDWidgetViewController(assets: assetContainers, configuration: configuration, delegate: self) } private func createImagePicker() -> UIImagePickerController { let picker = UIImagePickerController() picker.delegate = self picker.sourceType = .photoLibrary picker.mediaTypes = [kUTTypeImage as String, kUTTypeMovie as String] return picker } private func upload(assets: [CLDWidgetAssetContainer]) -> [CLDUploadRequest] { let images = assets.filter { $0.assetType == .image } let videos = assets.filter { $0.assetType == .video } let imagesUploadRequests = upload(images: images) let videosUploadRequests = upload(videos: videos) return videosUploadRequests + imagesUploadRequests } private func upload(images: [CLDWidgetAssetContainer]) -> [CLDUploadRequest] { guard images.count > 0 else { return [] } let datas = images.compactMap { $0.editedImage?.pngData() } let uploader = cloudinaryObject.createUploader() let requests = datas.compactMap { (data) -> CLDUploadRequest? in // unsigned upload must have a preset if let configuration = configuration, configuration.uploadType.signed == false, let preset = configuration.uploadType.preset { return uploader.upload(data: data, uploadPreset: preset).response { (result, error) in print("image unsigned result \(String(describing: result))") print("image unsigned error \(String(describing: error))") } } else { // default upload type return uploader.signedUpload(data: data).response { (result, error) in print("image signed result \(String(describing: result))") print("image signed error \(String(describing: error))") } } } return requests } private func upload(videos: [CLDWidgetAssetContainer]) -> [CLDUploadRequest] { guard videos.count > 0 else { return [] } let videosUrls = videos.compactMap { ($0.originalVideo?.asset as? AVURLAsset)?.url } let params = CLDUploadRequestParams() params.setResourceType(.video) let uploader = cloudinaryObject.createUploader() let requests = videosUrls.compactMap { (url) -> CLDUploadRequest? in // unsigned upload must have a preset if let configuration = configuration, configuration.uploadType.signed == false, let preset = configuration.uploadType.preset { return uploader.upload(url: url, uploadPreset: preset, params: params).response { (result, error) in print("video unsigned result \(String(describing: result))") print("video unsigned error \(String(describing: error))") } } else { // default upload type return uploader.signedUpload(url: url, params: params).response { (result, error) in print("video signed result \(String(describing: result))") print("video signed error \(String(describing: error))") } } } return requests } } // MARK: - UIImagePickerControllerDelegate extension CLDUploaderWidget: UIImagePickerControllerDelegate, UINavigationControllerDelegate { public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let image = info[.originalImage] as? UIImage { images = [image] } else if let videoUrl = info[.mediaURL] as? URL { let playerItem = AVPlayerItem(url: videoUrl) videos = [playerItem] } else { dismissWidget() return } widgetViewController = createWidgetViewController() picker.pushViewController(widgetViewController, animated: true) } public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { delegate?.widgetDidCancel(self) dismissWidget() } } // MARK: - CLDWidgetViewControllerDelegate extension CLDUploaderWidget: CLDWidgetViewControllerDelegate { func widgetViewController(_ controller: CLDWidgetViewController, didFinishEditing editedAssets: [CLDWidgetAssetContainer]) { let uploadRequests = upload(assets: editedAssets) delegate?.uploadWidget(self, willCall: uploadRequests) dismissWidget() } func widgetViewControllerDidCancel(_ controller: CLDWidgetViewController) { delegate?.widgetDidCancel(self) dismissWidget() } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/CropView/CLDCropOverlayView.swift ================================================ // // CLDCropOverlayView.swift // ProjectCLDWidgetEditViewController // // Created by Arkadi Yoskovitz on 8/17/20. // Copyright © 2020 Gini-Apps LTD. All rights reserved. // import UIKit private let kCLDCropOverLayerPlaceholder = CGFloat(00.0); private let kCLDCropOverLayerCornerWidth = CGFloat(20.0); @objc @objcMembers public class CLDCropOverlayView : UIView { // MARK: - public override var frame: CGRect { didSet { layoutLinesIfNeeded() } } public var gridColor : UIColor { didSet { layoutLinesIfNeeded() } } public var knobColor : UIColor { didSet { layoutLinesIfNeeded() } } // MARK: - Public Properties /** Hides the interior grid lines, sans animation. */ public var isGridHidden : Bool { set { setGridlines(hidden: newValue, animted: false) } get { areGridLinesHidden } } /** Add/Remove the interior horizontal grid lines. */ public var shouldDisplayHorizontalGridLines : Bool { set { setDisplayHorizontalGridLines(newValue) } get { displayGridLinesHorizontal } } /** Add/Remove the interior vertical grid lines. */ public var shouldDisplayVerticalGridLines : Bool { set { setDisplayVerticalGridLines(newValue) } get { displayGridLinesVertical } } // MARK: - Private Properties private var areGridLinesHidden : Bool private var displayGridLinesHorizontal: Bool private var displayGridLinesVertical : Bool private var gridLinesHorizontal : [UIView] private var gridLinesVertical : [UIView] private var outerLineViews : [UIView] // top, right, bottom, left private var topLeftLineViews : [UIView] // vertical, horizontal private var bottomLeftLineViews : [UIView] private var bottomRightLineViews : [UIView] private var topRightLineViews : [UIView] // MARK: - init public override init(frame: CGRect) { self.areGridLinesHidden = false self.displayGridLinesVertical = true self.displayGridLinesHorizontal = true self.gridColor = UIColor.white self.knobColor = UIColor.white self.gridLinesHorizontal = [UIView]() self.gridLinesVertical = [UIView]() self.outerLineViews = [UIView]() //top, right, bottom, left self.topLeftLineViews = [UIView]() self.topRightLineViews = [UIView]() self.bottomLeftLineViews = [UIView]() self.bottomRightLineViews = [UIView]() super.init(frame: frame) self.clipsToBounds = false self.outerLineViews.append(contentsOf: [ createLineView(background: gridColor), createLineView(background: gridColor), createLineView(background: gridColor), createLineView(background: gridColor) ]) self.topLeftLineViews.append(contentsOf: [ createLineView(background: gridColor), createLineView(background: gridColor) ]) self.topRightLineViews.append(contentsOf: [ createLineView(background: gridColor), createLineView(background: gridColor) ]) self.bottomLeftLineViews.append(contentsOf: [ createLineView(background: gridColor), createLineView(background: gridColor) ]) self.bottomRightLineViews.append(contentsOf: [ createLineView(background: gridColor), createLineView(background: gridColor) ]) self.setDisplayHorizontalGridLines(true) self.setDisplayVerticalGridLines(true) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - public override func didMoveToSuperview() { super.didMoveToSuperview() layoutLinesIfNeeded() } // MARK: - /** Shows and hides the interior grid lines with an optional crossfade animation. */ public func setGridlines(hidden: Bool, animted: Bool) { areGridLinesHidden = hidden; let block : () -> Void = { self.gridLinesHorizontal.forEach { $0.alpha = hidden ? 0.0 : 1.0 } self.gridLinesVertical.forEach { $0.alpha = hidden ? 0.0 : 1.0 } } switch animted { case true : UIView.animate(withDuration: hidden ? 0.35 : 0.2) { block() } case false: block() } } // MARK: - Private setter actions private func setDisplayHorizontalGridLines(_ display: Bool) { displayGridLinesHorizontal = display gridLinesHorizontal.forEach { $0.removeFromSuperview() } gridLinesHorizontal.removeAll() if displayGridLinesHorizontal { gridLinesHorizontal.append(contentsOf: [ createLineView(background: gridColor), createLineView(background: gridColor) ]) } setNeedsDisplay() } private func setDisplayVerticalGridLines(_ display: Bool) { displayGridLinesVertical = display gridLinesVertical.forEach { $0.removeFromSuperview() } gridLinesVertical.removeAll() if displayGridLinesVertical { gridLinesVertical.append(contentsOf: [ createLineView(background: gridColor), createLineView(background: gridColor) ]) } setNeedsDisplay() } // MARK: - Private methods private func layoutLinesIfNeeded() { guard !outerLineViews.isEmpty else { return } applyLayoutLines() } private func applyLayoutLines() { let boundsSize = bounds.size // Border lines outerLineViews.enumerated().forEach { (item) in let aRect : CGRect switch item.offset { case 0: aRect = CGRect(x: 0.0 , y: -1.0, width: boundsSize.width + 2.0, height: 1.0) // Top case 1: aRect = CGRect(x: boundsSize.width, y: 0.0, width: 1.0, height: boundsSize.height + 0.0) // Right case 2: aRect = CGRect(x: -1.0 , y: boundsSize.height, width: boundsSize.width + 2.0, height: 1.0) // Bottom case 3: aRect = CGRect(x: -1.0 , y: 0.0, width: 1.0, height: boundsSize.height + 1.0) // Left default: aRect = CGRect.zero } item.element.frame = aRect } // Corner liness [topLeftLineViews, topRightLineViews, bottomRightLineViews, bottomLeftLineViews].enumerated().forEach { item in var VFrame = CGRect.zero var HFrame = CGRect.zero switch item.offset { case 0: // Top left VFrame = CGRect(x: kCLDCropOverLayerPlaceholder - 3.0, y: kCLDCropOverLayerPlaceholder - 3.0, width: 3.0, height: kCLDCropOverLayerCornerWidth + 3.0) HFrame = CGRect(x: kCLDCropOverLayerPlaceholder + 0.0, y: kCLDCropOverLayerPlaceholder - 3.0, width: kCLDCropOverLayerCornerWidth + 0.0, height: 3.0) break case 1: // Top right VFrame = CGRect(x: boundsSize.width + kCLDCropOverLayerPlaceholder, y: kCLDCropOverLayerPlaceholder - 3.0, width: 3.0, height: kCLDCropOverLayerCornerWidth + 3.0) HFrame = CGRect(x: boundsSize.width - kCLDCropOverLayerCornerWidth, y: kCLDCropOverLayerPlaceholder - 3.0, width: kCLDCropOverLayerCornerWidth + 0.0, height: 3.0) break case 2: // Bottom right VFrame = CGRect(x: boundsSize.width + kCLDCropOverLayerPlaceholder, y: boundsSize.height - kCLDCropOverLayerCornerWidth, width: 3.0, height: kCLDCropOverLayerCornerWidth + 3.0) HFrame = CGRect(x: boundsSize.width - kCLDCropOverLayerCornerWidth, y: boundsSize.height + kCLDCropOverLayerPlaceholder, width: kCLDCropOverLayerCornerWidth + 0.0, height: 3.0) break case 3: // Bottom left VFrame = CGRect(x: kCLDCropOverLayerPlaceholder - 3.0, y: boundsSize.height - kCLDCropOverLayerCornerWidth, width: 3.0, height: kCLDCropOverLayerCornerWidth) HFrame = CGRect(x: kCLDCropOverLayerPlaceholder - 3.0, y: boundsSize.height + kCLDCropOverLayerPlaceholder, width: kCLDCropOverLayerCornerWidth + 3.0, height: 3.0) break default: break } item.element[0].frame = VFrame item.element[1].frame = HFrame } var padding : CGFloat var linesCount : Int let thickness = 1.0 / UIScreen.main.scale // Grid lines - horizontal linesCount = gridLinesHorizontal.count padding = (bounds.height - (thickness * CGFloat(linesCount))) / (CGFloat(linesCount) + 1.0) gridLinesHorizontal.enumerated().forEach { item in var aRect = CGRect.zero aRect.size.width = bounds.width aRect.size.height = thickness aRect.origin.y = (padding * CGFloat(item.offset + 1)) + (thickness * CGFloat(item.offset)) item.element.frame = aRect } // Grid lines - vertical linesCount = gridLinesVertical.count padding = (bounds.width - (thickness * CGFloat(linesCount))) / (CGFloat(linesCount) + 1.0) gridLinesVertical.enumerated().forEach { item in var aRect = CGRect.zero aRect.size.width = thickness aRect.size.height = bounds.height aRect.origin.x = (padding * CGFloat(item.offset + 1)) + (thickness * CGFloat(item.offset)) item.element.frame = aRect } } private func createLineView(background color: UIColor) -> UIView { let newLine = UIView(frame: .zero) newLine.backgroundColor = color addSubview(newLine) return newLine } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/CropView/CLDCropScrollView.swift ================================================ // // CLDCropScrollView.swift // ProjectCLDWidgetEditViewController // // Created by Arkadi Yoskovitz on 8/17/20. // Copyright © 2020 Gini-Apps LTD. All rights reserved. // import UIKit public typealias CLDTouchHandler = () -> Void /* Subclassing UIScrollView was necessary in order to directly capture touch events that weren't otherwise accessible via UIGestureRecognizer objects. */ @objc @objcMembers public class CLDCropScrollView : UIScrollView { public var touchesBegan :CLDTouchHandler? public var touchesCancelled:CLDTouchHandler? public var touchesEnded :CLDTouchHandler? override init(frame: CGRect) { super.init(frame: frame) configureInitialState() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) configureInitialState() } private func configureInitialState() { self.autoresizingMask = [.flexibleHeight , .flexibleWidth] self.alwaysBounceHorizontal = true self.alwaysBounceVertical = true self.showsHorizontalScrollIndicator = false self.showsVerticalScrollIndicator = false } public override func touchesBegan(_ touches: Set, with event: UIEvent?) { if let handler = touchesBegan { handler() } super.touchesBegan(touches, with: event) } public override func touchesEnded(_ touches: Set, with event: UIEvent?) { if let handler = touchesEnded { handler() } super.touchesEnded(touches, with: event) } public override func touchesCancelled(_ touches: Set, with event: UIEvent?) { if let handler = touchesEnded { handler() } super.touchesCancelled(touches, with: event) } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/CropView/CLDCropScrollViewController.swift ================================================ // // CLDCropViewScrollViewDelegate.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit class CLDCropScrollViewController: NSObject, UIScrollViewDelegate { weak var cropView: CLDCropView! init(cropView: CLDCropView) { self.cropView = cropView super.init() } func viewForZooming(in scrollView: UIScrollView) -> UIView? { return cropView.backgroundContainerView } func scrollViewDidScroll(_ scrollView: UIScrollView) { cropView.matchForegroundToBackground() } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { cropView.startEditing() cropView.isResettable = true } func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { cropView.startEditing() cropView.isResettable = true } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { cropView.startResetTimer() cropView.checkForCanReset() } func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { cropView.startResetTimer() cropView.checkForCanReset() } func scrollViewDidZoom(_ scrollView: UIScrollView) { if scrollView.isTracking { cropView.cropBoxLastEditedZoomScale = scrollView.zoomScale cropView.cropBoxLastEditedMinZoomScale = scrollView.minimumZoomScale } cropView.matchForegroundToBackground() } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { cropView.startResetTimer() } } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/CropView/CLDCropView.swift ================================================ // // CLDCropView.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit public extension CGFloat { static var ulpOfOne: CGFloat { CGFloat(Float.ulpOfOne) } } /** Preset values of the most common aspect ratios that can be used to quickly configure the crop view controller. */ @objc public enum CLDCropViewControllerAspectRatioPreset : Int { case original case square case rect3x2 case rect5x3 case rect4x3 case rect5x4 case rect7x5 case rect16x9 case custom } /** When the user taps down to resize the box, this state is used to determine where they tapped and how to manipulate the box */ @objc public enum CLDCropViewOverlayEdge : Int { case none case topLeft case top case topRight case right case bottomRight case bottom case bottomLeft case left } // ===================================================================================================================== @objc public protocol CLDCropViewDelegate : NSObjectProtocol { @objc optional func cropView(_ cropView: CLDCropView, didChangeResettable state: Bool) } // ===================================================================================================================== @objc @objcMembers public class CLDCropView: UIView { // MARK: - Constants internal struct Constants { internal static let padding : CGFloat = CGFloat(14.0) // kTOCropViewPadding internal static let timerDuration : TimeInterval = TimeInterval(0.8) // kTOCropTimerDuration internal static let minimumBoxSize : CGFloat = CGFloat(42.0) // kTOCropViewMinimumBoxSize internal static let circularPathRadius : CGFloat = CGFloat(300.0) // kTOCropViewCircularPathRadius internal static let maximumZoomScale : CGFloat = CGFloat(15.0) // kTOMaximumZoomScale } // MARK: - Types internal class Calculator { internal class CropBox {} internal class ImageCrop {} internal class AspectRatio {} } // MARK: - Properties /** A delegate object that receives notifications from the crop view */ public weak var delegate: CLDCropViewDelegate? /** If false, the user cannot resize the crop box frame using a pan gesture from a corner. Default value is true. */ public var cropBoxResizeEnabled: Bool { get { _cropBoxResizeEnabled } set { _cropBoxResizeEnabled = newValue gridPanGestureRecognizer.isEnabled = newValue } } /** Inset the workable region of the crop view in case in order to make space for accessory views */ public var cropRegionInsets: UIEdgeInsets /** When performing manual content layout (such as during screen rotation), disable any internal layout */ public var internalLayoutDisabled: Bool /** A width x height ratio that the crop box will be rescaled to (eg 4:3 is {4.0f, 3.0f}) Setting it to CGSizeZero will reset the aspect ratio to the image's own ratio. */ public var aspectRatio: CGSize { get { _aspectRatio } set { setAspectRatio(newValue, animated: false) } } /** When the cropping box is locked to its current aspect ratio (But can still be resized) */ public var aspectRatioLockEnabled: Bool /** If true, a custom aspect ratio is set, and the aspectRatioLockEnabled is set to YES, the crop box will swap it's dimensions depending on portrait or landscape sized images. This value also controls whether the dimensions can swap when the image is rotated. Default is true. */ public var aspectRatioLockDimensionSwapEnabled: Bool /** When the user taps 'reset', whether the aspect ratio will also be reset as well Default is true. */ public var resetAspectRatioEnabled: Bool /** The rotation angle of the crop view (Will always be negative as it rotates in a counter-clockwise direction) */ public var angle: Int { get { _angle } set { // The initial layout would not have been performed yet. // Save the value and it will be applied when it has var newAngle = newValue if (newValue % 90 != 0) { newAngle = 0 } if !initialSetupPerformed { restoreAngle = newAngle return } // Negative values are allowed, so rotate clockwise or counter clockwise depending on direction if (newAngle >= 0) { while ( labs(angle) != labs(newAngle)) { rotateImageNinetyDegreesAnimated(false, clockwise: true ) } } else { while (-labs(angle) != -labs(newAngle)) { rotateImageNinetyDegreesAnimated(false, clockwise: false) } } } } /** In relation to the coordinate space of the image, the frame that the crop view is focusing on */ public var imageCropFrame: CGRect { get { return Calculator.ImageCrop.computeFrame(given: imageSize, scrollView: scrollView, cropBox: cropBoxFrame) } set { guard initialSetupPerformed else { restoreImageCropFrame = newValue; return } let adjutsments = Calculator.ImageCrop.computeAdjutsments(for: self, scrollView: scrollView, content: contentBounds, newValue: newValue) cropBoxFrame = adjutsments.cropFrame applyCropFrameAdjutsments(to: scrollView, originPoint: newValue.origin, minimumScale: adjutsments.minimumZoomScale, adjustedZoom: adjutsments.adjustedZoomScale) } } /** Paddings of the crop rectangle. Default to 14.0 */ public var cropViewPadding: CGFloat /** Delay before crop frame is adjusted according new crop area. Default to 0.8 */ public var cropAdjustingDelay: TimeInterval /** The minimum croping aspect ratio. If set, user is prevented from setting cropping rectangle to lower aspect ratio than defined by the parameter. */ public var minimumAspectRatio: CGFloat /** The maximum scale that user can apply to image by pinching to zoom. Small values are only recomended with aspectRatioLockEnabled set to true. Default to 15.0 */ public var maximumZoomScale: CGFloat /** Always show the cropping grid lines, even when the user isn't interacting. This also disables the fading animation. (Default is NO) */ public var alwaysShowCroppingGrid: Bool { get { _alwaysShowCroppingGrid } set { if _alwaysShowCroppingGrid == newValue { return } _alwaysShowCroppingGrid = newValue gridOverlayView.setGridlines(hidden: !newValue, animted: true) } } /** Permanently hides the translucency effect covering the outside bounds of the crop box. Default is NO */ public var translucencyAlwaysHidden: Bool { get { _translucencyAlwaysHidden } set { guard _translucencyAlwaysHidden != newValue else { return } _translucencyAlwaysHidden = newValue translucencyView.isHidden = newValue } } /** The image that the crop view is displaying. This cannot be changed once the crop view is instantiated. */ public internal(set) var image : UIImage /* Views */ /** The main image view, placed within the scroll view */ internal var backgroundImageView: UIImageView! /** A view which contains the background image view, to separate its transforms from the scroll view. */ internal var backgroundContainerView: UIView! /** A container view that clips the a copy of the image so it appears over the dimming view */ public private(set) var foregroundContainerView: UIView! /** A copy of the background image view, placed over the dimming views */ internal var foregroundImageView: UIImageView! /** The scroll view in charge of panning/zooming the image. */ internal private(set) var scrollView: CLDCropScrollView! /** The scroll view in charge of panning/zooming the image. */ private var scrollViewDelegate: CLDCropScrollViewController! /** A semi-transparent grey view, overlaid on top of the background image */ private var overlayView: UIView! /** A blur view that is made visible when the user isn't interacting with the crop view */ internal var translucencyView: UIVisualEffectView! /** The dark blur visual effect applied to the visual effect view. */ private var translucencyEffect: UIBlurEffect! /** A grid view overlaid on top of the foreground image view's container. */ public private(set) var gridOverlayView: CLDCropOverlayView! /* Gesture Recognizers */ /** The gesture recognizer in charge of controlling the resizing of the crop view */ private var gridPanGestureRecognizer: UIPanGestureRecognizer! /* Crop box handling */ /** No by default, when setting initialCroppedImageFrame this will be set to YES, and set back to NO after first application - so it's only done once */ private var applyInitialCroppedImageFrame: Bool /** The edge region that the user tapped on, to resize the cropping region */ internal var tappedEdge: CLDCropViewOverlayEdge /** When resizing, this is the original frame of the crop box. */ internal var cropOriginFrame : CGRect /** The initial touch point of the pan gesture recognizer */ internal var panOriginPoint : CGPoint /** The frame of the cropping box in the coordinate space of the crop view */ public internal(set) var cropBoxFrame: CGRect { get { _cropBoxFrame } set { if _cropBoxFrame.equalTo(newValue) { return } guard newValue.isValidBoxSize else { return } // Clamp the cropping region to the inset boundaries of the screen _cropBoxFrame = Calculator.CropBox.clampFrame(given: contentBounds, cropBox: newValue, minimumBoxSize: Constants.minimumBoxSize) applyDidSetCropBoxFrame(to: _cropBoxFrame) } } /** A manager for the crop view UI */ fileprivate var cropUIManager: CLDCropViewUIManager! /** The timer used to reset the view after the user stops interacting with it */ internal var resetTimer: Timer? /** Used to denote the active state of the user manipulating the content default is NO. setting is not animated. */ private var isEditing: Bool { get { _isEditing } set { setEditing(newValue, resetCropBox: false, animated: false) } } internal func startEditing() { cancelResetTimer() setEditing(true, resetCropBox: false, animated: true) } internal func setEditing(_ editing: Bool, resetCropBox: Bool, animated: Bool) { if _isEditing == editing { return } _isEditing = editing // Toggle the visiblity of the gridlines when not editing var hidden = !editing if (alwaysShowCroppingGrid) { hidden = false } // Override this if the user requires gridOverlayView.setGridlines(hidden: hidden, animted: animated) if resetCropBox { moveCroppedContentToCenterAnimated(animated) captureStateForImageRotation() cropBoxLastEditedAngle = angle } switch animated { case false: toggleTranslucencyViewVisible(!editing) case true: let duration = editing ? 0.05 : 0.35 let delay = editing ? 0.00 : 0.35 UIView.animateKeyframes(withDuration: duration, delay: delay, options: [], animations: { self.toggleTranslucencyViewVisible(!editing) }, completion: nil) } } /** At times during animation, disable matching the forground image view to the background */ internal var disableForgroundMatching: Bool /* Pre-screen-rotation state information */ private var rotationContentOffset: CGPoint private var rotationContentSize : CGSize private var rotationBoundFrame : CGRect /* View State information */ /** Give the current screen real-estate, the frame that the scroll view is allowed to use */ internal var contentBounds: CGRect { return Calculator.bounds(for: self.bounds, padding: cropViewPadding, cropRegion: cropRegionInsets) } /** Given the current rotation of the image, the size of the image */ internal var imageSize: CGSize { if (angle == -90 || angle == -270 || angle == 90 || angle == 270 ) { return CGSize(width: image.size.height, height: image.size.width ) } else { return CGSize(width: image.size.width , height: image.size.height) } } /** True if an aspect ratio was explicitly applied to this crop view */ internal var hasAspectRatio: Bool { return aspectRatio.width > CGFloat.ulpOfOne && aspectRatio.height > CGFloat.ulpOfOne } /* 90-degree rotation state data */ /** When performing 90-degree rotations, remember what our last manual size was to use that as a base */ internal var cropBoxLastEditedSize: CGSize /** Remember which angle we were at when we saved the editing size */ internal var cropBoxLastEditedAngle: Int /** Remember the zoom size when we last edited */ internal var cropBoxLastEditedZoomScale: CGFloat /** Remember the minimum size when we last edited. */ internal var cropBoxLastEditedMinZoomScale: CGFloat /** Disallow any input while the rotation animation is playing */ internal var rotateAnimationInProgress: Bool /* Reset state data */ /** Save the original crop box size so we can tell when the content has been edited */ private var originalCropBoxSize: CGSize /** Save the original content offset so we can tell if it's been scrolled. */ private var originalContentOffset: CGPoint /** Whether the user has manipulated the crop view to the point where it can be reset */ public internal(set) var isResettable: Bool { get{ _isResettableFlag } set{ guard newValue != _isResettableFlag else { return } _isResettableFlag = newValue delegate?.cropView?(self, didChangeResettable: _isResettableFlag) } } /** In iOS 9, a new dynamic blur effect became available. */ public internal(set) var dynamicBlurEffect: Bool // MARK: - ====================================================================================== /** If restoring to a previous crop setting, these properties hang onto the values until the view is configured for the first time. */ private var restoreAngle : Int private var restoreImageCropFrame: CGRect /** Set to YES once `performInitialLayout` is called. This lets pending properties get queued until the view has been properly set up in its parent. */ private var initialSetupPerformed: Bool // MARK: - Private properties ====================================================================================== private var _cropBoxFrame : CGRect private var _cropBoxResizeEnabled : Bool private var _simpleRenderMode : Bool private var _imageCropFrame : CGRect private var _isEditing : Bool private var _isResettableFlag : Bool private var _aspectRatio : CGSize private var _croppingViewsHidden : Bool private var _gridOverlayHidden : Bool private var _alwaysShowCroppingGrid : Bool private var _translucencyAlwaysHidden: Bool var _angle : Int // MARK: - Initialized ====================================================================================== /** Create a new instance of the crop view with the specified image and cropping */ public init(image: UIImage) { self.image = image self.aspectRatioLockDimensionSwapEnabled = true self.resetAspectRatioEnabled = true self.cropViewPadding = Constants.padding self.cropAdjustingDelay = Constants.timerDuration self.cropBoxLastEditedSize = CGSize.zero self.cropBoxLastEditedAngle = 0 self.cropBoxLastEditedZoomScale = 0.0 self.cropBoxLastEditedMinZoomScale = 0.0 self.rotateAnimationInProgress = false self.cropRegionInsets = .zero self.minimumAspectRatio = CGFloat(0.0) self.internalLayoutDisabled = false self.aspectRatioLockEnabled = false self.tappedEdge = .none self.cropOriginFrame = CGRect.zero self.panOriginPoint = CGPoint.zero self._angle = 0 self._isEditing = false self._cropBoxFrame = .zero self._aspectRatio = .zero self._imageCropFrame = .zero self._isResettableFlag = false self._simpleRenderMode = false self._cropBoxResizeEnabled = false self._alwaysShowCroppingGrid = false self._translucencyAlwaysHidden = false self._gridOverlayHidden = false self._croppingViewsHidden = false self.disableForgroundMatching = false self.rotationContentOffset = .zero self.rotationContentSize = .zero self.rotationBoundFrame = .zero self.originalCropBoxSize = .zero self.originalContentOffset = .zero self.initialSetupPerformed = false self.maximumZoomScale = Constants.maximumZoomScale self.applyInitialCroppedImageFrame = false self.dynamicBlurEffect = false self.restoreAngle = 0 self.restoreImageCropFrame = .zero super.init(frame: .zero) // UI manager self.cropUIManager = CLDCropViewUIManager(cropView: self) // The pan controller to recognize gestures meant to resize the grid view self.gridPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(gridPanGestureRecognized(_:))) self.cropBoxResizeEnabled = true self.alwaysShowCroppingGrid = false self.translucencyAlwaysHidden = false // View properties self.autoresizingMask = [.flexibleHeight , .flexibleWidth] self.backgroundColor = UIColor(white: 0.12, alpha: 1.0) self.cropBoxFrame = CGRect.zero self._isEditing = false self._cropBoxResizeEnabled = true self._aspectRatio = CGSize.zero self.resetAspectRatioEnabled = true self.cropAdjustingDelay = Constants.timerDuration self.cropViewPadding = Constants.padding /* Dynamic animation blurring is only possible on iOS 9, however since the API was available on iOS 8, we'll need to manually check the system version to ensure that it's available. */ self.dynamicBlurEffect = UIDevice.current.systemVersion.compare("9.0", options: .numeric) != .orderedAscending // Scroll View self.scrollViewDelegate = CLDCropScrollViewController(cropView: self) self.scrollView = CLDCropScrollView(frame: bounds) self.scrollView.delegate = self.scrollViewDelegate self.addSubview(self.scrollView) // Disable smart inset behavior in iOS 11 if #available(iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never } self.scrollView.touchesBegan = { [weak self] in self?.startEditing() } self.scrollView.touchesEnded = { [weak self] in self?.startResetTimer() } // Background Image View self.backgroundImageView = UIImageView(image: self.image) self.backgroundImageView.layer.minificationFilter = CALayerContentsFilter.trilinear // Background container view self.backgroundContainerView = UIView(frame: self.backgroundImageView.frame) self.backgroundContainerView.addSubview(self.backgroundImageView) self.scrollView.addSubview(self.backgroundContainerView) // Grey transparent overlay view self.overlayView = UIView(frame: bounds) self.overlayView.autoresizingMask = [.flexibleHeight , .flexibleWidth] self.overlayView.backgroundColor = self.backgroundColor?.withAlphaComponent(0.35) self.overlayView.isHidden = false self.overlayView.isUserInteractionEnabled = false self.addSubview(self.overlayView) // Translucency View self.translucencyEffect = UIBlurEffect(style: UIBlurEffect.Style.dark) self.translucencyView = UIVisualEffectView(effect: self.translucencyEffect) self.translucencyView.frame = self.bounds self.translucencyView.isHidden = self.translucencyAlwaysHidden self.translucencyView.isUserInteractionEnabled = false self.translucencyView.autoresizingMask = [.flexibleHeight , .flexibleWidth] self.addSubview(self.translucencyView) // The forground container that holds the foreground image view self.foregroundContainerView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200)) self.foregroundContainerView.clipsToBounds = true self.foregroundContainerView.isUserInteractionEnabled = false self.addSubview(self.foregroundContainerView) self.foregroundImageView = UIImageView(image: self.image) self.foregroundImageView.layer.minificationFilter = CALayerContentsFilter.trilinear self.foregroundContainerView.addSubview(self.foregroundImageView) // Disable colour inversion for the image views if #available(iOS 11.0, *) { self.foregroundImageView.accessibilityIgnoresInvertColors = true self.backgroundImageView.accessibilityIgnoresInvertColors = true } // The white grid overlay view self.gridOverlayView = CLDCropOverlayView(frame: self.foregroundContainerView.frame) self.gridOverlayView.isUserInteractionEnabled = false self.gridOverlayView.isGridHidden = true self.addSubview(self.gridOverlayView) self.gridPanGestureRecognizer.delegate = self self.scrollView.panGestureRecognizer.require(toFail: self.gridPanGestureRecognizer) self.addGestureRecognizer(self.gridPanGestureRecognizer) } public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - View Layout /** Performs the initial set up, including laying out the image and applying any restore properties. This should be called once the crop view has been added to a parent that is in its final layout frame. */ public func performInitialSetup() { // Calling this more than once is potentially destructive if initialSetupPerformed { return } // Disable from calling again initialSetupPerformed = true // Perform the initial layout of the image layoutInitialImage() // -- State Restoration -- // If the angle value was previously set before this point, apply it now if (restoreAngle != 0) { angle = restoreAngle restoreAngle = 0 cropBoxLastEditedAngle = angle } // If an image crop frame was also specified before creation, apply it now if !restoreImageCropFrame.isEmpty { imageCropFrame = restoreImageCropFrame restoreImageCropFrame = .zero } // Save the current layout state for later captureStateForImageRotation() // Check if we performed any resetabble modifications checkForCanReset() } private func layoutInitialImage() { let scaledImageSize = cropUIManager.manualLayout_initialImageAndGetScaledImageSize() // Save the current state for use with 90-degree rotations cropBoxLastEditedAngle = 0 captureStateForImageRotation() //save the size for checking if we're in a resettable state originalCropBoxSize = resetAspectRatioEnabled ? scaledImageSize : cropBoxFrame.size originalContentOffset = scrollView.contentOffset checkForCanReset() matchForegroundToBackground() } internal func matchForegroundToBackground() { if disableForgroundMatching { return } // We can't simply match the frames since if the images are rotated, the frame property becomes unusable if let view = backgroundContainerView.superview { foregroundImageView.frame = view.convert(backgroundContainerView.frame, to: foregroundContainerView) } } private func updateCropBoxFrame(withGesture point: CGPoint) { cropBoxFrame = Calculator.CropBox.calculateFrame(given: point, in: self) checkForCanReset() } private func toggleTranslucencyViewVisible(_ visible: Bool) { if dynamicBlurEffect == false { translucencyView.alpha = visible ? 1.0 : 0.0 } else { translucencyView.effect = visible ? translucencyEffect : nil } } // MARK: - Gesture Recognizer - ==================================================================================== @objc private func gridPanGestureRecognized(_ recognizer: UIPanGestureRecognizer) { let point = recognizer.location(in: self) switch recognizer.state { case .began: startEditing() panOriginPoint = point cropOriginFrame = cropBoxFrame tappedEdge = Calculator.overlayEdge(for: panOriginPoint, cropBoxFrame: cropBoxFrame) break case .ended: startResetTimer() break default: break } updateCropBoxFrame(withGesture: point) } // MARK: - Timer ==================================================================================== internal func startResetTimer() { if let _ = resetTimer {} else { resetTimer = Timer.scheduledTimer(timeInterval: cropAdjustingDelay, target: self, selector: #selector(timerTriggered), userInfo: nil, repeats: false) } } @objc private func timerTriggered() { setEditing(false, resetCropBox: true, animated: true) resetTimer?.invalidate() resetTimer = nil } internal func cancelResetTimer() { resetTimer?.invalidate() resetTimer = nil } // MARK: - ========================================================================================================= /** Changes the aspect ratio of the crop box to match the one specified - parameter aspectRatio The aspect ratio (For example 16:9 is 16.0f/9.0f). 'CGSizeZero' will reset it to the image's own ratio - parameter animated Whether the locking effect is animated */ public func setAspectRatio(_ newValue: CGSize, animated: Bool) { _aspectRatio = newValue let adjustments = Calculator.AspectRatio.postChangeAdjustments(given: newValue, animated: animated, in: self) cropBoxLastEditedSize = adjustments.cropBoxFrame.size cropBoxLastEditedAngle = angle let translateBlock : () -> Void = { self.scrollView.contentOffset = adjustments.scrollOffset self.cropBoxFrame = adjustments.cropBoxFrame if (adjustments.ShouldZoomOut) { self.scrollView.zoomScale = self.scrollView.minimumZoomScale } self.moveCroppedContentToCenterAnimated(false) self.checkForCanReset() } switch animated { case false: translateBlock() case true : UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.7, options: [.beginFromCurrentState], animations: translateBlock, completion: nil) } } public func lockAspectRatio(to aspectRatio: CGSize) { if aspectRatioLockEnabled { _aspectRatio = aspectRatio } } /** Rotates the entire canvas to a 90-degree angle. The default rotation is counterclockwise. - parameter animated Whether the transition is animated */ public func rotateImageNinetyDegreesAnimated(_ animated: Bool) { rotateImageNinetyDegreesAnimated(animated, clockwise: false) } /** Rotates the entire canvas to a 90-degree angle - parameter animated Whether the transition is animated - parameter clockwise Whether the rotation is clockwise. Passing 'NO' means counterclockwise */ public func rotateImageNinetyDegreesAnimated(_ animated: Bool, clockwise: Bool) { cropUIManager.manualLayout_rotateImageNinetyDegreesAnimated(animated, clockwise: clockwise) } /** When triggered, the crop view will perform a relayout to ensure the crop box fills the entire crop view region */ public func moveCroppedContentToCenterAnimated(_ animated: Bool) { cropUIManager.manualLayout_moveCroppedContentToCenterAnimated(animated) } // MARK: -- Editing Mode internal func captureStateForImageRotation() { cropBoxLastEditedSize = cropBoxFrame.size cropBoxLastEditedZoomScale = scrollView.zoomScale cropBoxLastEditedMinZoomScale = scrollView.minimumZoomScale } // MARK: -- Resettable State internal func checkForCanReset() { var resettable = false if angle != 0 { // Image has been rotated resettable = true } else if scrollView.zoomScale > scrollView.minimumZoomScale + CGFloat.ulpOfOne { // Image has been zoomed in resettable = true } else if Int(floor(cropBoxFrame.width )) != Int(floor(originalCropBoxSize.width )) || Int(floor(cropBoxFrame.height)) != Int(floor(originalCropBoxSize.height)) // Crop box has been changed { resettable = true } else if Int(floor(scrollView.contentOffset.x)) != Int(floor(originalContentOffset.x)) || Int(floor(scrollView.contentOffset.y)) != Int(floor(originalContentOffset.y)) { resettable = true } isResettable = resettable } // MARK: -- fileprivate methods fileprivate func applyCropFrameAdjutsments(to scrollView: UIScrollView, originPoint: CGPoint, minimumScale: CGFloat, adjustedZoom scale: CGFloat) { let scaledOffset = CGPoint(x: originPoint.x * minimumScale, y: originPoint.y * minimumScale) // Zoom into the scroll view to the appropriate size scrollView.zoomScale = scrollView.minimumZoomScale * scale scrollView.contentOffset = CGPoint(x: (scaledOffset.x * scale) - scrollView.contentInset.left, y: (scaledOffset.y * scale) - scrollView.contentInset.top) } fileprivate func applyDidSetCropBoxFrame(to rectangle: CGRect) { // Set the clipping view to match the new rect foregroundContainerView.frame = rectangle // Set the new overlay view to match the same region gridOverlayView.frame = rectangle Calculator.CropBox.updateScrollView(scrollView, given: backgroundContainerView.bounds.size, boundingRect: self.bounds, newCropBox: rectangle) // Re-align the background content to match matchForegroundToBackground() } } // MARK: - UIGestureRecognizerDelegate extension CLDCropView : UIGestureRecognizerDelegate { public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer != gridPanGestureRecognizer { return true } let tapPoint = gestureRecognizer.location(in: self) let rectangle = gridOverlayView.frame let innerRectangle = rectangle.insetBy(dx: 22.0, dy: 22.0) let outerRectangle = rectangle.insetBy(dx: -22.0, dy: -22.0) if innerRectangle.contains(tapPoint) || !outerRectangle.contains(tapPoint) { return false } return true } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { if gridPanGestureRecognizer.state == .changed { return false } return true } } // MARK: - General extensions fileprivate extension CGRect { var isValidBoxSize : Bool { // Upon init, sometimes the box size is still 0 (or NaN), which can result in CALayer issues let threashold = CGFloat.ulpOfOne if (width < threashold || height < threashold) { return false } if (width.isNaN || height.isNaN ) { return false } return true } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/CropView/CLDCropViewCalculator.swift ================================================ // // CLDCropViewCalculator.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit import Foundation import CoreGraphics fileprivate extension CGSize { var floored : CGSize { return CGSize(width : floor(width ), height: floor(height)) } } internal extension CLDCropView.Calculator { static func bounds(for viewBounds: CGRect, padding: CGFloat, cropRegion insets: UIEdgeInsets) -> CGRect { var rectangle = CGRect.zero rectangle.origin.x = padding + insets.left rectangle.origin.y = padding + insets.top rectangle.size.width = viewBounds.width - ((padding * 2) + insets.left + insets.right) rectangle.size.height = viewBounds.height - ((padding * 2) + insets.top + insets.bottom) return rectangle } static func overlayEdge(for point: CGPoint, cropBoxFrame: CGRect) -> CLDCropViewOverlayEdge { let threashold = CGFloat(64) let halfThreashold = threashold / 2.0 var rectangle = cropBoxFrame // Account for padding around the box rectangle = rectangle.insetBy(dx: -halfThreashold, dy: -halfThreashold) // Make sure the corners take priority let rectangleTopLeft = CGRect(origin: rectangle.origin, size: CGSize(width: threashold, height: threashold)) if rectangleTopLeft.contains(point) { return CLDCropViewOverlayEdge.topLeft } var rectangleTopRight = rectangleTopLeft rectangleTopRight.origin.x = rectangle.maxX - threashold if rectangleTopRight.contains(point) { return CLDCropViewOverlayEdge.topRight } var rectangleBottomLeft = rectangleTopLeft rectangleBottomLeft.origin.y = rectangle.maxY - threashold if rectangleBottomLeft.contains(point) { return CLDCropViewOverlayEdge.bottomLeft } var rectangleBottomRight = rectangleTopRight rectangleBottomRight.origin.y = rectangleBottomLeft.origin.y if rectangleBottomRight.contains(point) { return CLDCropViewOverlayEdge.bottomRight } // Check for edges let topRect = CGRect(origin: rectangle.origin, size: CGSize(width: rectangle.width, height: threashold)) if topRect.contains(point) { return CLDCropViewOverlayEdge.top } var bottomRect = topRect bottomRect.origin.y = rectangle.maxY - threashold if bottomRect.contains(point) { return CLDCropViewOverlayEdge.bottom } let leftRect = CGRect(origin: rectangle.origin, size: CGSize(width: threashold, height: rectangle.height)) if leftRect.contains(point) { return CLDCropViewOverlayEdge.left } var rightRect = leftRect rightRect.origin.x = rectangle.maxX - threashold if rightRect.contains(point) { return CLDCropViewOverlayEdge.right } return CLDCropViewOverlayEdge.none } } internal extension CLDCropView.Calculator.ImageCrop { /** In relation to the coordinate space of the image, the frame that the crop view is focusing on */ static func computeFrame(given imageSize: CGSize, scrollView: UIScrollView, cropBox: CGRect) -> CGRect { let targetSize = imageSize let contentSize = scrollView.contentSize let cropRectangle = cropBox let offset = scrollView.contentOffset let insets = scrollView.contentInset let scale = min(targetSize.width / contentSize.width, targetSize.height / contentSize.height) var rectangle = CGRect.zero // Calculate the normalized origin rectangle.origin.x = floor( (floor(offset.x) + insets.left) * (targetSize.width / contentSize.width ) ) rectangle.origin.x = max(0, rectangle.origin.x) rectangle.origin.y = floor( (floor(offset.y) + insets.top ) * (targetSize.height / contentSize.height) ) rectangle.origin.y = max(0, rectangle.origin.y) // Calculate the normalized width rectangle.size.width = ceil(cropRectangle.width * scale) rectangle.size.width = min(targetSize.width, rectangle.width) // Calculate normalized height if floor(cropRectangle.width) == floor(cropRectangle.height) { rectangle.size.height = rectangle.size.width } else { rectangle.size.height = ceil(cropRectangle.height * scale) rectangle.size.height = min(targetSize.height, rectangle.size.height) } rectangle.size.height = min(targetSize.height, rectangle.size.height) return rectangle } static func computeAdjutsments(for cropView: CLDCropView, scrollView: UIScrollView, content contentBounds: CGRect, newValue: CGRect) -> (cropFrame:CGRect, minimumZoomScale: CGFloat, adjustedZoomScale: CGFloat) { // Convert the image crop frame's size from image space to the screen space let minimumScale = scrollView.minimumZoomScale let scaledCropSize = CGSize(width : newValue.width * minimumScale, height: newValue.height * minimumScale) // Work out the scale necessary to upscale the crop size to fit the content bounds of the crop bound let boundsRectangle = contentBounds let scale = min(boundsRectangle.width / scaledCropSize.width, boundsRectangle.height / scaledCropSize.height) // Work out the size and offset of the upscaled crop box var frameRectangle = CGRect.zero frameRectangle.size = CGSize(width : scaledCropSize.width * scale, height: scaledCropSize.height * scale) //set the crop box var cropBoxRectangle = CGRect.zero cropBoxRectangle.size = frameRectangle.size cropBoxRectangle.origin.x = boundsRectangle.midX - (frameRectangle.width * 0.5) cropBoxRectangle.origin.y = boundsRectangle.midY - (frameRectangle.height * 0.5) return (cropFrame: cropBoxRectangle, minimumZoomScale: minimumScale, adjustedZoomScale: scale) } } internal extension CLDCropView.Calculator.CropBox { /** Clamp the cropping region to the inset boundaries of the screen */ static func clampFrame(given rectangle: CGRect, cropBox: CGRect, minimumBoxSize: CGFloat) -> CGRect { func clampValueToRange(input value: CGFloat, maximumValue: CGFloat, minimumValue: CGFloat) -> CGFloat { var temp = value temp = floor( min(temp, maximumValue) ) temp = ceil ( max(temp, minimumValue) ) return temp } func clampedValues(contentOrigin: CGFloat, clampedOrigin: CGFloat, side: CGFloat) -> (origin:CGFloat, side: CGFloat) { var internalVariable = side let internalClampedOrigin = floor( max(clampedOrigin, ceil(contentOrigin)) ) // If we clamp the value, ensure we compensate for the subsequent delta // generated in the width (Or else, the box will keep growing) let delta = clampedOrigin - ceil(contentOrigin) if (delta < -CGFloat.ulpOfOne) { internalVariable += delta } return (internalClampedOrigin,internalVariable) } let xValues = clampedValues(contentOrigin: rectangle.origin.x, clampedOrigin: cropBox.origin.x, side: cropBox.width) let yValues = clampedValues(contentOrigin: rectangle.origin.y, clampedOrigin: cropBox.origin.y, side: cropBox.height) // Given the clamped X values, make sure we can't extend the crop box beyond the edge of the screen in the current state // Make sure we can't make the crop box too small let maxWidth = (rectangle.origin.x + rectangle.width) - xValues.origin let clampedWidth = clampValueToRange(input: xValues.side, maximumValue: maxWidth, minimumValue: minimumBoxSize) // Given the clamped Y values, make sure we can't extend the crop box beyond the edge of the screen in the current state // Make sure we can't make the crop box too small let maxHeight = (rectangle.origin.y + rectangle.height) - yValues.origin let clampedHeight = clampValueToRange(input: yValues.side, maximumValue: maxHeight, minimumValue: minimumBoxSize) return CGRect(x: xValues.origin, y: yValues.origin, width: clampedWidth, height: clampedHeight) } static func updateScrollView(_ scrollView: UIScrollView, given imageSize : CGSize, boundingRect: CGRect, newCropBox cropBox: CGRect) { // if necessary, work out the new minimum size of the scroll view so it fills the crop box let scale = max(cropBox.height / imageSize.height, cropBox.width / imageSize.width) // Reset the scroll view insets to match the region of the new crop rect scrollView.contentInset = UIEdgeInsets(top : cropBox.minY, left: cropBox.minX, bottom: boundingRect.maxY - cropBox.maxY, right : boundingRect.maxX - cropBox.maxX) scrollView.minimumZoomScale = scale // Make sure content isn't smaller than the crop box scrollView.contentSize = scrollView.contentSize.floored // IMPORTANT: Force the scroll view to update its content after changing the zoom scale let zoomScale = scrollView.zoomScale scrollView.zoomScale = zoomScale } /** Calculates the new crop box frame by the users gesture */ static func calculateFrame(given gesturePoint: CGPoint, in cropView: CLDCropView) -> CGRect { var rectangle = cropView.cropBoxFrame let originFrame = cropView.cropOriginFrame let contentFrame = cropView.contentBounds var point = gesturePoint point.x = max(contentFrame.origin.x - cropView.cropViewPadding, point.x) point.y = max(contentFrame.origin.y - cropView.cropViewPadding, point.y) // The delta between where we first tapped, and where our finger is now var xDelta = ceil(point.x - cropView.panOriginPoint.x) var yDelta = ceil(point.y - cropView.panOriginPoint.y) // Current aspect ratio of the crop box in case we need to clamp it let aspectRatio = (originFrame.width / originFrame.height) // Note whether we're being aspect transformed horizontally or vertically var aspectHorizontal = false var aspectVertical = false // Depending on which corner we drag from, set the appropriate min flag to // ensure we can properly clamp the XY value of the box if it overruns the minimum size // (Otherwise the image itself will slide with the drag gesture) var clampMinFromTop = false var clampMinFromLeft = false switch cropView.tappedEdge { case .left: if cropView.aspectRatioLockEnabled { aspectHorizontal = true xDelta = max(xDelta, 0) let scaleOrigin = CGPoint(x: originFrame.maxX, y: originFrame.midY) rectangle.size.height = rectangle.width / aspectRatio rectangle.origin.y = scaleOrigin.y - (rectangle.height * 0.5) } let newWidth = originFrame.width - xDelta let newHeight = originFrame.height if min(newHeight, newWidth) / max(newHeight, newWidth) >= cropView.minimumAspectRatio { rectangle.origin.x = originFrame.origin.x + xDelta rectangle.size.width = originFrame.width - xDelta } clampMinFromLeft = true case .right: if cropView.aspectRatioLockEnabled { aspectHorizontal = true let scaleOrigin = CGPoint(x: originFrame.minX, y: originFrame.midY) rectangle.size.height = rectangle.width / aspectRatio rectangle.origin.y = scaleOrigin.y - (rectangle.height * 0.5) rectangle.size.width = originFrame.size.width + xDelta rectangle.size.width = min(rectangle.width, contentFrame.height * aspectRatio) } else { let newWidth = originFrame.width + xDelta let newHeight = originFrame.height if min(newHeight, newWidth) / max(newHeight, newWidth) >= cropView.minimumAspectRatio { rectangle.size.width = originFrame.width + xDelta } } case .bottom: if cropView.aspectRatioLockEnabled { aspectVertical = true let scaleOrigin = CGPoint(x: originFrame.midX, y: originFrame.minY) rectangle.size.width = rectangle.height * aspectRatio rectangle.origin.x = scaleOrigin.x - (rectangle.size.width * 0.5) rectangle.size.height = originFrame.height + yDelta rectangle.size.height = min(rectangle.height, contentFrame.width / aspectRatio) } else { let newWidth = originFrame.width let newHeight = originFrame.height + yDelta if min(newHeight, newWidth) / max(newHeight, newWidth) >= cropView.minimumAspectRatio { rectangle.size.height = originFrame.height + yDelta } } case .top: if cropView.aspectRatioLockEnabled { aspectVertical = true yDelta = max(0,yDelta) let scaleOrigin = CGPoint(x: originFrame.midX, y: originFrame.midY) rectangle.origin.x = scaleOrigin.x - (rectangle.size.width * 0.5) rectangle.origin.y = originFrame.origin.y + yDelta rectangle.size.width = rectangle.height * aspectRatio rectangle.size.height = originFrame.height - yDelta } else { let newWidth = originFrame.width let newHeight = originFrame.height - yDelta if min(newHeight, newWidth) / max(newHeight, newWidth) >= cropView.minimumAspectRatio { rectangle.origin.y = originFrame.origin.y + yDelta rectangle.size.height = originFrame.height - yDelta } } clampMinFromTop = true case .topLeft: if cropView.aspectRatioLockEnabled { xDelta = max(xDelta, 0) yDelta = max(yDelta, 0) var distance = CGPoint.zero distance.x = 1.0 - (xDelta / originFrame.width ) distance.y = 1.0 - (yDelta / originFrame.height) let scale = (distance.x + distance.y) * 0.5 rectangle.size.width = ceil(originFrame.width * scale) rectangle.size.height = ceil(originFrame.height * scale) rectangle.origin.x = originFrame.origin.x + (originFrame.width - rectangle.width ) rectangle.origin.y = originFrame.origin.y + (originFrame.height - rectangle.height) aspectHorizontal = true aspectVertical = true } else { let newWidth = originFrame.width - xDelta let newHeight = originFrame.height - yDelta if min(newHeight, newWidth) / max(newHeight, newWidth) >= cropView.minimumAspectRatio { rectangle.origin.x = originFrame.origin.x + xDelta rectangle.origin.y = originFrame.origin.y + yDelta rectangle.size.width = originFrame.width - xDelta rectangle.size.height = originFrame.height - yDelta } } clampMinFromTop = true clampMinFromLeft = true case .topRight: if cropView.aspectRatioLockEnabled { xDelta = min(xDelta, 0) yDelta = max(yDelta, 0) var distance = CGPoint.zero distance.x = 1.0 - ((-xDelta) / originFrame.width ) distance.y = 1.0 - (( yDelta) / originFrame.height) let scale = (distance.x + distance.y) * 0.5 rectangle.size.width = ceil(originFrame.width * scale) rectangle.size.height = ceil(originFrame.height * scale) rectangle.origin.y = originFrame.origin.y + (originFrame.height - rectangle.height) aspectHorizontal = true aspectVertical = true } else { let newWidth = originFrame.width + xDelta let newHeight = originFrame.height - yDelta if min(newHeight, newWidth) / max(newHeight, newWidth) >= cropView.minimumAspectRatio { rectangle.size.width = originFrame.width + xDelta rectangle.size.height = originFrame.height - yDelta rectangle.origin.y = originFrame.origin.y + yDelta } } clampMinFromTop = true case .bottomLeft: if cropView.aspectRatioLockEnabled { var distance = CGPoint.zero distance.x = 1.0 - ( xDelta / originFrame.width ) distance.y = 1.0 - (-yDelta / originFrame.height) let scale = (distance.x + distance.y) * 0.5 rectangle.size.width = ceil(originFrame.width * scale) rectangle.size.height = ceil(originFrame.height * scale) rectangle.origin.x = originFrame.maxX - rectangle.width aspectHorizontal = true aspectVertical = true } else { let newWidth = originFrame.width - xDelta let newHeight = originFrame.height + yDelta if min(newHeight, newWidth) / max(newHeight, newWidth) >= cropView.minimumAspectRatio { rectangle.origin.x = originFrame.origin.x + xDelta rectangle.size.height = originFrame.height + yDelta rectangle.size.width = originFrame.width - xDelta } } clampMinFromLeft = true case .bottomRight: if cropView.aspectRatioLockEnabled { var distance = CGPoint.zero distance.x = 1.0 - ((-1 * xDelta) / originFrame.width ) distance.y = 1.0 - ((-1 * yDelta) / originFrame.height) let scale = (distance.x + distance.y) * 0.5 rectangle.size.width = ceil(originFrame.width * scale) rectangle.size.height = ceil(originFrame.height * scale) aspectHorizontal = true aspectVertical = true } else { let newWidth = originFrame.width + xDelta let newHeight = originFrame.height + yDelta if min(newHeight, newWidth) / max(newHeight, newWidth) >= cropView.minimumAspectRatio { rectangle.size.height = originFrame.height + yDelta rectangle.size.width = originFrame.width + xDelta } } case .none: break } //The absolute max/min size the box may be in the bounds of the crop view var minSize = CGSize(width: CLDCropView.Constants.minimumBoxSize, height: CLDCropView.Constants.minimumBoxSize) var maxSize = CGSize(width: contentFrame.width , height: contentFrame.height ) //clamp the box to ensure it doesn't go beyond the bounds we've set if (cropView.aspectRatioLockEnabled && aspectHorizontal) { minSize.width = CLDCropView.Constants.minimumBoxSize * aspectRatio maxSize.height = contentFrame.width / aspectRatio } if (cropView.aspectRatioLockEnabled && aspectVertical) { maxSize.width = contentFrame.height * aspectRatio minSize.height = CLDCropView.Constants.minimumBoxSize / aspectRatio } // Clamp the width if it goes over if (clampMinFromLeft) { let maxWidth = cropView.cropOriginFrame.maxX - contentFrame.origin.x rectangle.size.width = min(rectangle.width, maxWidth) } if (clampMinFromTop) { let maxHeight = cropView.cropOriginFrame.maxY - contentFrame.origin.y rectangle.size.height = min(rectangle.height, maxHeight) } // Clamp the minimum size rectangle.size.width = max(rectangle.width , minSize.width ) rectangle.size.height = max(rectangle.height, minSize.height) // Clamp the maximum size rectangle.size.width = min(rectangle.width , maxSize.width ) rectangle.size.height = min(rectangle.height, maxSize.height) //Clamp the X position of the box to the interior of the cropping bounds rectangle.origin.x = max(rectangle.origin.x, contentFrame.minX) rectangle.origin.x = min(rectangle.origin.x, contentFrame.maxX - minSize.width) //Clamp the Y postion of the box to the interior of the cropping bounds rectangle.origin.y = max(rectangle.origin.y, contentFrame.minY) rectangle.origin.y = min(rectangle.origin.y, contentFrame.maxY - minSize.height) //Once the box is completely shrunk, clamp its ability to move if (clampMinFromLeft && rectangle.width <= minSize.width + CGFloat.ulpOfOne) { rectangle.origin.x = originFrame.maxX - minSize.width } //Once the box is completely shrunk, clamp its ability to move if (clampMinFromTop && rectangle.height <= minSize.height + CGFloat.ulpOfOne) { rectangle.origin.y = originFrame.maxY - minSize.height } return rectangle } } internal extension CLDCropView.Calculator.AspectRatio { static func postChangeAdjustments(given newValue: CGSize, animated: Bool, in cropView: CLDCropView) -> (scrollOffset: CGPoint, cropBoxFrame: CGRect , ShouldZoomOut: Bool){ var newAspectRatio = newValue // Passing in an empty size will revert back to the image aspect ratio if (newAspectRatio.width < CGFloat.ulpOfOne && newAspectRatio.height < CGFloat.ulpOfOne ) { newAspectRatio = CGSize(width: cropView.imageSize.width, height: cropView.imageSize.height) } let boundsFrame = cropView.contentBounds var rectangle = cropView.cropBoxFrame var offset = cropView.scrollView.contentOffset var cropBoxIsPortrait = false if (Int(newAspectRatio.width) == 1 && Int(newAspectRatio.height) == 1) { cropBoxIsPortrait = cropView.image.size.width > cropView.image.size.height } else { cropBoxIsPortrait = newAspectRatio.width < newAspectRatio.height } var shouldZoomOut = false switch cropBoxIsPortrait { case true : let newWidth = floor(rectangle.height * (newAspectRatio.width / newAspectRatio.height) ) var delta = rectangle.width - newWidth rectangle.size.width = newWidth offset.x += (delta * 0.5) // Set to 0 to avoid accidental clamping by the crop frame sanitizer if (delta < CGFloat.ulpOfOne ) { rectangle.origin.x = boundsFrame.origin.x } // If the aspect ratio causes the new width to extend // beyond the content width, we'll need to zoom the image out let boundsWidth = boundsFrame.width if (newWidth > boundsWidth) { let scale = boundsWidth / newWidth // Scale the new height let newHeight = rectangle.height * scale delta = rectangle.height - newHeight rectangle.size.height = newHeight // Offset the Y position so it stays in the middle offset.y += (delta * 0.5) // Clamp the width to the bounds width rectangle.size.width = boundsWidth shouldZoomOut = true } case false: let newHeight = floor(rectangle.width * (newAspectRatio.height/newAspectRatio.width) ) var delta = rectangle.height - newHeight rectangle.size.height = newHeight offset.y += (delta * 0.5) if delta < CGFloat.ulpOfOne { rectangle.origin.y = boundsFrame.origin.y } // If the aspect ratio causes the new height to extend // beyond the content width, we'll need to zoom the image out let boundsHeight = boundsFrame.height if (newHeight > boundsHeight) { let scale = boundsHeight / newHeight // Scale the new width let newWidth = rectangle.width * scale delta = rectangle.size.width - newWidth rectangle.size.width = newWidth // Offset the Y position so it stays in the middle offset.x += (delta * 0.5) // Clamp the width to the bounds height rectangle.size.height = boundsHeight shouldZoomOut = true } } return (scrollOffset: offset, cropBoxFrame: rectangle , ShouldZoomOut: shouldZoomOut) } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/CropView/CLDCropViewUIManager.swift ================================================ // // CLDCropViewUIManager.swift // Cloudinary // // Created by Oz Deutsch on 04/10/2020. // import UIKit internal class CLDCropViewUIManager { internal weak var cropView: CLDCropView! internal init(cropView: CLDCropView) { self.cropView = cropView } /** layout the initial image and returns the scaled image size */ internal func manualLayout_initialImageAndGetScaledImageSize() -> CGSize { cropView.scrollView.contentSize = cropView.imageSize let boundsRectangle = cropView.contentBounds let boundsSize = boundsRectangle.size // Work out the minimum scale of the object var scale = CGFloat(0.0) // Work out the size of the image to fit into the content bounds scale = min(boundsRectangle.width / cropView.imageSize.width, boundsRectangle.height / cropView.imageSize.height) let scaledImageSize = CGSize(width : floor(cropView.imageSize.width * scale), height: floor(cropView.imageSize.height * scale)) // If an aspect ratio was pre-applied to the crop view, use that to work out the minimum scale the image needs to be to fit var cropBoxSize = CGSize.zero if (cropView.hasAspectRatio) { let ratioScale = cropView.aspectRatio.width / cropView.aspectRatio.height // Work out the size of the width in relation to height let fullSizeRatio = CGSize(width: boundsSize.height * ratioScale, height: boundsSize.height) let fitScale = min(boundsSize.width / fullSizeRatio.width, boundsSize.height / fullSizeRatio.height) cropBoxSize = CGSize(width: fullSizeRatio.width * fitScale, height: fullSizeRatio.height * fitScale) scale = max(cropBoxSize.width / cropView.imageSize.width, cropBoxSize.height / cropView.imageSize.height) } //Whether aspect ratio, or original, the final image size we'll base the rest of the calculations off let scaledSize = CGSize(width: floor(cropView.imageSize.width * scale), height: floor(cropView.imageSize.height * scale)) // Configure the scroll view cropView.scrollView.minimumZoomScale = scale cropView.scrollView.maximumZoomScale = scale * cropView.maximumZoomScale // Set the crop box to the size we calculated and align in the middle of the screen var rectangle = CGRect.zero rectangle.size = cropView.hasAspectRatio ? cropBoxSize : scaledSize rectangle.origin.x = boundsRectangle.origin.x + floor((boundsRectangle.width - rectangle.width ) * 0.5) rectangle.origin.y = boundsRectangle.origin.y + floor((boundsRectangle.height - rectangle.height) * 0.5) cropView.cropBoxFrame = rectangle // Set the fully zoomed out state initially cropView.scrollView.zoomScale = cropView.scrollView.minimumZoomScale cropView.scrollView.contentSize = scaledSize // If we ended up with a smaller crop box than the content, line up the content so its center // is in the center of the cropbox if (rectangle.width < scaledSize.width - CGFloat.ulpOfOne || rectangle.height < scaledSize.height - CGFloat.ulpOfOne) { var offset = CGPoint.zero offset.x = -floor(boundsRectangle.midX - (scaledSize.width * 0.5)) offset.y = -floor(boundsRectangle.midY - (scaledSize.height * 0.5)) cropView.scrollView.contentOffset = offset } return scaledImageSize } internal func manualLayout_moveCroppedContentToCenterAnimated(_ animated: Bool) { if cropView.internalLayoutDisabled { return } let contentRect = cropView.contentBounds var cropFrame = cropView.cropBoxFrame // Ensure we only proceed after the crop frame has been setup for the first time if (cropFrame.width < CGFloat.ulpOfOne || cropFrame.height < CGFloat.ulpOfOne) { return } // The scale we need to scale up the crop box to fit full screen let scale = min(contentRect.width / cropFrame.width, contentRect.height / cropFrame.height) let focusPoint = CGPoint(x: cropFrame.midX, y: cropFrame.midY) let midPoint = CGPoint(x: contentRect.midX, y: contentRect.midY) cropFrame.size.width = ceil(cropFrame.width * scale) cropFrame.size.height = ceil(cropFrame.height * scale) cropFrame.origin.x = contentRect.origin.x + ceil(0.5 * (contentRect.width - cropFrame.width )) cropFrame.origin.y = contentRect.origin.y + ceil(0.5 * (contentRect.height - cropFrame.height)) // Work out the point on the scroll content that the focusPoint is aiming at var contentTargetPoint = CGPoint.zero contentTargetPoint.x = ((focusPoint.x + cropView.scrollView.contentOffset.x) * scale) contentTargetPoint.y = ((focusPoint.y + cropView.scrollView.contentOffset.y) * scale) // Work out where the crop box is focusing, so we can re-align to center that point var offset = CGPoint.zero offset.x = -midPoint.x + contentTargetPoint.x offset.y = -midPoint.y + contentTargetPoint.y // Clamp the content so it doesn't create any seams around the grid offset.x = max(-cropFrame.origin.x, offset.x) offset.y = max(-cropFrame.origin.y, offset.y) let translateBlock : () -> Void = { var offsetHelper = offset // Setting these scroll view properties will trigger // the foreground matching method via their delegates, // multiple times inside the same animation block, resulting // in glitchy animations. // // Disable matching for now, and explicitly update at the end. self.cropView.disableForgroundMatching = true // Slight hack. This method needs to be called during `[UIViewController viewDidLayoutSubviews]` // in order for the crop view to resize itself during iPad split screen events. // On the first run, even though scale is exactly 1.0f, performing this multiplication introduces // a floating point noise that zooms the image in by about 5 pixels. This fixes that issue. if scale < 1.0 - CGFloat.ulpOfOne || scale > 1.0 + CGFloat.ulpOfOne { self.cropView.scrollView.zoomScale *= scale self.cropView.scrollView.zoomScale = min(self.cropView.scrollView.maximumZoomScale, self.cropView.scrollView.zoomScale) } // If it turns out the zoom operation would have exceeded the minizum zoom scale, don't apply // the content offset if (self.cropView.scrollView.zoomScale < self.cropView.scrollView.maximumZoomScale - CGFloat.ulpOfOne) { offsetHelper.x = min(-cropFrame.maxX + self.cropView.scrollView.contentSize.width , offset.x) offsetHelper.y = min(-cropFrame.maxY + self.cropView.scrollView.contentSize.height, offset.y) self.cropView.scrollView.contentOffset = offsetHelper } self.cropView.cropBoxFrame = cropFrame self.cropView.disableForgroundMatching = false // Explicitly update the matching at the end of the calculations self.cropView.matchForegroundToBackground() } switch animated { case false: translateBlock() case true: cropView.matchForegroundToBackground() DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: [.beginFromCurrentState], animations: translateBlock, completion: nil) } } } internal func manualLayout_rotateImageNinetyDegreesAnimated(_ animated: Bool, clockwise: Bool) { // Only allow one rotation animation at a time if cropView.rotateAnimationInProgress { return } // Cancel any pending resizing timers if cropView.resetTimer != nil { cropView.cancelResetTimer() cropView.setEditing(false, resetCropBox: true, animated: false) cropView.cropBoxLastEditedAngle = cropView.angle cropView.captureStateForImageRotation() } // Work out the new angle, and wrap around once we exceed 360s var newAngle = cropView.angle newAngle = clockwise ? newAngle + 90 : newAngle - 90 if (newAngle <= -360 || newAngle >= 360) { newAngle = 0 } cropView._angle = newAngle // Convert the new angle to radians var angleInRadians = CGFloat(0.0) switch (newAngle) { case 90: angleInRadians = CGFloat(Double.pi / 2) case -90: angleInRadians = -CGFloat(Double.pi / 2) case 180: angleInRadians = CGFloat(Double.pi) case -180: angleInRadians = -CGFloat(Double.pi) case 270: angleInRadians = CGFloat(Double.pi + (Double.pi / 2)) case -270: angleInRadians = -CGFloat(Double.pi + (Double.pi / 2)) default: break } // Set up the transformation matrix for the rotation let rotation = CGAffineTransform.identity.rotated(by: angleInRadians) // Work out how much we'll need to scale everything to fit to the new rotation let contentBounds = cropView.contentBounds let cropBoxFrame = cropView.cropBoxFrame let scale = min(contentBounds.width / cropBoxFrame.height, contentBounds.height / cropBoxFrame.width) // Work out which section of the image we're currently focusing at let cropMidPoint = CGPoint(x: cropBoxFrame.midX, y: cropBoxFrame.midY) var cropTargetPoint = CGPoint(x: cropMidPoint.x + cropView.scrollView.contentOffset.x, y: cropMidPoint.y + cropView.scrollView.contentOffset.y) // Work out the dimensions of the crop box when rotated var newCropFrame = CGRect.zero if (labs(cropView.angle) * 1) == ((labs(cropView.cropBoxLastEditedAngle) ) ) || (labs(cropView.angle) * -1) == ((labs(cropView.cropBoxLastEditedAngle) - 180) % 360) { newCropFrame.size = cropView.cropBoxLastEditedSize cropView.scrollView.minimumZoomScale = cropView.cropBoxLastEditedMinZoomScale cropView.scrollView.zoomScale = cropView.cropBoxLastEditedZoomScale } else { newCropFrame.size = CGSize(width : floor(cropBoxFrame.height * scale), height: floor(cropBoxFrame.width * scale)) // Re-adjust the scrolling dimensions of the scroll view to match the new size cropView.scrollView.minimumZoomScale *= scale cropView.scrollView.zoomScale *= scale } newCropFrame.origin.x = floor(contentBounds.midX - (newCropFrame.width * 0.5)) newCropFrame.origin.y = floor(contentBounds.midY - (newCropFrame.height * 0.5)) // If we're animated, generate a snapshot view that we'll animate in place of the real view var snapshotView: UIView? = nil if (animated) { snapshotView = cropView.foregroundContainerView.snapshotView(afterScreenUpdates: false) cropView.rotateAnimationInProgress = true } // Rotate the background image view, inside its container view cropView.backgroundImageView.transform = rotation // Flip the width/height of the container view so it matches the rotated image view's size let containerSize = cropView.backgroundContainerView.frame.size cropView.backgroundContainerView.frame = CGRect(origin: .zero, size: CGSize(width: containerSize.height, height: containerSize.width)) cropView.backgroundImageView.frame = CGRect(origin: .zero, size: cropView.backgroundImageView.frame.size) // Rotate the foreground image view to match cropView.foregroundContainerView.transform = CGAffineTransform.identity cropView.foregroundImageView.transform = rotation // Flip the content size of the scroll view to match the rotated bounds cropView.scrollView.contentSize = cropView.backgroundContainerView.frame.size // Assign the new crop box frame and re-adjust the content to fill it cropView.cropBoxFrame = newCropFrame cropView.moveCroppedContentToCenterAnimated(false) newCropFrame = cropView.cropBoxFrame // Work out how to line up out point of interest into the middle of the crop box cropTargetPoint.x *= scale cropTargetPoint.y *= scale // Swap the target dimensions to match a 90 degree rotation (clockwise or counterclockwise) let swapValue = cropTargetPoint.x if (clockwise) { cropTargetPoint.x = cropView.scrollView.contentSize.width - cropTargetPoint.y cropTargetPoint.y = swapValue } else { cropTargetPoint.x = cropTargetPoint.y cropTargetPoint.y = cropView.scrollView.contentSize.height - swapValue } // Reapply the translated scroll offset to the scroll view let midPoint = CGPoint(x: newCropFrame.midX, y: newCropFrame.midY) var offset = CGPoint.zero offset.x = floor(-midPoint.x + cropTargetPoint.x) offset.y = floor(-midPoint.y + cropTargetPoint.y) offset.x = max(-cropView.scrollView.contentInset.left, offset.x) offset.y = max(-cropView.scrollView.contentInset.top , offset.y) offset.x = min(cropView.scrollView.contentSize.width - (newCropFrame.width - cropView.scrollView.contentInset.right ), offset.x) offset.y = min(cropView.scrollView.contentSize.height - (newCropFrame.height - cropView.scrollView.contentInset.bottom), offset.y) // If the scroll view's new scale is 1 and the new offset is equal to the old, will not trigger the delegate 'scrollViewDidScroll:' // so we should call the method manually to update the foregroundImageView's frame if (offset.x == cropView.scrollView.contentOffset.x && offset.y == cropView.scrollView.contentOffset.y && scale == 1) { cropView.matchForegroundToBackground() } cropView.scrollView.contentOffset = offset // If we're animated, play an animation of the snapshot view rotating, // then fade it out over the live content if (animated) { if let snapshotView = snapshotView { snapshotView.center = CGPoint(x: contentBounds.midX, y: contentBounds.midY) cropView.addSubview(snapshotView) } cropView.backgroundContainerView.isHidden = true cropView.foregroundContainerView.isHidden = true cropView.translucencyView.isHidden = true cropView.gridOverlayView.isHidden = true UIView.animate(withDuration: 0.45, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.8, options: [.beginFromCurrentState], animations: { let target = CGAffineTransform.identity .rotated(by: clockwise ? CGFloat(Double.pi / 2) : -CGFloat(Double.pi / 2)) .scaledBy(x: scale, y: scale) if let snapshotView = snapshotView { snapshotView.transform = target } }, completion: { (complete) in self.cropView.backgroundContainerView.isHidden = false self.cropView.foregroundContainerView.isHidden = false self.cropView.translucencyView.isHidden = self.cropView.translucencyAlwaysHidden self.cropView.gridOverlayView.isHidden = false self.cropView.backgroundContainerView.alpha = 0.0 self.cropView.gridOverlayView.alpha = 0.0 self.cropView.translucencyView.alpha = 1.0 UIView.animate(withDuration: 0.45, animations: { snapshotView?.alpha = 0.0 self.cropView.backgroundContainerView.alpha = 1.0 self.cropView.gridOverlayView.alpha = 1.0 }, completion: { (complete) in self.cropView.rotateAnimationInProgress = false snapshotView?.removeFromSuperview() // If the aspect ratio lock is not enabled, allow a swap // If the aspect ratio lock is on, allow a aspect ratio swap // only if the allowDimensionSwap option is specified. let aspectRatioCanSwapDimensions = (!self.cropView.aspectRatioLockEnabled) || (self.cropView.aspectRatioLockEnabled && self.cropView.aspectRatioLockDimensionSwapEnabled) if (!aspectRatioCanSwapDimensions) { //This will animate the aspect ratio back to the desired locked ratio after the image is rotated. self.cropView.setAspectRatio(self.cropView.aspectRatio, animated: animated) } }) }) } cropView.checkForCanReset() } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetConfiguration/CLDWidgetConfiguration.swift ================================================ // // CLDWidgetConfiguration.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit @objcMembers public class CLDWidgetConfiguration: NSObject { public var allowRotate : Bool public var initialAspectLockState: AspectRatioLockState public var uploadType : CLDUploadType // MARK: - init /** Initializes the `CLDWidgetConfiguration` instance with the specified allowRotate, initialAspectLockState and uploadType. - parameter allowRotate: A boolean value specifying whether or not to allow image rotation. true by default. - parameter initialAspectLockState: Enum value specifying the initial aspect ratio lock state. enabledAndOn by default. - parameter uploadType: CLDUploadType object specifying the upload request type. signed without preset by default. - returns: The new `CLDWidgetConfiguration` instance. */ public init( allowRotate : Bool = true, initialAspectLockState: AspectRatioLockState = .enabledAndOff, uploadType : CLDUploadType = CLDUploadType(signed: true, preset: nil) ) { self.allowRotate = allowRotate self.initialAspectLockState = initialAspectLockState self.uploadType = uploadType super.init() } // MARK: - AspectRatioLockState /** Aspect ratio lock state * enabledAndOff: User can change the aspect ratio lock state - initial state is aspect ratio not locked. * enabledAndOn: User can change the aspect ratio lock state - initial state is aspect ratio locked. * disabled: Aspect ratio lock state button is removed. User will be able to change the aspect ratio of the image, but not the lock state. */ @objc public enum AspectRatioLockState: Int { case enabledAndOff case enabledAndOn case disabled } } @objcMembers public class CLDUploadType: NSObject { private(set) var signed: Bool private(set) var preset: String? /** Initializes the `CLDUploadType` instance with the specified signed and preset. - parameter signed: A boolean value specifying whether to use signed or unsigned upload. - parameter preset: A string represents the preset for unsigned upload requests. preset MUST be set when choosing unsigned requests. - returns: The new `CLDUploadType` instance. */ public init(signed: Bool, preset: String?) { self.signed = signed self.preset = preset if !signed, (preset == nil || preset == String()) { print("unsigned upload must have a predefined preset!") } } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetElements/CLDWidgetAssetContainer.swift ================================================ // // CLDWidgetAssetContainer.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit import AVKit class CLDWidgetAssetContainer: NSObject { enum AssetType: Int { case image case video } private(set) var originalImage : UIImage? var editedImage : UIImage? private(set) var originalVideo : AVPlayerItem? var presentationImage: UIImage private(set) var assetType : AssetType init(originalImage: UIImage, editedImage: UIImage) { self.originalImage = originalImage self.editedImage = editedImage self.presentationImage = editedImage assetType = .image super.init() } init(videoUrl: URL) { let playerItem = AVPlayerItem(url: videoUrl) self.originalVideo = playerItem self.presentationImage = CLDWidgetAssetContainer.createThumbnailForVideo(playerItem: self.originalVideo) assetType = .video super.init() } init(videoItem: AVPlayerItem) { self.originalVideo = videoItem self.presentationImage = CLDWidgetAssetContainer.createThumbnailForVideo(playerItem: self.originalVideo) assetType = .video super.init() } private static func createThumbnailForVideo(playerItem: AVPlayerItem?) -> UIImage { if let urlAsset = playerItem?.asset as? AVURLAsset { let url = urlAsset.url let asset = AVAsset(url: url) let avAssetImageGenerator = AVAssetImageGenerator(asset: asset) avAssetImageGenerator.appliesPreferredTrackTransform = true let thumnailTime = CMTimeMake(value: 2, timescale: 1) do { let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumnailTime, actualTime: nil) return UIImage(cgImage: cgThumbImage) } catch { print(error.localizedDescription) return UIImage() } } else { return UIImage() } } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetElements/CLDWidgetPreviewCollectionCell.swift ================================================ // // CLDWidgetPreviewCollectionCell.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit class CLDWidgetPreviewCollectionCell: UICollectionViewCell { var imageView: UIImageView! // MARK: - init override init(frame: CGRect) { super.init(frame: frame) initialize() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - private methods private func initialize() { self.imageView = UIImageView(frame: self.contentView.bounds) addSubview(imageView) addImageViewProperties() } private func addImageViewProperties() { imageView.contentMode = .scaleAspectFill } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetImages/BackIconInstructions.swift ================================================ // // BackIconInstructions.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit class BackIconInstructions : CLDImageDrawingInstructions { var targetSize : CGSize var fillColor : UIColor // MARK: - Initialization init(targetSize size: CGSize = CGSize(width: 24.0, height: 24.0), fillColor: UIColor = .white) { self.targetSize = size self.fillColor = fillColor } // MARK: - draw func draw(in context: CGContext) { let container = CGRect(origin: .zero, size: targetSize) let pathBackArrowPath = UIBezierPath() pathBackArrowPath.move(to: CGPoint(x: container.minX + 0.83333 * container.width, y: container.minY + 0.45833 * container.height)) pathBackArrowPath.addLine(to: CGPoint(x: container.minX + 0.32625 * container.width, y: container.minY + 0.45833 * container.height)) pathBackArrowPath.addLine(to: CGPoint(x: container.minX + 0.55917 * container.width, y: container.minY + 0.22542 * container.height)) pathBackArrowPath.addLine(to: CGPoint(x: container.minX + 0.50000 * container.width, y: container.minY + 0.16667 * container.height)) pathBackArrowPath.addLine(to: CGPoint(x: container.minX + 0.16667 * container.width, y: container.minY + 0.50000 * container.height)) pathBackArrowPath.addLine(to: CGPoint(x: container.minX + 0.50000 * container.width, y: container.minY + 0.83333 * container.height)) pathBackArrowPath.addLine(to: CGPoint(x: container.minX + 0.55875 * container.width, y: container.minY + 0.77458 * container.height)) pathBackArrowPath.addLine(to: CGPoint(x: container.minX + 0.32625 * container.width, y: container.minY + 0.54167 * container.height)) pathBackArrowPath.addLine(to: CGPoint(x: container.minX + 0.83333 * container.width, y: container.minY + 0.54167 * container.height)) pathBackArrowPath.addLine(to: CGPoint(x: container.minX + 0.83333 * container.width, y: container.minY + 0.45833 * container.height)) pathBackArrowPath.close() pathBackArrowPath.usesEvenOddFillRule = true fillColor.setFill() pathBackArrowPath.fill() } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetImages/CropIconInstructions.swift ================================================ // // CropIconInstructions.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit class CropIconInstructions : CLDImageDrawingInstructions { var targetSize : CGSize var fillColor : UIColor // MARK: - Initialization init(targetSize size: CGSize = CGSize(width: 24.0, height: 24.0), fillColor: UIColor = .white) { self.targetSize = size self.fillColor = fillColor } // MARK: - draw func draw(in context: CGContext) { let container = CGRect(origin: .zero, size: targetSize) let pathCropPath = UIBezierPath() pathCropPath.move(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.62500 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.79167 * container.width, y: container.minY + 0.62500 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.79167 * container.width, y: container.minY + 0.29167 * container.height)) pathCropPath.addCurve(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.20833 * container.height), controlPoint1: CGPoint(x: container.minX + 0.79167 * container.width, y: container.minY + 0.24583 * container.height), controlPoint2: CGPoint(x: container.minX + 0.75417 * container.width, y: container.minY + 0.20833 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.37500 * container.width, y: container.minY + 0.20833 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.37500 * container.width, y: container.minY + 0.29167 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.29167 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.62500 * container.height)) pathCropPath.close() pathCropPath.move(to: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.70833 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.04167 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.20833 * container.width, y: container.minY + 0.04167 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.20833 * container.width, y: container.minY + 0.20833 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.04167 * container.width, y: container.minY + 0.20833 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.04167 * container.width, y: container.minY + 0.29167 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.20833 * container.width, y: container.minY + 0.29167 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.20833 * container.width, y: container.minY + 0.70833 * container.height)) pathCropPath.addCurve(to: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.79167 * container.height), controlPoint1: CGPoint(x: container.minX + 0.20833 * container.width, y: container.minY + 0.75417 * container.height), controlPoint2: CGPoint(x: container.minX + 0.24583 * container.width, y: container.minY + 0.79167 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.79167 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.95833 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.79167 * container.width, y: container.minY + 0.95833 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.79167 * container.width, y: container.minY + 0.79167 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.95833 * container.width, y: container.minY + 0.79167 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.95833 * container.width, y: container.minY + 0.70833 * container.height)) pathCropPath.addLine(to: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.70833 * container.height)) pathCropPath.close() pathCropPath.usesEvenOddFillRule = true fillColor.setFill() pathCropPath.fill() } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetImages/CropRotateIconInstructions.swift ================================================ // // CropRotateIconInstructions.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit class CropRotateIconInstructions : CLDImageDrawingInstructions { var targetSize : CGSize var fillColor : UIColor // MARK: - Initialization init(targetSize size: CGSize = CGSize(width: 24.0, height: 24.0), fillColor: UIColor = .white) { self.targetSize = size self.fillColor = fillColor } // MARK: - draw func draw(in context: CGContext) { let container = CGRect(origin: .zero, size: targetSize) let pathCropRotatePath = UIBezierPath() pathCropRotatePath.move(to: CGPoint(x: container.minX + 0.62500 * container.width, y: container.minY + 0.62500 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.62500 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.37500 * container.height)) pathCropRotatePath.addCurve(to: CGPoint(x: container.minX + 0.62500 * container.width, y: container.minY + 0.29167 * container.height), controlPoint1: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.32875 * container.height), controlPoint2: CGPoint(x: container.minX + 0.67083 * container.width, y: container.minY + 0.29167 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.37500 * container.width, y: container.minY + 0.29167 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.37500 * container.width, y: container.minY + 0.37500 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.62500 * container.width, y: container.minY + 0.37500 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.62500 * container.width, y: container.minY + 0.62500 * container.height)) pathCropRotatePath.close() pathCropRotatePath.move(to: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.70833 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.20833 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.20833 * container.width, y: container.minY + 0.20833 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.20833 * container.width, y: container.minY + 0.29167 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.12500 * container.width, y: container.minY + 0.29167 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.12500 * container.width, y: container.minY + 0.37500 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.20833 * container.width, y: container.minY + 0.37500 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.20833 * container.width, y: container.minY + 0.70833 * container.height)) pathCropRotatePath.addCurve(to: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.79167 * container.height), controlPoint1: CGPoint(x: container.minX + 0.20833 * container.width, y: container.minY + 0.75417 * container.height), controlPoint2: CGPoint(x: container.minX + 0.24542 * container.width, y: container.minY + 0.79167 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.62500 * container.width, y: container.minY + 0.79167 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.62500 * container.width, y: container.minY + 0.87500 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.87500 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.79167 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.79167 * container.width, y: container.minY + 0.79167 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.79167 * container.width, y: container.minY + 0.70833 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.70833 * container.height)) pathCropRotatePath.close() pathCropRotatePath.move(to: CGPoint(x: container.minX + 0.46042 * container.width, y: container.minY + 0.04167 * container.height)) pathCropRotatePath.addCurve(to: CGPoint(x: container.minX + 0.43292 * container.width, y: container.minY + 0.04333 * container.height), controlPoint1: CGPoint(x: container.minX + 0.45083 * container.width, y: container.minY + 0.04167 * container.height), controlPoint2: CGPoint(x: container.minX + 0.44208 * container.width, y: container.minY + 0.04250 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.59167 * container.width, y: container.minY + 0.20208 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.64708 * container.width, y: container.minY + 0.14667 * container.height)) pathCropRotatePath.addCurve(to: CGPoint(x: container.minX + 0.89583 * container.width, y: container.minY + 0.50000 * container.height), controlPoint1: CGPoint(x: container.minX + 0.78333 * container.width, y: container.minY + 0.21125 * container.height), controlPoint2: CGPoint(x: container.minX + 0.88083 * container.width, y: container.minY + 0.34333 * container.height)) pathCropRotatePath.addLine(to: CGPoint(x: container.minX + 0.95833 * container.width, y: container.minY + 0.50000 * container.height)) pathCropRotatePath.addCurve(to: CGPoint(x: container.minX + 0.46042 * container.width, y: container.minY + 0.04167 * container.height), controlPoint1: CGPoint(x: container.minX + 0.93708 * container.width, y: container.minY + 0.24333 * container.height), controlPoint2: CGPoint(x: container.minX + 0.72250 * container.width, y: container.minY + 0.04167 * container.height)) pathCropRotatePath.close() pathCropRotatePath.usesEvenOddFillRule = true fillColor.setFill() pathCropRotatePath.fill() } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetImages/DoneIconInstructions.swift ================================================ // // DoneIconInstructions.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit class DoneIconInstructions : CLDImageDrawingInstructions { var targetSize : CGSize var fillColor : UIColor var ovalBackgroundColor: UIColor // MARK: - Initialization init(targetSize size: CGSize = CGSize(width: 60.0, height: 60.0), fillColor: UIColor = .white, ovalBackgroundColor: UIColor = UIColor(red: 38.0/255, green: 91.0/255, blue: 220.0/255, alpha: 1.0)) { self.targetSize = size self.fillColor = fillColor self.ovalBackgroundColor = ovalBackgroundColor } // MARK: - draw func draw(in context: CGContext) { let container = CGRect(origin: .zero, size: targetSize) let vContainer = CGRect(origin: .zero, size: CGSize(width: targetSize.width/2, height: targetSize.height/2)) // This non-generic function dramatically improves compilation times of complex expressions. func fastFloor(_ x: CGFloat) -> CGFloat { return floor(x) } // Oval Drawing let ovalPath = UIBezierPath(ovalIn: CGRect(x: container.minX + fastFloor(container.width * 0.00000 + 0.5), y: container.minY + fastFloor(container.height * 0.00000 + 0.5), width: fastFloor(container.width * 1.00000 + 0.5) - fastFloor(container.width * 0.00000 + 0.5), height: fastFloor(container.height * 1.00000 + 0.5) - fastFloor(container.height * 0.00000 + 0.5))) ovalBackgroundColor.setFill() ovalPath.fill() //// pathDone Drawing let pathDonePath = UIBezierPath() pathDonePath.move(to: CGPoint(x: vContainer.midX + 0.37500 * vContainer.width, y: vContainer.midY + 0.67500 * vContainer.height)) pathDonePath.addLine(to: CGPoint(x: vContainer.midX + 0.20000 * vContainer.width, y: vContainer.midY + 0.50000 * vContainer.height)) pathDonePath.addLine(to: CGPoint(x: vContainer.midX + 0.14167 * vContainer.width, y: vContainer.midY + 0.55833 * vContainer.height)) pathDonePath.addLine(to: CGPoint(x: vContainer.midX + 0.37500 * vContainer.width, y: vContainer.midY + 0.79167 * vContainer.height)) pathDonePath.addLine(to: CGPoint(x: vContainer.midX + 0.87500 * vContainer.width, y: vContainer.midY + 0.29167 * vContainer.height)) pathDonePath.addLine(to: CGPoint(x: vContainer.midX + 0.81667 * vContainer.width, y: vContainer.midY + 0.23333 * vContainer.height)) pathDonePath.addLine(to: CGPoint(x: vContainer.midX + 0.37500 * vContainer.width, y: vContainer.midY + 0.67500 * vContainer.height)) pathDonePath.close() pathDonePath.usesEvenOddFillRule = true fillColor.setFill() pathDonePath.fill() } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetImages/RatioLockedIconInstructions.swift ================================================ // // RatioLockedIconInstructions.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit class RatioLockedIconInstructions : CLDImageDrawingInstructions { var targetSize : CGSize var fillColor : UIColor // MARK: - Initialization init(targetSize size: CGSize = CGSize(width: 24.0, height: 24.0), fillColor: UIColor = .white) { self.targetSize = size self.fillColor = fillColor } // MARK: - draw func draw(in context: CGContext) { let container = CGRect(origin: .zero, size: targetSize) let pathRatioLockedPath = UIBezierPath() pathRatioLockedPath.move(to: CGPoint(x: container.minX + 0.75000 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioLockedPath.addLine(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioLockedPath.addLine(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.25000 * container.height)) pathRatioLockedPath.addCurve(to: CGPoint(x: container.minX + 0.50000 * container.width, y: container.minY + 0.04167 * container.height), controlPoint1: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.13500 * container.height), controlPoint2: CGPoint(x: container.minX + 0.61500 * container.width, y: container.minY + 0.04167 * container.height)) pathRatioLockedPath.addCurve(to: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.25000 * container.height), controlPoint1: CGPoint(x: container.minX + 0.38500 * container.width, y: container.minY + 0.04167 * container.height), controlPoint2: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.13500 * container.height)) pathRatioLockedPath.addLine(to: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioLockedPath.addLine(to: CGPoint(x: container.minX + 0.25000 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioLockedPath.addCurve(to: CGPoint(x: container.minX + 0.16667 * container.width, y: container.minY + 0.41667 * container.height), controlPoint1: CGPoint(x: container.minX + 0.20417 * container.width, y: container.minY + 0.33333 * container.height), controlPoint2: CGPoint(x: container.minX + 0.16667 * container.width, y: container.minY + 0.37083 * container.height)) pathRatioLockedPath.addLine(to: CGPoint(x: container.minX + 0.16667 * container.width, y: container.minY + 0.83333 * container.height)) pathRatioLockedPath.addCurve(to: CGPoint(x: container.minX + 0.25000 * container.width, y: container.minY + 0.91667 * container.height), controlPoint1: CGPoint(x: container.minX + 0.16667 * container.width, y: container.minY + 0.87917 * container.height), controlPoint2: CGPoint(x: container.minX + 0.20417 * container.width, y: container.minY + 0.91667 * container.height)) pathRatioLockedPath.addLine(to: CGPoint(x: container.minX + 0.75000 * container.width, y: container.minY + 0.91667 * container.height)) pathRatioLockedPath.addCurve(to: CGPoint(x: container.minX + 0.83333 * container.width, y: container.minY + 0.83333 * container.height), controlPoint1: CGPoint(x: container.minX + 0.79583 * container.width, y: container.minY + 0.91667 * container.height), controlPoint2: CGPoint(x: container.minX + 0.83333 * container.width, y: container.minY + 0.87917 * container.height)) pathRatioLockedPath.addLine(to: CGPoint(x: container.minX + 0.83333 * container.width, y: container.minY + 0.41667 * container.height)) pathRatioLockedPath.addCurve(to: CGPoint(x: container.minX + 0.75000 * container.width, y: container.minY + 0.33333 * container.height), controlPoint1: CGPoint(x: container.minX + 0.83333 * container.width, y: container.minY + 0.37083 * container.height), controlPoint2: CGPoint(x: container.minX + 0.79583 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioLockedPath.close() pathRatioLockedPath.move(to: CGPoint(x: container.minX + 0.50000 * container.width, y: container.minY + 0.70833 * container.height)) pathRatioLockedPath.addCurve(to: CGPoint(x: container.minX + 0.41667 * container.width, y: container.minY + 0.62500 * container.height), controlPoint1: CGPoint(x: container.minX + 0.45417 * container.width, y: container.minY + 0.70833 * container.height), controlPoint2: CGPoint(x: container.minX + 0.41667 * container.width, y: container.minY + 0.67083 * container.height)) pathRatioLockedPath.addCurve(to: CGPoint(x: container.minX + 0.50000 * container.width, y: container.minY + 0.54167 * container.height), controlPoint1: CGPoint(x: container.minX + 0.41667 * container.width, y: container.minY + 0.57917 * container.height), controlPoint2: CGPoint(x: container.minX + 0.45417 * container.width, y: container.minY + 0.54167 * container.height)) pathRatioLockedPath.addCurve(to: CGPoint(x: container.minX + 0.58333 * container.width, y: container.minY + 0.62500 * container.height), controlPoint1: CGPoint(x: container.minX + 0.54583 * container.width, y: container.minY + 0.54167 * container.height), controlPoint2: CGPoint(x: container.minX + 0.58333 * container.width, y: container.minY + 0.57917 * container.height)) pathRatioLockedPath.addCurve(to: CGPoint(x: container.minX + 0.50000 * container.width, y: container.minY + 0.70833 * container.height), controlPoint1: CGPoint(x: container.minX + 0.58333 * container.width, y: container.minY + 0.67083 * container.height), controlPoint2: CGPoint(x: container.minX + 0.54583 * container.width, y: container.minY + 0.70833 * container.height)) pathRatioLockedPath.close() pathRatioLockedPath.move(to: CGPoint(x: container.minX + 0.37500 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioLockedPath.addLine(to: CGPoint(x: container.minX + 0.37500 * container.width, y: container.minY + 0.25000 * container.height)) pathRatioLockedPath.addCurve(to: CGPoint(x: container.minX + 0.50000 * container.width, y: container.minY + 0.12500 * container.height), controlPoint1: CGPoint(x: container.minX + 0.37500 * container.width, y: container.minY + 0.18083 * container.height), controlPoint2: CGPoint(x: container.minX + 0.43083 * container.width, y: container.minY + 0.12500 * container.height)) pathRatioLockedPath.addCurve(to: CGPoint(x: container.minX + 0.62500 * container.width, y: container.minY + 0.25000 * container.height), controlPoint1: CGPoint(x: container.minX + 0.56917 * container.width, y: container.minY + 0.12500 * container.height), controlPoint2: CGPoint(x: container.minX + 0.62500 * container.width, y: container.minY + 0.18083 * container.height)) pathRatioLockedPath.addLine(to: CGPoint(x: container.minX + 0.62500 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioLockedPath.addLine(to: CGPoint(x: container.minX + 0.37500 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioLockedPath.close() pathRatioLockedPath.usesEvenOddFillRule = true fillColor.setFill() pathRatioLockedPath.fill() } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetImages/RatioOpenedIconInstructions.swift ================================================ // // RatioOpenedIconInstructions.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit class RatioOpenedIconInstructions : CLDImageDrawingInstructions { var targetSize : CGSize var fillColor : UIColor // MARK: - Initialization init(targetSize size: CGSize = CGSize(width: 24.0, height: 24.0), fillColor: UIColor = .white) { self.targetSize = size self.fillColor = fillColor } // MARK: - draw func draw(in context: CGContext) { let container = CGRect(origin: .zero, size: targetSize) let pathRatioOpenedPath = UIBezierPath() pathRatioOpenedPath.move(to: CGPoint(x: container.minX + 0.75000 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.25000 * container.height)) pathRatioOpenedPath.addCurve(to: CGPoint(x: container.minX + 0.50000 * container.width, y: container.minY + 0.04167 * container.height), controlPoint1: CGPoint(x: container.minX + 0.70833 * container.width, y: container.minY + 0.13500 * container.height), controlPoint2: CGPoint(x: container.minX + 0.61500 * container.width, y: container.minY + 0.04167 * container.height)) pathRatioOpenedPath.addCurve(to: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.25000 * container.height), controlPoint1: CGPoint(x: container.minX + 0.38500 * container.width, y: container.minY + 0.04167 * container.height), controlPoint2: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.13500 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.29167 * container.width, y: container.minY + 0.25000 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.37500 * container.width, y: container.minY + 0.25000 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.37500 * container.width, y: container.minY + 0.25000 * container.height)) pathRatioOpenedPath.addCurve(to: CGPoint(x: container.minX + 0.50000 * container.width, y: container.minY + 0.12500 * container.height), controlPoint1: CGPoint(x: container.minX + 0.37500 * container.width, y: container.minY + 0.18083 * container.height), controlPoint2: CGPoint(x: container.minX + 0.43083 * container.width, y: container.minY + 0.12500 * container.height)) pathRatioOpenedPath.addCurve(to: CGPoint(x: container.minX + 0.62500 * container.width, y: container.minY + 0.25000 * container.height), controlPoint1: CGPoint(x: container.minX + 0.56917 * container.width, y: container.minY + 0.12500 * container.height), controlPoint2: CGPoint(x: container.minX + 0.62500 * container.width, y: container.minY + 0.18083 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.62500 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.25000 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioOpenedPath.addCurve(to: CGPoint(x: container.minX + 0.16667 * container.width, y: container.minY + 0.41667 * container.height), controlPoint1: CGPoint(x: container.minX + 0.20417 * container.width, y: container.minY + 0.33333 * container.height), controlPoint2: CGPoint(x: container.minX + 0.16667 * container.width, y: container.minY + 0.37083 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.16667 * container.width, y: container.minY + 0.83333 * container.height)) pathRatioOpenedPath.addCurve(to: CGPoint(x: container.minX + 0.25000 * container.width, y: container.minY + 0.91667 * container.height), controlPoint1: CGPoint(x: container.minX + 0.16667 * container.width, y: container.minY + 0.87917 * container.height), controlPoint2: CGPoint(x: container.minX + 0.20417 * container.width, y: container.minY + 0.91667 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.75000 * container.width, y: container.minY + 0.91667 * container.height)) pathRatioOpenedPath.addCurve(to: CGPoint(x: container.minX + 0.83333 * container.width, y: container.minY + 0.83333 * container.height), controlPoint1: CGPoint(x: container.minX + 0.79583 * container.width, y: container.minY + 0.91667 * container.height), controlPoint2: CGPoint(x: container.minX + 0.83333 * container.width, y: container.minY + 0.87917 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.83333 * container.width, y: container.minY + 0.41667 * container.height)) pathRatioOpenedPath.addCurve(to: CGPoint(x: container.minX + 0.75000 * container.width, y: container.minY + 0.33333 * container.height), controlPoint1: CGPoint(x: container.minX + 0.83333 * container.width, y: container.minY + 0.37083 * container.height), controlPoint2: CGPoint(x: container.minX + 0.79583 * container.width, y: container.minY + 0.33333 * container.height)) pathRatioOpenedPath.close() pathRatioOpenedPath.move(to: CGPoint(x: container.minX + 0.75000 * container.width, y: container.minY + 0.83333 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.25000 * container.width, y: container.minY + 0.83333 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.25000 * container.width, y: container.minY + 0.41667 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.75000 * container.width, y: container.minY + 0.41667 * container.height)) pathRatioOpenedPath.addLine(to: CGPoint(x: container.minX + 0.75000 * container.width, y: container.minY + 0.83333 * container.height)) pathRatioOpenedPath.close() pathRatioOpenedPath.move(to: CGPoint(x: container.minX + 0.50000 * container.width, y: container.minY + 0.70833 * container.height)) pathRatioOpenedPath.addCurve(to: CGPoint(x: container.minX + 0.58333 * container.width, y: container.minY + 0.62500 * container.height), controlPoint1: CGPoint(x: container.minX + 0.54583 * container.width, y: container.minY + 0.70833 * container.height), controlPoint2: CGPoint(x: container.minX + 0.58333 * container.width, y: container.minY + 0.67083 * container.height)) pathRatioOpenedPath.addCurve(to: CGPoint(x: container.minX + 0.50000 * container.width, y: container.minY + 0.54167 * container.height), controlPoint1: CGPoint(x: container.minX + 0.58333 * container.width, y: container.minY + 0.57917 * container.height), controlPoint2: CGPoint(x: container.minX + 0.54583 * container.width, y: container.minY + 0.54167 * container.height)) pathRatioOpenedPath.addCurve(to: CGPoint(x: container.minX + 0.41667 * container.width, y: container.minY + 0.62500 * container.height), controlPoint1: CGPoint(x: container.minX + 0.45417 * container.width, y: container.minY + 0.54167 * container.height), controlPoint2: CGPoint(x: container.minX + 0.41667 * container.width, y: container.minY + 0.57917 * container.height)) pathRatioOpenedPath.addCurve(to: CGPoint(x: container.minX + 0.50000 * container.width, y: container.minY + 0.70833 * container.height), controlPoint1: CGPoint(x: container.minX + 0.41667 * container.width, y: container.minY + 0.67083 * container.height), controlPoint2: CGPoint(x: container.minX + 0.45417 * container.width, y: container.minY + 0.70833 * container.height)) pathRatioOpenedPath.close() pathRatioOpenedPath.usesEvenOddFillRule = true fillColor.setFill() pathRatioOpenedPath.fill() } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetImages/RotateIconInstructions.swift ================================================ // // RotateIconInstructions.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit class RotateIconInstructions : CLDImageDrawingInstructions { var targetSize : CGSize var fillColor : UIColor // MARK: - Initialization init(targetSize size: CGSize = CGSize(width: 24.0, height: 24.0), fillColor: UIColor = .white) { self.targetSize = size self.fillColor = fillColor } // MARK: - draw func draw(in context: CGContext) { let container = CGRect(origin: .zero, size: targetSize) let pathRotatePath = UIBezierPath() pathRotatePath.move(to: CGPoint(x: container.minX + 0.30583 * container.width, y: container.minY + 0.26708 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.03583 * container.width, y: container.minY + 0.53750 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.30625 * container.width, y: container.minY + 0.80750 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.57667 * container.width, y: container.minY + 0.53750 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.30583 * container.width, y: container.minY + 0.26708 * container.height)) pathRotatePath.close() pathRotatePath.move(to: CGPoint(x: container.minX + 0.15375 * container.width, y: container.minY + 0.53750 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.30625 * container.width, y: container.minY + 0.38500 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.45833 * container.width, y: container.minY + 0.53750 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.30583 * container.width, y: container.minY + 0.69000 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.15375 * container.width, y: container.minY + 0.53750 * container.height)) pathRotatePath.close() pathRotatePath.move(to: CGPoint(x: container.minX + 0.80667 * container.width, y: container.minY + 0.27667 * container.height)) pathRotatePath.addCurve(to: CGPoint(x: container.minX + 0.54167 * container.width, y: container.minY + 0.16667 * container.height), controlPoint1: CGPoint(x: container.minX + 0.73375 * container.width, y: container.minY + 0.20333 * container.height), controlPoint2: CGPoint(x: container.minX + 0.63750 * container.width, y: container.minY + 0.16667 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.54167 * container.width, y: container.minY + 0.03167 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.36500 * container.width, y: container.minY + 0.20833 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.54167 * container.width, y: container.minY + 0.38500 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.54167 * container.width, y: container.minY + 0.25000 * container.height)) pathRotatePath.addCurve(to: CGPoint(x: container.minX + 0.74792 * container.width, y: container.minY + 0.33542 * container.height), controlPoint1: CGPoint(x: container.minX + 0.61625 * container.width, y: container.minY + 0.25000 * container.height), controlPoint2: CGPoint(x: container.minX + 0.69083 * container.width, y: container.minY + 0.27833 * container.height)) pathRotatePath.addCurve(to: CGPoint(x: container.minX + 0.74792 * container.width, y: container.minY + 0.74792 * container.height), controlPoint1: CGPoint(x: container.minX + 0.86167 * container.width, y: container.minY + 0.44917 * container.height), controlPoint2: CGPoint(x: container.minX + 0.86167 * container.width, y: container.minY + 0.63417 * container.height)) pathRotatePath.addCurve(to: CGPoint(x: container.minX + 0.54167 * container.width, y: container.minY + 0.83333 * container.height), controlPoint1: CGPoint(x: container.minX + 0.69083 * container.width, y: container.minY + 0.80500 * container.height), controlPoint2: CGPoint(x: container.minX + 0.61625 * container.width, y: container.minY + 0.83333 * container.height)) pathRotatePath.addCurve(to: CGPoint(x: container.minX + 0.42333 * container.width, y: container.minY + 0.80792 * container.height), controlPoint1: CGPoint(x: container.minX + 0.50125 * container.width, y: container.minY + 0.83333 * container.height), controlPoint2: CGPoint(x: container.minX + 0.46083 * container.width, y: container.minY + 0.82458 * container.height)) pathRotatePath.addLine(to: CGPoint(x: container.minX + 0.36125 * container.width, y: container.minY + 0.87000 * container.height)) pathRotatePath.addCurve(to: CGPoint(x: container.minX + 0.54167 * container.width, y: container.minY + 0.91667 * container.height), controlPoint1: CGPoint(x: container.minX + 0.41750 * container.width, y: container.minY + 0.90083 * container.height), controlPoint2: CGPoint(x: container.minX + 0.47958 * container.width, y: container.minY + 0.91667 * container.height)) pathRotatePath.addCurve(to: CGPoint(x: container.minX + 0.80667 * container.width, y: container.minY + 0.80667 * container.height), controlPoint1: CGPoint(x: container.minX + 0.63750 * container.width, y: container.minY + 0.91667 * container.height), controlPoint2: CGPoint(x: container.minX + 0.73375 * container.width, y: container.minY + 0.88000 * container.height)) pathRotatePath.addCurve(to: CGPoint(x: container.minX + 0.80667 * container.width, y: container.minY + 0.27667 * container.height), controlPoint1: CGPoint(x: container.minX + 0.95333 * container.width, y: container.minY + 0.66042 * container.height), controlPoint2: CGPoint(x: container.minX + 0.95333 * container.width, y: container.minY + 0.42292 * container.height)) pathRotatePath.close() pathRotatePath.usesEvenOddFillRule = true fillColor.setFill() pathRotatePath.fill() } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetVideo/CLDDisplayLinkObserver.swift ================================================ // // CLDDisplayLinkObservers.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit protocol CLDDisplayLinkObserverDelegate: AnyObject { func displayLinkObserverDidTick(_ linkObserver: CLDDisplayLinkObserver) } class CLDDisplayLinkObserver: NSObject { private(set) var tickerTimestamp : CFTimeInterval private(set) var displayLinkTicker: CADisplayLink? var delayValue : Double weak var delegate: CLDDisplayLinkObserverDelegate? // MARK: - Init init(delegate: CLDDisplayLinkObserverDelegate?) { self.delayValue = 0.0 self.tickerTimestamp = CFTimeInterval(0.0) self.displayLinkTicker = nil super.init() self.delegate = delegate } deinit { stopTicker() } // MARK: - private methods @objc fileprivate func updateTicker(with displayLink: CADisplayLink) { guard tickerTimestamp != 0.0 else { tickerTimestamp = displayLink.timestamp ; return } let delta = displayLink.timestamp - tickerTimestamp guard delta >= delayValue else { return } delegate?.displayLinkObserverDidTick(self) tickerTimestamp = displayLink.timestamp } } // MARK: - internal methods extension CLDDisplayLinkObserver { func startTicker() { guard displayLinkTicker == nil else { return } let displayLink = CADisplayLink(target: self, selector: #selector(updateTicker(with:))) displayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.common) displayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.tracking) displayLinkTicker = displayLink } func stopTicker() { displayLinkTicker?.remove(from: RunLoop.main, forMode: RunLoop.Mode.common) displayLinkTicker?.remove(from: RunLoop.main, forMode: RunLoop.Mode.tracking) displayLinkTicker?.invalidate() displayLinkTicker = nil tickerTimestamp = 0.0 } func isValid() -> Bool { return displayLinkTicker != nil } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetVideo/CLDVideoControlsState.swift ================================================ // // CLDVideoControlsState.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation protocol CLDVideoControlsState: AnyObject { var controlsView: CLDVideoControlsView { get set } init(controlsView: CLDVideoControlsView) func playPausePressed() func backgroundPressed() func timerFinished() func videoEnded() } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetVideo/CLDVideoControlsView.swift ================================================ // // CLDVideoControlsView.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit protocol CLDVideoControlsViewDelegate: AnyObject { func playPressedOnVideoControls (_ videoControls: CLDVideoControlsView) func pausePressedOnVideoControls(_ videoControls: CLDVideoControlsView) } class CLDVideoControlsView: UIControl { private(set) var playPauseButton : UIButton! private(set) var visibilityTimer : CLDDisplayLinkObserver! weak var delegate : CLDVideoControlsViewDelegate? private(set) var currentState : CLDVideoControlsState! private(set) var shownAndPlayingState : CLDVideoControlsState! private(set) var shownAndPausedState : CLDVideoControlsState! private(set) var hiddenAndPlayingState: CLDVideoControlsState! private(set) var hiddenAndPausedState : CLDVideoControlsState! private let transparentBackgroundColor = UIColor.black.withAlphaComponent(0.5) private let controlTransitionsDuration = 0.2 private let controlAppearanceDuration = 2.5 // MARK: - init init(frame: CGRect, delegate: CLDVideoControlsViewDelegate?) { self.delegate = delegate super.init(frame: frame) // handle state shownAndPlayingState = CLDVideoShownAndPlayingState (controlsView: self) shownAndPausedState = CLDVideoShownAndPausedState (controlsView: self) hiddenAndPlayingState = CLDVideoHiddenAndPlayingState(controlsView: self) hiddenAndPausedState = CLDVideoHiddenAndPausedState (controlsView: self) currentState = shownAndPlayingState // initial timer state visibilityTimer = CLDDisplayLinkObserver(delegate: self) startTimer() addTarget(self, action: #selector(backgroundPressed), for: .touchUpInside) createUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func resetControls() { currentState = shownAndPlayingState showControls() playVideo() } } // MARK: - handle events extension CLDVideoControlsView { @objc private func backgroundPressed() { currentState.backgroundPressed() } @objc private func playPausePressed() { currentState.playPausePressed() } func videoEnded() { currentState.videoEnded() } func pauseVideo() { delegate?.pausePressedOnVideoControls(self) playPauseButton.setTitle("▶", for: .normal) stopTimer() } func playVideo() { delegate?.playPressedOnVideoControls(self) playPauseButton.setTitle("||", for: .normal) startTimer() } } // MARK: - handle state extension CLDVideoControlsView { func setNewState(newState: CLDVideoControlsState) { currentState = newState } } // MARK: - display link timer extension CLDVideoControlsView: CLDDisplayLinkObserverDelegate { func startTimer() { visibilityTimer.stopTicker() visibilityTimer.delayValue = controlAppearanceDuration visibilityTimer.startTicker() } func stopTimer() { visibilityTimer.stopTicker() } func displayLinkObserverDidTick(_ linkObserver: CLDDisplayLinkObserver) { currentState.timerFinished() } } // MARK: - UI extension CLDVideoControlsView { private func createUI() { backgroundColor = transparentBackgroundColor playPauseButton = UIButton(type: .custom) playPauseButton.accessibilityIdentifier = "videoViewPausePlayButton" playPauseButton.setTitle("||", for: .normal) playPauseButton.addTarget(self, action: #selector(playPausePressed), for: .touchUpInside) playPauseButton.titleLabel?.font = playPauseButton.titleLabel?.font.withSize(40) addSubview(playPauseButton) playPauseButton.cld_addConstraintsToCenter(self) } func showControls() { UIView.animate(withDuration: controlTransitionsDuration, animations: { self.backgroundColor = self.transparentBackgroundColor self.playPauseButton.alpha = 1.0 }) } func hideControls() { // we dont set the background alpha to 0 in order to keep getting touch events UIView.animate(withDuration: controlTransitionsDuration, animations: { self.backgroundColor = UIColor.clear self.playPauseButton.alpha = 0.0 }) } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetVideo/CLDVideoHiddenAndPausedState.swift ================================================ // // CLDVideoHiddenAndPausedState.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation class CLDVideoHiddenAndPausedState: NSObject, CLDVideoControlsState { var controlsView: CLDVideoControlsView required init(controlsView: CLDVideoControlsView) { self.controlsView = controlsView super.init() } func playPausePressed() { print("playPausePressed shouldn't be called in CLDVideoHiddenAndPausedState") } func backgroundPressed() { controlsView.setNewState(newState: controlsView.shownAndPausedState) controlsView.stopTimer() controlsView.showControls() } func timerFinished() { print("timerFinished shouldn't be called in CLDVideoHiddenAndPausedState") } func videoEnded() { print("videoEnded shouldn't be called in CLDVideoHiddenAndPausedState") } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetVideo/CLDVideoHiddenAndPlayingState.swift ================================================ // // CLDVideoHiddenAndPlayingState.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation class CLDVideoHiddenAndPlayingState: NSObject, CLDVideoControlsState { var controlsView: CLDVideoControlsView required init(controlsView: CLDVideoControlsView) { self.controlsView = controlsView super.init() } func playPausePressed() { print("playPausePressed shouldn't be called in CLDVideoHiddenAndPlayingState") } func backgroundPressed() { controlsView.setNewState(newState: controlsView.shownAndPlayingState) controlsView.startTimer() controlsView.showControls() } func timerFinished() { print("timerFinished shouldn't be called in CLDVideoHiddenAndPlayingState") } func videoEnded() { controlsView.pauseVideo() controlsView.setNewState(newState: controlsView.shownAndPausedState) controlsView.showControls() } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetVideo/CLDVideoPlayerView.swift ================================================ // // CLDVideoPlayerView.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit import AVKit class CLDVideoPlayerView: UIView { override class var layerClass: AnyClass { return AVPlayerLayer.self } var player : AVPlayer? { get { return playerLayer?.player } set { playerLayer?.player = newValue } } weak var playerLayer : AVPlayerLayer? { guard let layer = layer as? AVPlayerLayer else { return nil } return layer as AVPlayerLayer } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetVideo/CLDVideoShownAndPausedState.swift ================================================ // // CLDVideoShownAndPausedState.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation class CLDVideoShownAndPausedState: NSObject, CLDVideoControlsState { var controlsView: CLDVideoControlsView required init(controlsView: CLDVideoControlsView) { self.controlsView = controlsView super.init() } func playPausePressed() { controlsView.playVideo() controlsView.setNewState(newState: controlsView.shownAndPlayingState) } func backgroundPressed() { controlsView.setNewState(newState: controlsView.hiddenAndPausedState) controlsView.hideControls() } func timerFinished() { print("timerFinished shouldn't be called in CLDVideoShownAndPausedState") } func videoEnded() { print("videoEnded shouldn't be called in CLDVideoShownAndPausedState") } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetVideo/CLDVideoShownAndPlayingState.swift ================================================ // // CLDVideoShownAndPlayingState.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation class CLDVideoShownAndPlayingState: NSObject, CLDVideoControlsState { var controlsView: CLDVideoControlsView required init(controlsView: CLDVideoControlsView) { self.controlsView = controlsView super.init() } func playPausePressed() { controlsView.pauseVideo() controlsView.setNewState(newState: controlsView.shownAndPausedState) } func backgroundPressed() { hideControls() } func timerFinished() { hideControls() } private func hideControls() { controlsView.stopTimer() controlsView.setNewState(newState: controlsView.hiddenAndPlayingState) controlsView.hideControls() } func videoEnded() { controlsView.pauseVideo() controlsView.setNewState(newState: controlsView.shownAndPausedState) } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetVideo/CLDVideoView.swift ================================================ // // CLDVideoView.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit import AVKit class CLDVideoView: UIView { private(set) var videoControlsView: CLDVideoControlsView! private(set) var videoPlayerView : CLDVideoPlayerView! private(set) var player : AVPlayer /** Initializes the CLDVideoView - parameter frame: The view's frame - parameter player: The player for the presented video - returns: A new CLDVideoView instance. */ init(frame: CGRect, playerItem: AVPlayerItem?, isMuted: Bool) { self.player = AVPlayer(playerItem: playerItem) self.player.isMuted = isMuted super.init(frame: frame) createUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } /** Plays a new video and adjust the controls - parameter item: The new AVPlayerItem to play */ func replaceCurrentItem(with item: AVPlayerItem) { player.seek(to: CMTime.zero) player.replaceCurrentItem(with: item) videoControlsView.resetControls() } /** Pausing the video player */ func pauseVideo() { player.pause() } } // MARK: - CLDVideoControlsViewDelegate extension CLDVideoView: CLDVideoControlsViewDelegate { func playPressedOnVideoControls(_ videoControls: CLDVideoControlsView) { player.play() } func pausePressedOnVideoControls(_ videoControls: CLDVideoControlsView) { player.pause() } } // MARK: - create UI extension CLDVideoView { private func createUI() { videoControlsView = CLDVideoControlsView(frame: frame, delegate: self) videoControlsView.accessibilityIdentifier = "videoControlsView" videoPlayerView = CLDVideoPlayerView (frame: frame) videoPlayerView.accessibilityIdentifier = "videoPlayerView" addSubview(videoPlayerView) addSubview(videoControlsView) videoPlayerView .cld_addConstraintsToFill(self) videoControlsView.cld_addConstraintsToFill(self) guard let playerLayer = videoPlayerView.playerLayer else { return } playerLayer.player = player NotificationCenter.default.addObserver(self, selector: #selector(videoEnded), name: .AVPlayerItemDidPlayToEndTime, object: nil) player.play() } @objc private func videoEnded() { player.seek(to: CMTime.zero) videoControlsView.videoEnded() } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetViewControllers/CLDWidgetEditViewController.swift ================================================ // // CLDWidgetEditViewController.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit internal protocol CLDWidgetEditDelegate: AnyObject { func widgetEditViewController(_ controller: CLDWidgetEditViewController, didFinishEditing image: CLDWidgetAssetContainer) func widgetEditViewControllerDidReset (_ controller: CLDWidgetEditViewController) func widgetEditViewControllerDidCancel(_ controller: CLDWidgetEditViewController) } internal class CLDWidgetEditViewController: UIViewController { private(set) var buttonsView : UIView! private(set) var cancelButton: UIButton! private(set) var rotateButton: UIButton! private(set) var doneButton : UIButton! private(set) var cropView : CLDCropView! private(set) var image : CLDWidgetAssetContainer private(set) var configuration : CLDWidgetConfiguration? private(set) var initialAspectLockState: CLDWidgetConfiguration.AspectRatioLockState internal weak var delegate : CLDWidgetEditDelegate? private var firstTime : Bool = true private let bottomCropViewPadding : CGFloat = 10 private let bottomButtonsViewPadding: CGFloat = 10 // MARK: - init init( image: CLDWidgetAssetContainer, configuration: CLDWidgetConfiguration? = nil, delegate: CLDWidgetEditDelegate? = nil, initialAspectLockState: CLDWidgetConfiguration.AspectRatioLockState = .enabledAndOff ) { self.image = image self.configuration = configuration self.delegate = delegate self.initialAspectLockState = initialAspectLockState super.init(nibName: nil, bundle: nil) if #available(iOS 11.0, *) {} else { automaticallyAdjustsScrollViewInsets = false } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { super.loadView() createUI() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() adjustCropViewLocation(cropView) if firstTime { firstTime = false cropView.performInitialSetup() } } } // MARK: - crop view extension CLDWidgetEditViewController { private func frameForCropView() -> CGRect { let bounds = view.bounds var frame = CGRect.zero frame.size.height = bounds.height - buttonsView.frame.height - bottomButtonsViewPadding - bottomCropViewPadding frame.size.width = bounds.width return frame } func aspectRatioLockPressed() { cropView.aspectRatioLockEnabled.toggle() if cropView.aspectRatioLockEnabled { cropView.lockAspectRatio(to: cropView.cropBoxFrame.size) } } } // MARK: - actions private extension CLDWidgetEditViewController { @objc func resetPressed(sender: UIButton) { image.editedImage = image.originalImage // crete a new crop view with original image let oldView = cropView let newView = createCropView(for: image.originalImage!, aspectLockState: initialAspectLockState == .enabledAndOn) view.insertSubview(newView, belowSubview: cropView) adjustCropViewLocation(newView) newView.performInitialSetup() newView.alpha = 0.0 cropView = newView UIView.animateKeyframes(withDuration: 1, delay: 0.0, options: [], animations: { UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.4) { oldView?.alpha = 0.0 } UIView.addKeyframe(withRelativeStartTime: 0.6, relativeDuration: 0.4) { newView.alpha = 1.0 } }) { (success) in oldView?.isHidden = true oldView?.removeFromSuperview() } cropView.aspectRatioLockEnabled = false delegate?.widgetEditViewControllerDidReset(self) } @objc func rotatePressed(sender: UIButton) { cropView.rotateImageNinetyDegreesAnimated(true) } @objc func donePressed(sender: UIButton) { let cropFrame = cropView.imageCropFrame let angle = cropView.angle let editedImage: CLDWidgetAssetContainer = image // check if image edited if angle != 0 || !cropFrame.equalTo(CGRect(origin: CGPoint.zero, size: image.editedImage!.size)) { let newImage = image.editedImage?.cld_cropImage(to: cropFrame, angle: angle) editedImage.editedImage = newImage if let presentationImage = newImage { editedImage.presentationImage = presentationImage } } delegate?.widgetEditViewController(self, didFinishEditing: editedImage) } } extension UIImage { func cld_hasAlpha() -> Bool { guard let cgimage = self.cgImage else { return false } let alphaInfo = cgimage.alphaInfo return (alphaInfo == .first || alphaInfo == .last || alphaInfo == .premultipliedFirst || alphaInfo == .premultipliedLast) } func cld_cropImage(to frame: CGRect, angle: NSInteger) -> UIImage { var croppedImage: UIImage? UIGraphicsBeginImageContextWithOptions(frame.size, !cld_hasAlpha(), scale) let context = UIGraphicsGetCurrentContext() //To conserve memory in not needing to completely re-render the image re-rotated, //map the image to a view and then use Core Animation to manipulate its rotation if (angle != 0) { let imageView = UIImageView(image: self) imageView.layer.minificationFilter = .nearest imageView.layer.magnificationFilter = .nearest imageView.transform = CGAffineTransform.identity.rotated(by: CGFloat(angle) * (CGFloat(Double.pi)/180.0)) let rotatedRect = imageView.bounds.applying(imageView.transform) let containerView = UIView(frame: CGRect(origin: .zero, size: rotatedRect.size)) containerView.addSubview(imageView) imageView.center = containerView.center context?.translateBy(x: -frame.origin.x, y: -frame.origin.y) containerView.layer.render(in: context!) } else { context?.translateBy(x: -frame.origin.x, y: -frame.origin.y) draw(at: .zero) } croppedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() if let croppedCGImage = croppedImage?.cgImage { return UIImage(cgImage:croppedCGImage , scale: scale, orientation: .up) } else { return self } } } // MARK: - create UI private extension CLDWidgetEditViewController { var buttonsViewHeight: CGFloat { return 70 } // MARK: - methods func createUI() { view.backgroundColor = .black createAllSubviews() addAllSubviews() addAllConstraints() configureScreen() } func createCropView(for image: UIImage, aspectLockState: Bool) -> CLDCropView { let aView = CLDCropView(image: image) aView.autoresizingMask = [.flexibleWidth, .flexibleHeight] aView.aspectRatioLockEnabled = aspectLockState return aView } func createAllSubviews() { buttonsView = UIView() cropView = createCropView(for: image.editedImage!, aspectLockState: initialAspectLockState == .enabledAndOn) cancelButton = UIButton(type: .custom) cancelButton.setTitle("Reset", for: .normal) cancelButton.addTarget(self, action: #selector(resetPressed), for: .touchUpInside) let buttonImage = CLDImageGenerator.generateImage(from: RotateIconInstructions()) rotateButton = UIButton(type: .custom) rotateButton.accessibilityIdentifier = "editViewControllerRotateButton" rotateButton.setImage(buttonImage, for: .normal) rotateButton.addTarget(self, action: #selector(rotatePressed), for: .touchUpInside) doneButton = UIButton(type: .custom) doneButton.setTitle("Done", for: .normal) doneButton.addTarget(self, action: #selector(donePressed), for: .touchUpInside) } func adjustCropViewLocation(_ aView: CLDCropView) { aView.frame = frameForCropView() aView.cropRegionInsets = UIEdgeInsets.zero aView.moveCroppedContentToCenterAnimated(false) } func addAllSubviews() { view.addSubview(cropView) view.addSubview(buttonsView) buttonsView.addSubview(cancelButton) buttonsView.addSubview(rotateButton) buttonsView.addSubview(doneButton) } func addAllConstraints() { buttonsView .translatesAutoresizingMaskIntoConstraints = false cancelButton.translatesAutoresizingMaskIntoConstraints = false rotateButton.translatesAutoresizingMaskIntoConstraints = false doneButton .translatesAutoresizingMaskIntoConstraints = false var buttonsViewConstraints = [ NSLayoutConstraint(item: buttonsView!, attribute: .leading , relatedBy: .equal, toItem: view, attribute: .leading , multiplier: 1, constant: 0), NSLayoutConstraint(item: buttonsView!, attribute: .trailing , relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0), ] let bottomConstraint: NSLayoutConstraint if #available(iOS 11.0, *) { bottomConstraint = buttonsView!.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -bottomButtonsViewPadding) } else { bottomConstraint = NSLayoutConstraint(item: buttonsView!, attribute: .bottom , relatedBy: .equal, toItem: bottomLayoutGuide, attribute: .top, multiplier: 1, constant: -bottomButtonsViewPadding) } buttonsViewConstraints.append(bottomConstraint) buttonsViewConstraints.append(NSLayoutConstraint(item: buttonsView!, attribute: .height , relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: buttonsViewHeight)) NSLayoutConstraint.activate(buttonsViewConstraints) let cancelButtonConstraints = [ NSLayoutConstraint(item: cancelButton!, attribute: .leading , relatedBy: .equal, toItem: buttonsView , attribute: .leading, multiplier: 1, constant: 0), NSLayoutConstraint(item: cancelButton!, attribute: .trailing, relatedBy: .equal, toItem: rotateButton, attribute: .leading, multiplier: 1, constant: 0), NSLayoutConstraint(item: cancelButton!, attribute: .top , relatedBy: .equal, toItem: buttonsView , attribute: .top , multiplier: 1, constant: 0), NSLayoutConstraint(item: cancelButton!, attribute: .bottom , relatedBy: .equal, toItem: buttonsView , attribute: .bottom , multiplier: 1, constant: 0) ] NSLayoutConstraint.activate(cancelButtonConstraints) let rotateButtonConstraints = [ NSLayoutConstraint(item: rotateButton!, attribute: .trailing, relatedBy: .equal, toItem: doneButton , attribute: .leading, multiplier: 1, constant: 0), NSLayoutConstraint(item: rotateButton!, attribute: .top , relatedBy: .equal, toItem: buttonsView , attribute: .top , multiplier: 1, constant: 0), NSLayoutConstraint(item: rotateButton!, attribute: .height , relatedBy: .equal, toItem: cancelButton, attribute: .height , multiplier: 1, constant: 0), NSLayoutConstraint(item: rotateButton!, attribute: .width , relatedBy: .equal, toItem: cancelButton, attribute: .width , multiplier: 1, constant: 0) ] NSLayoutConstraint.activate(rotateButtonConstraints) let doneButtonConstraints = [ NSLayoutConstraint(item: doneButton!, attribute: .trailing, relatedBy: .equal, toItem: buttonsView , attribute: .trailing, multiplier: 1, constant: 0), NSLayoutConstraint(item: doneButton!, attribute: .top , relatedBy: .equal, toItem: buttonsView , attribute: .top , multiplier: 1, constant: 0), NSLayoutConstraint(item: doneButton!, attribute: .height , relatedBy: .equal, toItem: cancelButton, attribute: .height , multiplier: 1, constant: 0), NSLayoutConstraint(item: doneButton!, attribute: .width , relatedBy: .equal, toItem: cancelButton, attribute: .width , multiplier: 1, constant: 0) ] NSLayoutConstraint.activate(doneButtonConstraints) } func configureScreen() { guard let configuration = configuration else { return } if !configuration.allowRotate { rotateButton.isEnabled = false rotateButton.isHidden = true } } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetViewControllers/CLDWidgetPreviewViewController.swift ================================================ // // CLDWidgetPreviewViewController.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit import AVKit protocol CLDWidgetPreviewDelegate: AnyObject { func widgetPreviewViewController(_ controller: CLDWidgetPreviewViewController, didFinishEditing assets: [CLDWidgetAssetContainer]) func widgetPreviewViewController(_ controller: CLDWidgetPreviewViewController, didSelect asset: CLDWidgetAssetContainer) func widgetPreviewViewControllerDidCancel(_ controller: CLDWidgetPreviewViewController) } class CLDWidgetPreviewViewController: UIViewController { private(set) var collectionView: UICollectionView! private(set) var mainImageView : UIImageView! private(set) var uploadButton : UIButton! private(set) var videoView : CLDVideoView! private(set) var assets : [CLDWidgetAssetContainer] internal weak var delegate : CLDWidgetPreviewDelegate? private(set) var selectedIndex : Int // MARK: - init init(assets: [CLDWidgetAssetContainer], delegate: CLDWidgetPreviewDelegate? = nil) { self.assets = assets self.delegate = delegate self.selectedIndex = 0 super.init(nibName: nil, bundle: nil) modalPresentationStyle = .fullScreen } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { super.loadView() createUI() } func selectedImageEdited(newImage: CLDWidgetAssetContainer) { assets[selectedIndex] = newImage collectionView.reloadItems(at: [IndexPath(row: selectedIndex, section: 0)]) mainImageView.image = newImage.editedImage } func selectedImage() -> CLDWidgetAssetContainer { return assets[selectedIndex] } // MARK: - action @objc private func uploadPressed(sender: UIButton) { delegate?.widgetPreviewViewController(self, didFinishEditing: assets) } } // MARK: - UICollectionViewDataSource extension CLDWidgetPreviewViewController: UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return assets.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CLDWidgetPreviewCollectionCell cell.imageView.image = assets[indexPath.row].presentationImage return cell } } // MARK: - UICollectionViewDelegate extension CLDWidgetPreviewViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard selectedIndex != indexPath.row else { return } selectedIndex = indexPath.row let selectedAsset = assets[selectedIndex] switch selectedAsset.assetType { case .video: videoSelected(video: selectedAsset.originalVideo!) case .image: imageSelected(image: selectedAsset.editedImage!) } delegate?.widgetPreviewViewController(self, didSelect: selectedAsset) } } // MARK: - present asset extension CLDWidgetPreviewViewController { var videoTransitionDelay : TimeInterval { 0.1 } var videoTransitionDuration: TimeInterval { 0.2 } func videoSelected(video: AVPlayerItem) { UIView.animate(withDuration: videoTransitionDuration, animations: { self.videoView.alpha = 0.0 self.mainImageView.alpha = 0.0 }) { (complete) in self.videoView.replaceCurrentItem(with: video) UIView.animate(withDuration: self.videoTransitionDuration, delay: self.videoTransitionDelay, options: .curveEaseInOut, animations: { self.videoView.alpha = 1 }, completion: nil) } } func imageSelected(image: UIImage) { videoView.pauseVideo() mainImageView.image = image showImageView() } func showVideoView() { UIView.animate(withDuration: 0.4) { self.videoView.alpha = 1.0 self.mainImageView.alpha = 0.0 } } func showImageView() { UIView.animate(withDuration: 0.4) { self.videoView.alpha = 0.0 self.mainImageView.alpha = 1.0 } } } // MARK: - create UI private extension CLDWidgetPreviewViewController { var cellId : String { return "CLDWidgetPreviewCollectionCell" } var collectionCellSpacing: CGFloat { return 2 } var collectionHeight : CGFloat { return 70 } var buttonSize : CGSize { return CGSize(width: 80, height: 80) } var sectionInsets : UIEdgeInsets { return UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 5.0) } var collectionCellSize : CGSize { return CGSize(width: collectionHeight * 0.9, height: collectionHeight) } // MARK: - private methods func createUI() { view.backgroundColor = .black createAllSubviews() addAllSubviews() addAllConstraints() // initial asset presentation state switch assets[0].assetType { case .video: mainImageView.alpha = 0 case .image: videoView.alpha = 0 } } func createAllSubviews() { // collectionView let layout = UICollectionViewFlowLayout() layout.sectionInset = sectionInsets layout.minimumLineSpacing = collectionCellSpacing layout.minimumInteritemSpacing = collectionCellSpacing layout.itemSize = collectionCellSize layout.scrollDirection = .horizontal collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) collectionView.allowsSelection = true collectionView.allowsMultipleSelection = false collectionView.delegate = self collectionView.dataSource = self collectionView.register(CLDWidgetPreviewCollectionCell.self, forCellWithReuseIdentifier: cellId) // imageView mainImageView = UIImageView(image: assets[0].editedImage) mainImageView.contentMode = .scaleAspectFit // videoView let playerItem = assets[0].originalVideo videoView = CLDVideoView(frame: CGRect.zero, playerItem: playerItem, isMuted: true) videoView.accessibilityIdentifier = "previewViewControllerVideoView" // upload button let buttonImage = CLDImageGenerator.generateImage(from: DoneIconInstructions()) uploadButton = UIButton(type: .custom) uploadButton.setTitle(String(), for: .normal) uploadButton.setImage(buttonImage, for: .normal) uploadButton.addTarget(self, action: #selector(uploadPressed), for: .touchUpInside) } func addAllSubviews() { view.addSubview(mainImageView) view.addSubview(collectionView) view.addSubview(videoView) view.addSubview(uploadButton) } func addAllConstraints() { mainImageView .translatesAutoresizingMaskIntoConstraints = false collectionView.translatesAutoresizingMaskIntoConstraints = false uploadButton .translatesAutoresizingMaskIntoConstraints = false videoView .translatesAutoresizingMaskIntoConstraints = false // collectionView var collectionConstraints = [ NSLayoutConstraint(item: collectionView!, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 0), NSLayoutConstraint(item: collectionView!, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0), NSLayoutConstraint(item: collectionView!, attribute: .top, relatedBy: .equal, toItem: mainImageView, attribute: .bottom, multiplier: 1, constant: 5) ] let bottomConstraint: NSLayoutConstraint if #available(iOS 11.0, *) { bottomConstraint = collectionView!.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10) } else { bottomConstraint = NSLayoutConstraint(item: collectionView!, attribute: .bottom , relatedBy: .equal, toItem: bottomLayoutGuide, attribute: .top, multiplier: 1, constant: -10) } collectionConstraints.append(bottomConstraint) collectionConstraints.append(NSLayoutConstraint(item: collectionView!, attribute: .height , relatedBy : .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: collectionHeight)) NSLayoutConstraint.activate(collectionConstraints) // imageView var imageViewConstraints = [ NSLayoutConstraint(item: mainImageView!, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 0), NSLayoutConstraint(item: mainImageView!, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0), ] if #available(iOS 11.0, *) { imageViewConstraints.append(mainImageView!.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0)) } else { imageViewConstraints.append(NSLayoutConstraint(item: mainImageView!, attribute: .top , relatedBy: .equal, toItem: topLayoutGuide, attribute: .bottom, multiplier: 1, constant: 0)) } NSLayoutConstraint.activate(imageViewConstraints) // videoView let videoViewConstraints = [ NSLayoutConstraint(item: videoView!, attribute: .leading , relatedBy: .equal, toItem: mainImageView, attribute: .leading , multiplier: 1, constant: 0), NSLayoutConstraint(item: videoView!, attribute: .trailing, relatedBy: .equal, toItem: mainImageView, attribute: .trailing, multiplier: 1, constant: 0), NSLayoutConstraint(item: videoView!, attribute: .top , relatedBy: .equal, toItem: mainImageView, attribute: .top , multiplier: 1, constant: 0), NSLayoutConstraint(item: videoView!, attribute: .bottom , relatedBy: .equal, toItem: mainImageView, attribute: .bottom , multiplier: 1, constant: 0) ] NSLayoutConstraint.activate(videoViewConstraints) // uploadButton let uploadButtonConstraints = [ NSLayoutConstraint(item: uploadButton!, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing , multiplier: 1, constant: 0), NSLayoutConstraint(item: uploadButton!, attribute: .width , relatedBy: .equal, toItem: nil , attribute: .notAnAttribute , multiplier: 1, constant: buttonSize.width), NSLayoutConstraint(item: uploadButton!, attribute: .height , relatedBy: .equal, toItem: nil , attribute: .notAnAttribute , multiplier: 1, constant: buttonSize.height), NSLayoutConstraint(item: uploadButton!, attribute: .bottom , relatedBy: .equal, toItem: mainImageView, attribute: .bottom, multiplier: 1, constant: 0) ] NSLayoutConstraint.activate(uploadButtonConstraints) } } ================================================ FILE: Cloudinary/Classes/Core/Features/UploadWidget/WidgetViewControllers/CLDWidgetViewController.swift ================================================ // // CLDWidgetViewController.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit internal protocol CLDWidgetViewControllerDelegate: AnyObject { func widgetViewController(_ controller: CLDWidgetViewController, didFinishEditing editedAssets: [CLDWidgetAssetContainer]) func widgetViewControllerDidCancel(_ controller: CLDWidgetViewController) } internal class CLDWidgetViewController: UIViewController { private(set) var configuration : CLDWidgetConfiguration? private(set) var assets : [CLDWidgetAssetContainer] internal weak var delegate : CLDWidgetViewControllerDelegate? private(set) var topButtonsView : UIView! private(set) var backButton : UIButton! private(set) var actionButton : UIButton! private(set) var containerView : UIView! private(set) var previewViewController : CLDWidgetPreviewViewController! private(set) var editViewController : CLDWidgetEditViewController! private(set) var editIsPresented : Bool private var currentAspectLockState: CLDWidgetConfiguration.AspectRatioLockState private lazy var previewActionButtonTitle = NSAttributedString(string: "edit ", withSuffix: CLDImageGenerator.generateImage(from: CropRotateIconInstructions())) private lazy var editActionButtonLockedTitle = NSAttributedString(string: "Aspect ratio locked ", withSuffix: CLDImageGenerator.generateImage(from: RatioLockedIconInstructions())) private lazy var editActionButtonUnlockedTitle = NSAttributedString(string: "Aspect ratio unlocked ", withSuffix: CLDImageGenerator.generateImage(from: RatioOpenedIconInstructions())) private lazy var editActionButtonEmptyTitle = NSAttributedString(string: String()) private lazy var transitionDuration = 0.5 // MARK: - init internal init( assets : [CLDWidgetAssetContainer], configuration: CLDWidgetConfiguration? = nil, delegate : CLDWidgetViewControllerDelegate? = nil ) { self.configuration = configuration self.assets = assets self.delegate = delegate self.editIsPresented = false self.currentAspectLockState = .enabledAndOff super.init(nibName: nil, bundle: nil) if let initialAspectLockState = configuration?.initialAspectLockState { currentAspectLockState = initialAspectLockState } previewViewController = CLDWidgetPreviewViewController(assets: assets, delegate: self) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { super.loadView() createUI() } } // MARK: - private methods private extension CLDWidgetViewController { func moveToPreviewScreen() { editIsPresented = false transition(from: editViewController, to: previewViewController, inView: containerView) updateActionButton(by: .goToEditScreen) } func moveToEditScreen(with image: CLDWidgetAssetContainer) { editIsPresented = true editViewController = nil editViewController = CLDWidgetEditViewController(image: image, configuration: configuration, delegate: self, initialAspectLockState: currentAspectLockState) transition(from: previewViewController, to: editViewController, inView: containerView) updateActionButton(by: currentAspectLockState) } } // MARK: - CLDWidgetPreviewDelegate extension CLDWidgetViewController: CLDWidgetPreviewDelegate { func widgetPreviewViewController(_ controller: CLDWidgetPreviewViewController, didFinishEditing assets: [CLDWidgetAssetContainer]) { delegate?.widgetViewController(self, didFinishEditing: assets) } func widgetPreviewViewControllerDidCancel(_ controller: CLDWidgetPreviewViewController) { delegate?.widgetViewControllerDidCancel(self) } func widgetPreviewViewController(_ controller: CLDWidgetPreviewViewController, didSelect asset: CLDWidgetAssetContainer) { changeActionButtonState(by: asset) } func changeActionButtonState(by asset: CLDWidgetAssetContainer) { asset.assetType == .image ? updateActionButton(by: .goToEditScreen) : updateActionButton(by: .goToEditScreenDisabled) } } // MARK: - CLDWidgetEditDelegate extension CLDWidgetViewController: CLDWidgetEditDelegate { func widgetEditViewController(_ controller: CLDWidgetEditViewController, didFinishEditing image: CLDWidgetAssetContainer) { previewViewController.selectedImageEdited(newImage: image) moveToPreviewScreen() } func widgetEditViewControllerDidReset(_ controller: CLDWidgetEditViewController) { if currentAspectLockState != .disabled { currentAspectLockState = .enabledAndOff updateActionButton(by: .enabledAndOff) } } func widgetEditViewControllerDidCancel(_ controller: CLDWidgetEditViewController) { moveToPreviewScreen() } } // MARK: - child presentation private extension CLDWidgetViewController { func transition(from oldViewController: UIViewController, to newViewController: UIViewController, inView containerView: UIView) { // invoke viewWillAppear()... newViewController.beginAppearanceTransition(true, animated: true) oldViewController.beginAppearanceTransition(false, animated: true) // check if its already self child if newViewController.parent != self { addChild(newViewController) newViewController.didMove(toParent: self) } // adding newVC.view to fill containerView newViewController.view.removeFromSuperview() containerView.addSubview(newViewController.view) newViewController.view.cld_addConstraintsToFill(containerView) // prepare for animation newViewController.view.alpha = 0 containerView.bringSubviewToFront(newViewController.view) newViewController.view.layoutIfNeeded() // animate UIView.animate(withDuration: transitionDuration, delay: 0, options: [], animations: { newViewController.view.alpha = 1 oldViewController.view.alpha = 0 }) { (complete) in newViewController.endAppearanceTransition() oldViewController.endAppearanceTransition() oldViewController.view.removeFromSuperview() } } func presentAsChild(_ viewController: UIViewController, inView containerView: UIView) { viewController.beginAppearanceTransition(true, animated: true) if viewController.parent != self { addChild(viewController) viewController.didMove(toParent: self) } viewController.view.removeFromSuperview() containerView.addSubview(viewController.view) viewController.view.cld_addConstraintsToFill(containerView) containerView.bringSubviewToFront(viewController.view) viewController.view.layoutIfNeeded() viewController.endAppearanceTransition() } } // MARK: - top buttons private extension CLDWidgetViewController { enum ActionButtonState: Int { case aspectRatioEnabledAndOff case aspectRatioEnabledAndOn case aspectRatioDisabled case goToEditScreen case goToEditScreenDisabled } @objc func actionPressed(_ sender: UIButton) { if editIsPresented { toggleAspectRatioLockStates() editViewController.aspectRatioLockPressed() } else { moveToEditScreen(with: previewViewController.selectedImage()) } } @objc func backPressed(_ sender: UIButton) { if editIsPresented { moveToPreviewScreen() } else { delegate?.widgetViewControllerDidCancel(self) } } func toggleAspectRatioLockStates() { currentAspectLockState = currentAspectLockState == .enabledAndOff ? .enabledAndOn : .enabledAndOff updateActionButton(by: currentAspectLockState) } func updateActionButton(by aspectLockState: CLDWidgetConfiguration.AspectRatioLockState) { switch aspectLockState { case .enabledAndOff: updateActionButton(by: .aspectRatioEnabledAndOff) case .enabledAndOn : updateActionButton(by: .aspectRatioEnabledAndOn) case .disabled : updateActionButton(by: .aspectRatioDisabled) } } func updateActionButton(by actionButtonState: ActionButtonState) { UIView.transition(with: actionButton, duration: transitionDuration, options: .transitionCrossDissolve, animations: { switch actionButtonState { case .aspectRatioEnabledAndOff: self.actionButton.setAttributedTitle(self.editActionButtonUnlockedTitle, for: .normal) case .aspectRatioEnabledAndOn: self.actionButton.setAttributedTitle(self.editActionButtonLockedTitle, for: .normal) case .aspectRatioDisabled : fallthrough case .goToEditScreenDisabled: self.actionButton.setAttributedTitle(self.editActionButtonEmptyTitle, for: .normal) self.actionButton.isEnabled = false case .goToEditScreen: self.setActionButtonToGoToEdit() } }, completion: nil) } func setActionButtonToGoToEdit() { self.actionButton.setAttributedTitle(previewActionButtonTitle, for: .normal) self.actionButton.isEnabled = true } } // MARK: - create UI private extension CLDWidgetViewController { var topButtonsViewHeight: CGFloat { return 44 } var backButtonWidthRatio: CGFloat { return 0.25 } func createUI() { view.backgroundColor = .black createAllSubviews() addAllSubviews() addAllConstraints() presentAsChild(previewViewController, inView: containerView) } func createAllSubviews() { // top buttons view topButtonsView = UIView(frame: CGRect.zero) // buttons let buttonImage = CLDImageGenerator.generateImage(from: BackIconInstructions()) backButton = UIButton(type: .custom) backButton.setImage(buttonImage, for: .normal) backButton.contentHorizontalAlignment = .left backButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 0) backButton.addTarget(self, action: #selector(backPressed), for: .touchUpInside) actionButton = UIButton(type: .custom) actionButton.accessibilityIdentifier = "widgetViewControllerActionButton" changeActionButtonState(by: assets[0]) actionButton.contentHorizontalAlignment = .right actionButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 15) actionButton.addTarget(self, action: #selector(actionPressed), for: .touchUpInside) // container view containerView = UIView(frame: CGRect.zero) } func addAllSubviews() { view.addSubview(topButtonsView) topButtonsView.addSubview(backButton) topButtonsView.addSubview(actionButton) view.addSubview(containerView) } func addAllConstraints() { topButtonsView.translatesAutoresizingMaskIntoConstraints = false backButton .translatesAutoresizingMaskIntoConstraints = false actionButton .translatesAutoresizingMaskIntoConstraints = false containerView .translatesAutoresizingMaskIntoConstraints = false // top buttons view var collectionConstraints = [ NSLayoutConstraint(item: topButtonsView!, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 0), NSLayoutConstraint(item: topButtonsView!, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0), ] if #available(iOS 11.0, *) { collectionConstraints.append(topButtonsView!.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0)) } else { collectionConstraints.append(NSLayoutConstraint(item: topButtonsView!, attribute: .top , relatedBy: .equal, toItem: topLayoutGuide, attribute: .bottom, multiplier: 1, constant: 0)) } collectionConstraints.append(NSLayoutConstraint(item: topButtonsView!, attribute: .height , relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: topButtonsViewHeight)) NSLayoutConstraint.activate(collectionConstraints) // buttons let backButtonConstraints = [ NSLayoutConstraint(item: backButton!, attribute: .leading, relatedBy: .equal, toItem: topButtonsView, attribute: .leading, multiplier: 1, constant: 0), NSLayoutConstraint(item: backButton!, attribute: .top, relatedBy: .equal, toItem: topButtonsView, attribute: .top, multiplier: 1, constant: 0), NSLayoutConstraint(item: backButton!, attribute: .bottom, relatedBy: .equal, toItem: topButtonsView, attribute: .bottom, multiplier: 1, constant: 0), NSLayoutConstraint(item: backButton!, attribute: .width , relatedBy: .equal, toItem: topButtonsView, attribute: .width, multiplier: backButtonWidthRatio, constant: 0) ] NSLayoutConstraint.activate(backButtonConstraints) backButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) let actionButtonConstraints = [ NSLayoutConstraint(item: actionButton!, attribute: .trailing, relatedBy: .equal, toItem: topButtonsView, attribute: .trailing, multiplier: 1, constant: 0), NSLayoutConstraint(item: actionButton!, attribute: .top, relatedBy: .equal, toItem: topButtonsView, attribute: .top, multiplier: 1, constant: 0), NSLayoutConstraint(item: actionButton!, attribute: .bottom, relatedBy: .equal, toItem: topButtonsView, attribute: .bottom, multiplier: 1, constant: 0), NSLayoutConstraint(item: actionButton!, attribute: .leading, relatedBy: .lessThanOrEqual, toItem: backButton, attribute: .trailing, multiplier: 1, constant: view.frame.width * 0.25), ] NSLayoutConstraint.activate(actionButtonConstraints) // container view var containerViewConstraints = [ NSLayoutConstraint(item: containerView!, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 0), NSLayoutConstraint(item: containerView!, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0), NSLayoutConstraint(item: containerView!, attribute: .top, relatedBy: .equal, toItem: topButtonsView, attribute: .bottom, multiplier: 1, constant: 0), ] if #available(iOS 11.0, *) { containerViewConstraints.append(containerView!.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0)) } else { containerViewConstraints.append(NSLayoutConstraint(item: containerView!, attribute: .bottom, relatedBy : .equal, toItem: bottomLayoutGuide, attribute: .top , multiplier: 1, constant: 0)) } NSLayoutConstraint.activate(containerViewConstraints) } } // MARK: - extension UIView extension UIView { func cld_addConstraintsToFill(_ parentView: UIView) { translatesAutoresizingMaskIntoConstraints = false let constraints = [ NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: parentView, attribute: .leading, multiplier: 1, constant: 0), NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: parentView, attribute: .trailing, multiplier: 1, constant: 0), NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: parentView, attribute: .top, multiplier: 1, constant: 0), NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: parentView, attribute: .bottom, multiplier: 1, constant: 0) ] NSLayoutConstraint.activate(constraints) } func cld_addConstraintsToCenter(_ parentView: UIView) { translatesAutoresizingMaskIntoConstraints = false let constraints = [ NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 80), NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 80), NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: parentView, attribute: .centerX, multiplier: 1, constant: 0), NSLayoutConstraint(item: self, attribute: .centerY, relatedBy: .equal, toItem: parentView, attribute: .centerY, multiplier: 1, constant: 0), ] NSLayoutConstraint.activate(constraints) } } // MARK: - extension NSAttributedString private extension NSAttributedString { convenience init(string: String, color: UIColor = .white, withSuffix image: UIImage? = nil) { if let image = image { // Create Attachment let imageAttachment = NSTextAttachment() imageAttachment.image = image // Set bound to reposition let imageOffsetY: CGFloat = -5.0 imageAttachment.bounds = CGRect(x: 0, y: imageOffsetY, width: image.size.width, height: image.size.height) // Create string with attachment let attachmentString = NSAttributedString(attachment: imageAttachment) // Initialize mutable string let completeText = NSMutableAttributedString(string: string, attributes: [NSAttributedString.Key.foregroundColor: color]) // Add image to mutable string completeText.append(attachmentString) self.init(attributedString: completeText) } else { self.init(string: string) } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Uploader/CLDUploader.swift ================================================ // // CLDUploader.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The CLDUploader class is used to upload assets to your Cloudinary account's cloud. */ @objcMembers open class CLDUploader: CLDBaseNetworkObject { static public let defaultChunkSize = 20 * 1024 * 1024 // MARK: - Init fileprivate override init() { super.init() } internal override init(networkCoordinator: CLDNetworkCoordinator) { super.init(networkCoordinator: networkCoordinator) } // MARK: - Actions /** Uploads the given data to the configured cloud. - parameter data: The data to upload. - parameter uploadPreset: The upload preset to use for unsigned upload. - parameter params: An object holding all the available parameters for uploading. - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance implementing the protocol `CLDNetworkDataRequest`, allowing the options to add a progress closure that is called periodically during the upload and a response closure to be called once the upload is finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func upload(data: Data, uploadPreset: String, params: CLDUploadRequestParams? = nil, progress: ((Progress) -> Void)? = nil, completionHandler:((_ response: CLDUploadResult?, _ error: NSError?) -> ())? = nil) -> CLDUploadRequest { let params = params ?? CLDUploadRequestParams() params.setSigned(false) params.setUploadPreset(uploadPreset) return performUpload(data: data, params: params, preprocessChain: CLDPreprocessChain(), progress: progress, completionHandler: completionHandler) } /** Uploads the given data to the configured cloud. - parameter data: The data to upload. - parameter uploadPreset: The upload preset to use for unsigned upload. - parameter params: An object holding all the available parameters for uploading. - parameter preprocessChain A CLDPreprocessing chain containing processing steps and/or custom encoder to run on the resource before uploading - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance implementing the protocol `CLDNetworkDataRequest`, allowing the options to add a progress closure that is called periodically during the upload and a response closure to be called once the upload is finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func upload(data: Data, uploadPreset: String, params: CLDUploadRequestParams? = nil, preprocessChain:CLDPreprocessChain, progress: ((Progress) -> Void)? = nil, completionHandler:((_ response: CLDUploadResult?, _ error: NSError?) -> ())? = nil) -> CLDUploadRequest { let params = params ?? CLDUploadRequestParams() params.setSigned(false) params.setUploadPreset(uploadPreset) return performUpload(data: data, params: params, preprocessChain: preprocessChain, progress: progress, completionHandler: completionHandler) } /** Uploads a file from the specified URL to the configured cloud. The URL can either be of a local file (i.e. from the bundle) or can point to a remote file. - parameter url: The URL pointing to the file to upload. - parameter uploadPreset: The upload preset to use for unsigned upload. - parameter params: An object holding all the available parameters for uploading. - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance implementing the protocol `CLDNetworkDataRequest`, allowing the options to add a progress closure that is called periodically during the upload and a response closure to be called once the upload is finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func upload(url: URL, uploadPreset: String, params: CLDUploadRequestParams? = nil, progress: ((Progress) -> Void)? = nil, completionHandler:((_ response: CLDUploadResult?, _ error: NSError?) -> ())? = nil) -> CLDUploadRequest { let params = params ?? CLDUploadRequestParams() params.setSigned(false) params.setUploadPreset(uploadPreset) return performUpload(data: url, params: params, preprocessChain: CLDPreprocessChain(), progress: progress, completionHandler: completionHandler) } /** Uploads a file from the specified URL to the configured cloud. The URL can either be of a local file (i.e. from the bundle) or can point to a remote file. - parameter url: The URL pointing to the file to upload. - parameter uploadPreset: The upload preset to use for unsigned upload. - parameter params: An object holding all the available parameters for uploading. - parameter preprocessChain A CLDPreprocessing chain containing processing steps and/or custom encoder to run on the resource before uploading - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance implementing the protocol `CLDNetworkDataRequest`, allowing the options to add a progress closure that is called periodically during the upload and a response closure to be called once the upload is finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func upload(url: URL, uploadPreset: String, params: CLDUploadRequestParams? = nil, preprocessChain: CLDPreprocessChain, progress: ((Progress) -> Void)? = nil, completionHandler:((_ response: CLDUploadResult?, _ error: NSError?) -> ())? = nil) -> CLDUploadRequest { let params = params ?? CLDUploadRequestParams() params.setSigned(false) params.setUploadPreset(uploadPreset) return performUpload(data: url, params: params, preprocessChain: preprocessChain, progress: progress, completionHandler: completionHandler) } /** Uploads the given data to the configured cloud. - parameter data: The data to upload. - parameter params: An object holding all the available parameters for uploading. - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance implementing the protocol `CLDNetworkDataRequest`, allowing the options to add a progress closure that is called periodically during the upload and a response closure to be called once the upload is finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func signedUpload(data: Data, params: CLDUploadRequestParams? = nil, progress: ((Progress) -> Void)? = nil, completionHandler:((_ response: CLDUploadResult?, _ error: NSError?) -> ())? = nil) -> CLDUploadRequest { let params = params ?? CLDUploadRequestParams() params.setSigned(true) return performUpload(data: data, params: params, preprocessChain: CLDPreprocessChain(), progress: progress, completionHandler: completionHandler) } /** Uploads the given data to the configured cloud. - parameter data: The data to upload. - parameter params: An object holding all the available parameters for uploading. - parameter preprocessChain A CLDPreprocessing chain containing processing steps and/or custom encoder to run on the resource before uploading - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance implementing the protocol `CLDNetworkDataRequest`, allowing the options to add a progress closure that is called periodically during the upload and a response closure to be called once the upload is finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func signedUpload(data: Data, params: CLDUploadRequestParams? = nil, preprocessChain:CLDPreprocessChain, progress: ((Progress) -> Void)? = nil, completionHandler:((_ response: CLDUploadResult?, _ error: NSError?) -> ())? = nil) -> CLDUploadRequest { let params = params ?? CLDUploadRequestParams() params.setSigned(true) return performUpload(data: data, params: params, preprocessChain:preprocessChain, progress: progress, completionHandler: completionHandler) } /** Uploads a file from the specified URL to the configured cloud. The URL can either be of a local file (i.e. from the bundle) or can point to a remote file. - parameter url: The URL pointing to the file to upload. - parameter params: An object holding all the available parameters for uploading. - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance implementing the protocol `CLDNetworkDataRequest`, allowing the options to add a progress closure that is called periodically during the upload and a response closure to be called once the upload is finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func signedUpload(url: URL, params: CLDUploadRequestParams? = nil, progress: ((Progress) -> Void)? = nil, completionHandler:((_ response: CLDUploadResult?, _ error: NSError?) -> ())? = nil) -> CLDUploadRequest { let params = params ?? CLDUploadRequestParams() params.setSigned(true) return performUpload(data: url, params: params, preprocessChain: CLDPreprocessChain(), progress: progress, completionHandler: completionHandler) } /** Uploads a file from the specified URL to the configured cloud. The URL can either be of a local file (i.e. from the bundle) or can point to a remote file. - parameter url: The URL pointing to the file to upload. - parameter params: An object holding all the available parameters for uploading. - parameter preprocessChain A CLDPreprocessing chain containing processing steps and/or custom encoder to run on the resource before uploading - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: An instance implementing the protocol `CLDNetworkDataRequest`, allowing the options to add a progress closure that is called periodically during the upload and a response closure to be called once the upload is finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @discardableResult open func signedUpload(url: URL, params: CLDUploadRequestParams? = nil, preprocessChain: CLDPreprocessChain, progress: ((Progress) -> Void)? = nil, completionHandler:((_ response: CLDUploadResult?, _ error: NSError?) -> ())? = nil) -> CLDUploadRequest { let params = params ?? CLDUploadRequestParams() params.setSigned(true) return performUpload(data: url, params: params, preprocessChain: preprocessChain, progress: progress, completionHandler: completionHandler) } /** Uploads a file in chunks from the specified local-file URL to the configured cloud - parameter url: The URL pointing to the file to upload. - parameter uploadPreset: The upload preset to use for unsigned upload. - parameter params: An object holding all the available parameters for uploading. - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request is prepared, holding either the request object or an error */ @discardableResult open func uploadLarge(url: URL, uploadPreset: String, params: CLDUploadRequestParams = CLDUploadRequestParams(), chunkSize: Int = defaultChunkSize, progress: ((Progress) -> Void)? = nil, completionHandler: CLDUploadCompletionHandler? = nil) -> CLDUploadRequest{ params.setSigned(false) params.setUploadPreset(uploadPreset) return performUploadLarge(url: url, params: params, preprocessChain: CLDPreprocessChain(), chunkSize: chunkSize, progress: progress, completionHandler: completionHandler) } /** Uploads a file in chunks from the specified local-file URL to the configured cloud - parameter url: The URL pointing to the file to upload. - parameter uploadPreset: The upload preset to use for unsigned upload. - parameter params: An object holding all the available parameters for uploading. - parameter preprocessChain A CLDPreprocessing chain containing processing steps and/or custom encoder to run on the resource before uploading - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request is prepared, holding either the request object or an error */ @discardableResult open func uploadLarge(url: URL, uploadPreset: String, params: CLDUploadRequestParams = CLDUploadRequestParams(), preprocessChain: CLDPreprocessChain, chunkSize: Int = defaultChunkSize, progress: ((Progress) -> Void)? = nil, completionHandler: CLDUploadCompletionHandler? = nil) -> CLDUploadRequest{ params.setSigned(false) params.setUploadPreset(uploadPreset) return performUploadLarge(url: url, params: params, preprocessChain: preprocessChain, chunkSize: chunkSize, progress: progress, completionHandler: completionHandler) } /** Uploads a file in chunks from the specified local-file URL to the configured cloud - parameter url: The URL pointing to the local file to upload. - parameter params: An object holding all the available parameters for uploading. - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request is prepared, holding either the request object or an error */ @discardableResult open func signedUploadLarge(url: URL, params: CLDUploadRequestParams = CLDUploadRequestParams(), chunkSize: Int = defaultChunkSize, progress: ((Progress) -> Void)? = nil, completionHandler: CLDUploadCompletionHandler? = nil) -> CLDUploadRequest{ params.setSigned(true) return performUploadLarge(url: url, params: params, preprocessChain: CLDPreprocessChain(), chunkSize: chunkSize, progress: progress, completionHandler: completionHandler) } /** Uploads a file in chunks from the specified local-file URL to the configured cloud - parameter url: The URL pointing to the local file to upload. - parameter params: An object holding all the available parameters for uploading. - parameter preprocessChain A CLDPreprocessing chain containing processing steps and/or custom encoder to run on the resource before uploading - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request is prepared, holding either the request object or an error */ @discardableResult open func signedUploadLarge(url: URL, params: CLDUploadRequestParams = CLDUploadRequestParams(), preprocessChain: CLDPreprocessChain, chunkSize: Int = defaultChunkSize, progress: ((Progress) -> Void)? = nil, completionHandler: CLDUploadCompletionHandler? = nil) -> CLDUploadRequest{ params.setSigned(true) return performUploadLarge(url: url, params: params, preprocessChain: preprocessChain, chunkSize: chunkSize, progress: progress, completionHandler: completionHandler) } fileprivate func performUploadLarge(url: URL, params: CLDUploadRequestParams, preprocessChain: CLDPreprocessChain, chunkSize: Int, progress: ((Progress) -> Void)? = nil, completionHandler: CLDUploadCompletionHandler? = nil) -> CLDUploadRequest { if (url.absoluteString.cldIsRemoteUrl()){ // send the request to the regular upload function (that handles remote urls) return performUpload(data: url, params: params, preprocessChain: preprocessChain) } let uploadRequest = CLDUploadRequestWrapper() if let handler = completionHandler { uploadRequest.response(handler) } if let progress = progress { uploadRequest.progress(progress) } guard chunkSize >= 5 * 1024 * 1024 else { uploadRequest.setRequestError(CLDError.error(code: CLDError.CloudinaryErrorCode.generalErrorCode, message: "Chunk size must be greater than 5[MB]")) return uploadRequest } DispatchQueue.global().async { let newUrl:URL if (preprocessChain.isEmpty()){ newUrl = url } else { do { newUrl = try preprocessChain.execute(resourceData: url) } catch let error as NSError { uploadRequest.setRequestError(error) return } catch { uploadRequest.setRequestError(CLDError.generalError()) return } } let totalLength = CLDFileUtils.getFileSize(url: newUrl) guard totalLength != nil else { uploadRequest.setRequestError (CLDError.error(code: CLDError.CloudinaryErrorCode.failedRetrievingFileInfo, message: "zero file length")) return } let randomId = NSUUID().uuidString if let (baseUrl, parts) = CLDFileUtils.splitFile(url: newUrl, chunkSize: chunkSize) { uploadRequest.setRequestsData(count: parts.count, totalLength: totalLength) uploadRequest.cleanupHandler { success in DispatchQueue.global().async { CLDFileUtils.removeFile(file: baseUrl!) } } for part in parts { let range = "bytes \(part.offset)-\(part.offset + Int64(part.length - 1))/\(totalLength!)" uploadRequest.addRequest(self.performUpload(data: part.url, params: params, extraHeaders: ["X-Unique-Upload-Id": randomId, "Content-Range": range], preprocessChain: CLDPreprocessChain())) } } else { uploadRequest.setRequestError (CLDError.error(code: CLDError.CloudinaryErrorCode.failedRetrievingFileInfo, message: "Could not process file.")) } } return uploadRequest } fileprivate func performUpload(data: Any, params: CLDUploadRequestParams, extraHeaders: [String:String]? = [:], preprocessChain:CLDPreprocessChain, progress: ((Progress) -> Void)? = nil, completionHandler:((_ response: CLDUploadResult?, _ error: NSError?) -> ())? = nil) -> CLDUploadRequest { let uploadRequest:CLDUploadRequest // if preprocess or validations are required, we'll need to offload the work to a background queue if preprocessChain.isEmpty(){ let request = networkCoordinator.upload(data, params: params, extraHeaders: extraHeaders) uploadRequest = CLDDefaultUploadRequest(networkDataRequest: request) } else { let newRequest = CLDUploadRequestWrapper() uploadRequest = newRequest DispatchQueue.global().async { do { let newUrl = try preprocessChain.execute(resourceData: data) let length = CLDFileUtils.getFileSize(url: newUrl) newRequest.setRequestsData(count: 1, totalLength: length) // perform upload with the newly created file url, and empty preprocessing (it's done already) newRequest.addRequest(self.performUpload(data: newUrl, params: params, extraHeaders: extraHeaders, preprocessChain: CLDPreprocessChain())) } catch let error as NSError { newRequest.setRequestError(error) } catch { newRequest.setRequestError(CLDError.generalError()) } } } if let completionHandler = completionHandler { uploadRequest.response(completionHandler) } if let progress = progress { uploadRequest.progress(progress) } return uploadRequest } } ================================================ FILE: Cloudinary/Classes/Core/Features/Uploader/Preupload/CLDPreprocessChain.swift ================================================ // // // CLDPreprocessChain.swift // // Copyright (c) 2017 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation public typealias CLDPreprocessStep = (T) throws -> T public typealias CLDResourceEncoder = (T) throws -> URL? /** The CLDPreprocessChain is a generic base class used to run preprocessing on resources before uploading. Supports processing, validations and encoders, all fully customizable. Note: This class should not be used directly, use a concrete subclass (e.g. CLDImagePreprocessingChain). */ public class CLDPreprocessChain { internal var encoder: CLDResourceEncoder? var chain = [CLDPreprocessStep]() internal init() { } /** Returns true if the chain is empty (= NOP chain). */ public func isEmpty() -> Bool { return encoder == nil && chain.isEmpty } /** Add a step to the chain, for preprocess or validation - parameter preprocess: A CLDPreprocessStep to be used to encode the resource after the processing */ @discardableResult public func addStep(_ preprocess: @escaping CLDPreprocessStep) -> Self { chain.append(preprocess) return self } /** Set a custom resource encoder to use when saving the resource after executing the chain - parameter encoder: A CLDResourceEncoder to be used to encode the resource after the processing */ @discardableResult public func setEncoder(_ encoder: @escaping CLDResourceEncoder) -> Self { self.encoder = encoder return self } internal func execute(resourceData: Any) throws -> URL { try verifyEncoder() let resource: T? = try decodeResource(resourceData) if var resource = resource { for preprocess in chain { resource = try preprocess(resource) } if let url = try encoder!(resource) { return url } else { throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Error encoding resource") } } else { throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Error decoding resource") } } internal func decodeResource(_ resourceData: Any) throws -> T? { throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "No decoder implemented - Did you mean to use ImagePreprocessChain?") } internal func verifyEncoder() throws { if (encoder == nil) { throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "No encoder set - Did you mean to use ImagePreprocessChain?") } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Uploader/RequestParams/CLDUploadRequestParams.swift ================================================ // // CLDUploadRequestParams.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** This class represents the different parameters that can be passed when performing an upload request. For more information see the [documentation](http://cloudinary.com/documentation/image_upload_api_reference#upload). */ @objcMembers open class CLDUploadRequestParams: CLDRequestParams { /** A boolean variable representing whether or not the request should be signed. */ internal var signed: Bool = false // MARK: Init public override init() { super.init() } /** Initializes a CLDUploadRequestParams instance. - parameter params: A dictionary of the request parameters. - returns: A new instance of CLDUploadRequestParams. */ public init(params: [String : AnyObject]) { super.init() self.params = params } internal override func merge(_ other: CLDRequestParams?){ super.merge(other) if let uploadRequest = other as? CLDUploadRequestParams { self.signed = uploadRequest.signed } } // MARK: - Get Values open var publicId: String? { return getParam(.PublicId) as? String } open var format: String? { return getParam(.Format) as? String } open var type: String? { return getParam(.FileType) as? String } open var notificationUrl: String? { return getParam(.NotificationUrl) as? String } open var eagerNotificationUrl: String? { return getParam(.EagerNotificationUrl) as? String } open var proxy: String? { return getParam(.Proxy) as? String } open var folder: String? { return getParam(.Folder) as? String } open var moderation: String? { return getParam(.Moderation) as? String } open var accessControl: String? { return getParam(.AccessControl) as? String } open var eval: String? { return getParam(.Eval) as? String } open var rawConvert: String? { return getParam(.RawConvert) as? String } open var detection: String? { return getParam(.Detection) as? String } open var categorization: String? { return getParam(.Categorization) as? String } open var similaritySearch: String? { return getParam(.SimilaritySearch) as? String } open var autoTagging: String? { return getParam(.AutoTagging) as? String } open var backup: Bool? { return getParam(.Backup) as? Bool } open var useFilename: Bool? { return getParam(.UseFilename) as? Bool } open var uniqueFilename: Bool? { return getParam(.UniqueFilename) as? Bool } open var discardOriginalFilename: Bool? { return getParam(.DiscardOriginalFilename) as? Bool } open var async: Bool? { return getParam(.Async) as? Bool } open var eagerAsync: Bool? { return getParam(.EagerAsync) as? Bool } open var invalidate: Bool? { return getParam(.Invalidate) as? Bool } open var overwrite: Bool? { return getParam(.Overwrite) as? Bool } open var imageMetadata: Bool? { return getParam(.ImageMetadata) as? Bool } open var colors: Bool? { return getParam(.Colors) as? Bool } open var phash: Bool? { return getParam(.Phash) as? Bool } open var faces: Bool? { return getParam(.Faces) as? Bool } open var returnDeleteToken: Bool? { return getParam(.ReturnDeleteToken) as? Bool } open var transformation: String? { return getParam(.Transformation) as? String } open var tags: String? { return getParam(.Tags) as? String } open var allowedFormats: String? { return getParam(.AllowedFormats) as? String } open var context: String? { return getParam(.Context) as? String } open var faceCoordinates: String? { return getParam(.FaceCoordinates) as? String } open var customCoordinates: String? { return getParam(.CustomCoordinates) as? String } open var eager: String? { return getParam(.Eager) as? String } open var headers: String? { return getParam(.Headers) as? String } open var qualityAnalysis: Bool? { return getParam(.QualityAnalysis) as? Bool } open var accessibilityAnalysis: Bool? { return getParam(.AccessibilityAnalysis) as? Bool } open var ocr: Bool { return getParam(.Ocr) as? String != nil } open var backgroundRemoval: String? { return getParam(.BackgroundRemoval) as? String } open var filenameOverride: String? { return getParam(.FilenameOverride) as? String } open var assetFolder: String? { return getParam(.AssetFolder) as? String } open var publicIdPrefix: String? { return getParam(.PublicIdPrefix) as? String } open var useFilenameAsDisplayName: Bool? { return getParam(.UseFilenameAsDisplayName) as? Bool } open var displayName: String? { return getParam(.DisplayName) as? String } open var autoChaptering: Bool? { return getParam(.AutoChaptering) as? Bool } open var autoTranscription: Bool? { return getParam(.AutoTranscription) as? Bool } fileprivate func getParam(_ param: UploadRequestParams) -> AnyObject? { return params[param.rawValue] as AnyObject } // MARK: Set Simple Params /** Set the identifier that is used for accessing the uploaded resource. A randomly generated ID is assigned if not specified. The Public ID may contain a full path including folders separated by a slash (/). - parameter publicId: The identifier to set. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setPublicId(_ publicId: String) -> Self { setParam(UploadRequestParams.PublicId.rawValue, value: publicId) return self } /** Set the identifier prefix that is used for accessing the uploaded resource. - parameter prefix: The prefix to prepend. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setPublicIdPrefix(_ prefix: String) -> Self { setParam(UploadRequestParams.PublicIdPrefix.rawValue, value: prefix) return self } /** Set an optional format to convert the uploaded resource to before saving in the cloud. For example: jpg. - parameter format: The format to convert to. - returns: The same instance of CLDUploadRequestParams. */ open func setFormat(_ format: String) -> Self { setParam(UploadRequestParams.Format.rawValue, value: format) return self } /** Allows uploading resources as 'private' or 'authenticated' instead of the default public mode. - parameter type: The type to set. - returns: The same instance of CLDUploadRequestParams. */ @objc(setTypeFromType:) open func setType(_ type: CLDType) -> Self { return setType(String(describing: type)) } /** Allows uploading resources as 'private' or 'authenticated' instead of the default public mode. - parameter type: The type to set. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setType(_ type: String) -> Self { setParam(UploadRequestParams.FileType.rawValue, value: type) return self } /** Set an HTTP URL to send notification to (a webhook) when the upload is completed. - parameter notificationUrl: The URL to set. - returns: The same instance of CLDUploadRequestParams. */ open func setNotificationUrl(_ notificationUrl: String) -> Self { setParam(UploadRequestParams.NotificationUrl.rawValue, value: notificationUrl) return self } /** The default background removal behavior detects the foreground objects(s) of the image and removes the background. You activate this behavior by setting the 'backgroundRemoval' parameter to 'cloudinary_ai' when uploading an image (Upload method) or by using the Update method of the Admin API for existing images. - parameter backgroundRemoval: Use 'cloudinary_ai' or 'cloudinary_ai:[object_to_keep]' - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setBackgroundRemoval(_ backgroundRemoval: String) -> Self { setParam(UploadRequestParams.BackgroundRemoval.rawValue, value: backgroundRemoval) return self } /** Setting this params will override the original asset name. - parameter filenameOverride: New file name - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setFilenameOverride(_ filenameOverride: String) -> Self { setParam(UploadRequestParams.FilenameOverride.rawValue, value: filenameOverride) return self } /** Setting this will generate chapters file. - parameter autoChaptering: The boolean paramter - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setAutoChpatering(_ autoChaptering: Bool) -> Self { setParam(UploadRequestParams.AutoChaptering.rawValue, value: autoChaptering) return self } /** Setting this will generate transcription file. - parameter autoChaptering: The boolean paramter - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setAutoTranscription(_ autoTranscription: Bool) -> Self { setParam(UploadRequestParams.AutoTranscription.rawValue, value: autoTranscription) return self } /** Set an HTTP URL to send notification to (a webhook) when the generation of eager transformations is completed. - parameter eagerNotificationUrl: The URL to set. - returns: The same instance of CLDUploadRequestParams. */ open func setEagerNotificationUrl(_ eagerNotificationUrl: String) -> Self { setParam(UploadRequestParams.EagerNotificationUrl.rawValue, value: eagerNotificationUrl) return self } /** Tells Cloudinary to upload resources from remote URLs through the given proxy. Format: http://hostname:port. - parameter proxy: The proxy URL. - returns: The same instance of CLDUploadRequestParams. */ open func setProxy(_ proxy: String) -> Self { setParam(UploadRequestParams.Proxy.rawValue, value: proxy) return self } /** Set an optional folder name where the uploaded resource will be stored. The Public ID contains the full path of the uploaded resource, including the folder name. - parameter folder: The folder URL. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setFolder(_ folder: String) -> Self { setParam(UploadRequestParams.Folder.rawValue, value: folder) return self } /** Set an optional asset folder url where the uploaded resource will be stored. - parameter assetFolder: The folder URL. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setAssetFolder(_ assetFolder: String) -> Self { setParam(UploadRequestParams.AssetFolder.rawValue, value: assetFolder) return self } /** Set the display name of the uploaded resource. - parameter displayName: The name of the resource. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setDisplayName(_ displayName: String) -> Self { setParam(UploadRequestParams.DisplayName.rawValue, value: displayName) return self } /** Set A boolean parameter that determines whether to use the filename as the display name of the uploaded resource. - parameter useFilenameAsDisplayName: The boolean parameter. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setUseFilenameAsDisplayName(_ useFilenameAsDisplayName: Bool) -> Self { setParam(UploadRequestParams.UseFilenameAsDisplayName.rawValue, value: useFilenameAsDisplayName) return self } /** Set to manual to add the uploaded image to a queue of pending moderation images that can be moderated using the Admin API or the Cloudinary Management Console. Set to webpurify to automatically moderate the uploaded image using the WebPurify Image Moderation add-on. - parameter moderation: The moderation to set. - returns: The same instance of CLDUploadRequestParams. */ @objc(setModerationFromModeration:) @discardableResult open func setModeration(_ moderation: CLDModeration) -> Self { return setModeration(String(describing: moderation)) } /** Set to manual to add the uploaded image to a queue of pending moderation images that can be moderated using the [Cloudinary Management Console](https://cloudinary.com/console/media_library). Set to webpurify to automatically moderate the uploaded image using the [WebPurify Image Moderation add-on](http://dev.cloudinary.com:3002/documentation/webpurify_image_moderation_addon). - parameter moderation: The moderation to set. - returns: The same instance of CLDUploadRequestParams. */ open func setModeration(_ moderation: String) -> Self { setParam(UploadRequestParams.Moderation.rawValue, value: moderation) return self } /** Set to aspose to automatically convert Office documents to PDF files and other image formats using the [Aspose Document Conversion add-on](http://dev.cloudinary.com:3002/documentation/aspose_document_conversion_addon). - parameter rawConvert: The rawConvert to set. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setRawConvert(_ rawConvert: String) -> Self { setParam(UploadRequestParams.RawConvert.rawValue, value: rawConvert) return self } /** Set to adv_face to extract an extensive list of face attributes from a image using the [Advanced Facial Attribute Detection add-on](http://cloudinary.com/documentation/advanced_facial_attributes_detection_addon). - parameter detection: The detection to set. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setDetection(_ detection: String) -> Self { setParam(UploadRequestParams.Detection.rawValue, value: detection) return self } /** By setting the categorization parameter to imagga_tagging, Imagga is used to automatically classify the scenes of the uploaded image. - parameter categorization: The categorization to set. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setCategorization(_ categorization: String) -> Self { setParam(UploadRequestParams.Categorization.rawValue, value: categorization) return self } /** - parameter similaritySearch: The similarity search to set. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setSimilaritySearch(_ similaritySearch: String) -> Self { setParam(UploadRequestParams.SimilaritySearch.rawValue, value: similaritySearch) return self } /** - parameter accessControl: A list of access control rules for the upload request - returns: The same instance CLDUploadRequestParams */ @discardableResult open func setAccessControl(_ accessControl: [CLDAccessControlRule]) -> Self { setParam(UploadRequestParams.AccessControl.rawValue, value: asJsonArray(arr: accessControl)) return self } /** - parameter accessControl: A json string representing a list of access control rules for upload - returns: The same instance CLDUploadRequestParams */ @objc(setAccessControlWithString:) @discardableResult open func setAccessControl(_ accessControl: String) -> Self { setParam(UploadRequestParams.AccessControl.rawValue, value: accessControl) return self } /** Set whether to assign tags to an image according to detected scene categories with a confidence score higher than the given value (between 0.0 and 1.0). See [Imagga Auto Tagging](http://dev.cloudinary.com:3002/documentation/imagga_auto_tagging_addon) for more details. - parameter autoTagging: The auto tagging parameter to use. - returns: The same instance of CLDUploadRequestParams. */ @objc(setAutoTaggingWithDouble:) @discardableResult open func setAutoTagging(_ autoTagging: Double) -> Self { setAutoTagging(autoTagging.cldFormat(f: ".1")) return self } /** Set whether to assign tags to an image according to detected scene categories with a confidence score higher than the given value (between 0.0 and 1.0). See [Imagga Auto Tagging](http://dev.cloudinary.com:3002/documentation/imagga_auto_tagging_addon) for more details. - parameter autoTagging: The auto tagging parameter to use. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setAutoTagging(_ autoTagging: String) -> Self { setParam(UploadRequestParams.AutoTagging.rawValue, value: autoTagging) return self } // MARK: Set Boolean Params /** Set A boolean parameter that determines whether to backup the uploaded resource. Overrides the default backup settings of your account. - parameter backup: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setBackup(_ backup: Bool) -> Self { setBoolParam(.Backup, value: backup) return self } /** Set A boolean parameter that determines whether to use the original file name of the uploaded resource if available for the Public ID. The file name is normalized and random characters are appended to ensure uniqueness. Default: false. - parameter useFilename: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setUseFilename(_ useFilename: Bool) -> Self { setBoolParam(.UseFilename, value: useFilename) return self } /** When set to false, should not add random characters at the end of the filename that guarantee its uniqueness. Only relevant if use_filename is also set to true. Default: true. - parameter uniqueFilename: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setUniqueFilename(_ uniqueFilename: Bool) -> Self { setBoolParam(.UniqueFilename, value: uniqueFilename) return self } /** Set A boolean parameter that determines whether to discard the name of the original uploaded file. Relevant when delivering resources as attachments (setting the flag transformation parameter to attachment). Default: false. - parameter discardOriginalFilename: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setDiscardOriginalFilename(_ discardOriginalFilename: Bool) -> Self { setBoolParam(.DiscardOriginalFilename, value: discardOriginalFilename) return self } /** Set a boolean parameter indicating whether to perform the image generation asynchronously. default is false. - parameter async: The boolean parameter. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setAsync(_ async: Bool) -> Self { setBoolParam(UploadRequestParams.Async, value: async) return self } /** Set A boolean parameter that determines whether to generate the eager transformations asynchronously in the background. default is false. - parameter eagerAsync: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setEagerAsync(_ eagerAsync: Bool) -> Self { setBoolParam(.EagerAsync, value: eagerAsync) return self } /** Set boolean parameter indicating whether or not the asset should be invalidated through the CDN. default is false. - parameter invalidate: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setInvalidate(_ invalidate: Bool) -> Self { setBoolParam(.Invalidate, value: invalidate) return self } /** Set boolean parameter indicating whether to overwrite existing resources with the same Public ID. When set to false, return immediately if a resource with the same Public ID was found. Default: true (when using unsigned upload, the default is false and cannot be changed to true). - parameter overwrite: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setOverwrite(_ overwrite: Bool) -> Self { setBoolParam(.Overwrite, value: overwrite) return self } /** Set a boolean parameter indicating whether to retrieve IPTC and detailed Exif metadata of the uploaded asset. default is false. - parameter imageMetadata: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @available(*, deprecated, message: "Use the new method 'setMediaMetadata' instead.") @discardableResult open func setImageMetadata(_ imageMetadata: Bool) -> Self { setBoolParam(.ImageMetadata, value: imageMetadata) return self } /** Set a boolean parameter indicating whether to return IPTC, XMP, and detailed Exif metadata of the uploaded asset in the response. default is false. - parameter mediaMetadata: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setMediaMetadata(_ mediaMetadata: Bool) -> Self { setBoolParam(.MediaMetadata, value: mediaMetadata) return self } /** Set a boolean parameter indicating whether to retrieve predominant colors & color histogram of the uploaded asset. default is false. - parameter colors: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setColors(_ colors: Bool) -> Self { setBoolParam(.Colors, value: colors) return self } /** Set a boolean parameter indicating whether to return the perceptual hash (pHash) on the uploaded asset. The pHash acts as a fingerprint that allows checking image similarity. default is false. - parameter phash: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setPhash(_ phash: Bool) -> Self { setBoolParam(.Phash, value: phash) return self } /** Set a boolean parameter indicating whether to return the coordinates of faces contained in an uploaded asset (automatically detected or manually defined). Each face is specified by the X & Y coordinates of the top left corner and the width & height of the face. default is false. - parameter faces: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setFaces(_ faces: Bool) -> Self { setBoolParam(.Faces, value: faces) return self } /** Set a boolean parameter indicating whether to return a deletion token in the upload response. The token can be used to delete the uploaded resource within 10 minutes using an unauthenticated API request. Default: false. - parameter returnDeleteToken: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setReturnDeleteToken(_ returnDeleteToken: Bool) -> Self { setBoolParam(.ReturnDeleteToken, value: returnDeleteToken) return self } /** Set a boolean parameter indicating whether to return quality analysis of the image. Default: false. - parameter qualityAnalysis: The boolean parameter. - returns: The same instance of CLDExplicitRequestParams. */ @discardableResult open func setQualityAnalysis(_ qualityAnalysis: Bool) -> Self { setBoolParam(.QualityAnalysis, value: qualityAnalysis) return self } /** Set a boolean parameter indicating whether to return accessibility analysis of the image. Default: false. - parameter accessibilityAnalysis: The boolean parameter. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setAccessibilityAnalysis(_ accessibilityAnalysis: Bool) -> Self { setBoolParam(.AccessibilityAnalysis, value: accessibilityAnalysis) return self } /** A setter for any one of the simple boolean parameters. This is used to normalize boolean values in requests to be consistent across different platforms. - parameter value: The parameter value. - parameter param: The boolean parameter to set. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult fileprivate func setBoolParam(_ param: UploadRequestParams, value: Bool) -> Self { let boolNumber = NSNumber(value: value as Bool) setParam(param.rawValue, value: boolNumber) return self } // MARK: Set Params /** Set the upload preset. For more information see the [documentation](http://cloudinary.com/documentation/upload_images#unsigned_upload). - parameter uploadPreset: The upload preset from your account settings. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setUploadPreset(_ uploadPreset: String) -> Self { setParam(UploadRequestParams.UploadPreset.rawValue, value: uploadPreset) return self } /** Set the request to be an unsigned request, using an upload preset that can be set in the Cloudinary account dashboard. For more information see the [documentation](http://cloudinary.com/documentation/upload_images#unsigned_upload). - parameter uploadPreset: The upload preset from your account settings. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult @objc(setSignedWithBool:) internal func setSigned(_ signed: Bool) -> Self { self.signed = signed return self } /** Apply an incoming transformation as part of the upload request. Any image transformation parameter can be specified as an option in the upload call and these transformations are applied before saving the image in the cloud. For more information see the [documentation](http://cloudinary.com/documentation/upload_images#incoming_transformations). - parameter transformation: The transformation to apply on the uploaded asset. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult @objc(setTransformationFromTransformation:) open func setTransformation(_ transformation: CLDTransformation) -> Self { if let stringRep = transformation.asString() { setTransformation(stringRep) } return self } /** Apply an incoming transformation as part of the upload request. Any image transformation parameter can be specified as an option in the upload call and these transformations are applied before saving the image in the cloud. For more information see the [documentation](http://cloudinary.com/documentation/upload_images#incoming_transformations). - parameter transformation: The transformation to apply on the uploaded asset. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setTransformation(_ transformation: String) -> Self { setParam(UploadRequestParams.Transformation.rawValue, value: transformation) return self } /** Assign tags to the uploaded files. For more information see the [documentation](http://cloudinary.com/documentation/upload_images#tagging_images). - parameter tags: The tags to aggign to the uploaded asset. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult @objc(setTagsWithArray:) open func setTags(_ tags: [String]) -> Self { return setTags(tags.joined(separator: ",")) } /** Assign tags to the uploaded files. For more information see the [documentation](http://cloudinary.com/documentation/upload_images#tagging_images). - parameter tags: The tags to aggign to the uploaded asset. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setTags(_ tags: String) -> Self { setParam(UploadRequestParams.Tags.rawValue, value: tags) return self } /** Set An array of file formats that are allowed for uploading. The default is any supported image format for images, and any kind of raw file. Files of other types will be rejected. The formats can be any combination of image types, video formats or raw file extensions. For example: `mp4,ogv,jpg,png,pdf` - parameter allowedFormats: The array of allowed formats. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult @objc(setAllowedFormatsWithArray:) open func setAllowedFormats(_ allowedFormats: [String]) -> Self { return setAllowedFormats(allowedFormats.joined(separator: ",")) } /** Set An array of file formats that are allowed for uploading. The default is any supported image format for images, and any kind of raw file. Files of other types will be rejected. The formats can be any combination of image types, video formats or raw file extensions. For example: `mp4,ogv,jpg,png,pdf` - parameter allowedFormats: The array of allowed formats. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setAllowedFormats(_ allowedFormats: String) -> Self { setParam(UploadRequestParams.AllowedFormats.rawValue, value: allowedFormats) return self } /** Set a dictionary of the key-value pairs of general textual context metadata to attach to an uploaded resource. The context values of uploaded files are available for fetching using the Admin API. For example: `alt=My image❘caption=Profile image`. - parameter context: The context dictionary. - returns: The same instance of CLDUploadRequestParams. */ @objc(setContextFromDictionary:) @discardableResult open func setContext(_ context: [String : String]) -> Self { return setContext(buildContextString(context)) } /** Set a dictionary of the key-value pairs of general textual context metadata to attach to an uploaded resource. The context values of uploaded files are available for fetching using the Admin API. For example: `alt=My image❘caption=Profile image`. - parameter context: The context dictionary. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setContext(_ context: String) -> Self { setParam(UploadRequestParams.Context.rawValue, value: context) return self } /** Sets the coordinates of faces contained in an uploaded image and overrides the automatically detected faces. Each face is specified by the X & Y coordinates of the top left corner and the width & height of the face. - parameter faceCoordinates: The array of `CLDCoodinate` objects, each object holds a CGRect for a single face coordinate. - returns: The same instance of CLDUploadRequestParams. */ @objc(setFaceCoordinatesFromCoordinates:) @discardableResult open func setFaceCoordinates(_ faceCoordinates: [CLDCoordinate]) -> Self { return setFaceCoordinates(buildCoordinatesString(faceCoordinates)) } /** Sets the coordinates of faces contained in an uploaded image and overrides the automatically detected faces. Each face is specified by the X & Y coordinates of the top left corner and the width & height of the face. - parameter faceCoordinates: The array of `CLDCoodinate` objects, each object holds a CGRect for a single face coordinate. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setFaceCoordinates(_ faceCoordinates: String) -> Self { setParam(UploadRequestParams.FaceCoordinates.rawValue, value: faceCoordinates) return self } /** Sets the coordinates of a region contained in an uploaded image that is subsequently used for cropping uploaded images using the custom gravity mode. The region is specified by the X & Y coordinates of the top left corner and the width & height of the region. - parameter customCoordinates: The array of `CLDCoodinate` objects, each object holds a CGRect for a single custom coordinate. - returns: The same instance of CLDUploadRequestParams. */ @objc(setCustomCoordinatesFromCoordinates:) @discardableResult open func setCustomCoordinates(_ customCoordinates: [CLDCoordinate]) -> Self { return setCustomCoordinates(buildCoordinatesString(customCoordinates)) } /** Sets the coordinates of a region contained in an uploaded image that is subsequently used for cropping uploaded images using the custom gravity mode. The region is specified by the X & Y coordinates of the top left corner and the width & height of the region. - parameter customCoordinates: The array of `CLDCoodinate` objects, each object holds a CGRect for a single custom coordinate. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setCustomCoordinates(_ customCoordinates: String) -> Self { setParam(UploadRequestParams.CustomCoordinates.rawValue, value: customCoordinates) return self } /** Set an array of transformations to create for the uploaded resource during the upload process, instead of lazily creating each of them when first accessed by your site's visitors. - parameter eager: The array of transformations (CLDTransformation|CLDEagerTransformation) - returns: The same instance of CLDUploadRequestParams. */ @objc(setEagerFromTransformationArray:) @discardableResult open func setEager(_ eager: [CLDTransformation]) -> Self { return setEager(buildEagerString(eager)) } /** Set an array of transformations to create for the uploaded resource during the upload process, instead of lazily creating each of them when first accessed by your site's visitors. - parameter eager: The array of transformations. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setEager(_ eager: String) -> Self { setParam(UploadRequestParams.Eager.rawValue, value: eager) return self } /** Override quality settings for the resource - parameter quality: The quality configuration instance, see CLDQuality. - returns: The same instance of CLDUploadRequestParams. */ @objc(setQualityOverrideFromQuality:) @discardableResult open func setQualityOverride(_ quality: CLDTransformation.CLDQuality) -> Self { setParam(UploadRequestParams.QualityOverride.rawValue, value: quality.description) return self } /** Override quality settings for the resource - parameter quality: The quality as a string. - returns: The same instance of CLDUploadRequestParams. */ @objc(setQualityOverrideFromString:) @discardableResult open func setQualityOverride(_ quality: String) -> Self { setParam(UploadRequestParams.QualityOverride.rawValue, value: quality) return self } /** Allows modification on upload parameters by specifying custom logic with JavaScript code that is evaluated when uploading a file. For more information see the [documentation](https://cloudinary.com/documentation/analysis_on_upload#evaluating_and_modifying_upload_parameters). - parameter eval: The java script code to assign to the uploaded asset. - returns: The same instance of CLDUploadRequestParams. */ @objc(setEvalFromString:) @discardableResult open func setEval(_ eval: String) -> Self { setParam(UploadRequestParams.Eval.rawValue, value: eval) return self } /** Set an array of headers lines for returning as response HTTP headers when delivering the uploaded resource to your users. Supported headers: `Link, X-Robots-Tag`. For example: `X-Robots-Tag: noindex`. - parameter headers: The array of headers. - returns: The same instance of CLDUploadRequestParams. */ @objc(setHeadersWithDictionary:) @discardableResult open func setHeaders(_ headers: [String : String]) -> Self { return setHeaders(buildHeadersString(headers)) } /** Set an array of headers lines for returning as response HTTP headers when delivering the uploaded resource to your users. Supported headers: `Link, X-Robots-Tag`. For example: `X-Robots-Tag: noindex`. - parameter headers: The array of headers. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setHeaders(_ headers: String) -> Self { setParam(UploadRequestParams.Headers.rawValue, value: headers) return self } /** Requests that Cloudinary automatically find the best breakpoints, using an array of CLDResponsiveBreakpoints objects. - parameter responsiveBreakpoints: The array of responsive breakpoints setting. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setResponsiveBreakpoints(_ responsiveBreakpoints: [CLDResponsiveBreakpoints]) -> Self { var responsiveBreakpointsJSON: [String] = [] for rb in responsiveBreakpoints { responsiveBreakpointsJSON.append(rb.description) } super.setParam(UploadRequestParams.ResponsiveBreakpoints.rawValue, value: "[\(responsiveBreakpointsJSON.joined(separator: ","))]") return self } /** Set a boolean parameter that determines whether to retrieve detected text information in the uploaded image file. default is false. - parameter enable: The boolean parameter. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult open func setOcr(_ enable: Bool) -> Self { if enable { setOcr("adv_ocr") } else { setOcr(nil) } return self } /** Set a boolean parameter that determines whether to retrieve detected text information in the uploaded image file. default is false. - parameter enable: The boolean parameter. - returns: The same instance of CLDUploadRequestParams. */ @discardableResult @objc(setOcrString:) open func setOcr(_ ocrString: String?) -> Self { if let string = ocrString , !string.isEmpty { setParam(UploadRequestParams.Ocr.rawValue, value: string) } else { params.removeValue(forKey: UploadRequestParams.Ocr.rawValue) } return self } // MARK: - Helpers fileprivate enum UploadRequestParams: String { case PublicId = "public_id" case PublicIdPrefix = "public_id_prefix" case Callback = "callback" case Format = "format" case FileType = "type" case NotificationUrl = "notification_url" case EagerNotificationUrl = "eager_notification_url" case Proxy = "proxy" case Folder = "folder" case AssetFolder = "asset_folder" case Moderation = "moderation" case RawConvert = "raw_convert" case Categorization = "categorization" case Detection = "detection" case SimilaritySearch = "similarity_search" case AutoTagging = "auto_tagging" case UploadPreset = "upload_preset" case AccessControl = "access_control" case QualityOverride = "quality_override" case Eval = "eval" case DisplayName = "display_name" // Boolean params case UseFilenameAsDisplayName = "use_filename_as_display_name" case Backup = "backup" case Exif = "exif" case Faces = "faces" case Colors = "colors" case ImageMetadata = "image_metadata" case MediaMetadata = "media_metadata" case UseFilename = "use_filename" case UniqueFilename = "unique_filename" case DiscardOriginalFilename = "discard_original_filename" case Async = "async" case EagerAsync = "eager_async" case Invalidate = "invalidate" case Overwrite = "overwrite" case Phash = "phash" case ReturnDeleteToken = "return_delete_token" case QualityAnalysis = "quality_analysis" case AccessibilityAnalysis = "accessibility_analysis" case Transformation = "transformation" case Tags = "tags" case AllowedFormats = "allowed_formats" case Context = "context" case FaceCoordinates = "face_coordinates" case CustomCoordinates = "custom_coordinates" case Eager = "eager" case Headers = "headers" case ResponsiveBreakpoints = "responsive_breakpoints" case Ocr = "ocr" case BackgroundRemoval = "background_removal" case FilenameOverride = "filename_override" case AutoChaptering = "auto_chaptering" case AutoTranscription = "auto_transcription" } } ================================================ FILE: Cloudinary/Classes/Core/Features/Uploader/Requests/CLDDefaultUploadRequest.swift ================================================ // // CLDDefaultUploadRequest.swift // // Copyright (c) 2017 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** A `CLDUploadRequest` object is returned when creating a data transfer request to Cloudinary, e.g. uploading a file. It allows the options to add a progress closure that is called periodically during the transfer and a response closure to be called once the transfer has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ internal class CLDDefaultUploadRequest: CLDUploadRequest { internal var networkRequest: CLDNetworkDataRequest internal init(networkDataRequest: CLDNetworkDataRequest) { networkRequest = networkDataRequest } // MARK: - Public /** Resume the request. */ open override func resume() { networkRequest.resume() } /** Suspend the request. */ open override func suspend() { networkRequest.suspend() } /** Cancel the request. */ open override func cancel() { networkRequest.cancel() } //MARK: Handlers /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDUploadRequest. */ @discardableResult open override func responseRaw(_ completionHandler: @escaping (_ response: Any?, _ error: NSError?) -> ()) -> CLDUploadRequest { networkRequest.response(completionHandler) return self } /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDUploadRequest. */ @discardableResult open override func response(_ completionHandler: @escaping (_ result: CLDUploadResult?, _ error: NSError?) -> ()) -> CLDUploadRequest { responseRaw { (response, error) in if let res = response as? [String : AnyObject] { completionHandler(CLDUploadResult(json: res), nil) } else { completionHandler(nil, error ?? CLDError.generalError()) } } return self } /** Set a progress closure that is called periodically during the data transfer. - parameter progressBlock: The closure that is called periodically during the data transfer. - returns: The same instance of CLDUploadRequest. */ @discardableResult open override func progress(_ progress: @escaping ((Progress) -> Void)) -> CLDUploadRequest { networkRequest.progress(progress) return self } } ================================================ FILE: Cloudinary/Classes/Core/Features/Uploader/Requests/CLDUploadRequest.swift ================================================ // // CLDUploadRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** A `CLDUploadRequest` object is returned when creating a data transfer request to Cloudinary, e.g. uploading a file. It allows the options to add a progress closure that is called periodically during the transfer and a response closure to be called once the transfer has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @objcMembers open class CLDUploadRequest : NSObject { internal override init() { } /** Resume the request. */ open func resume(){} /** Suspend the request. */ open func suspend(){} /** Cancel the request. */ open func cancel(){} //MARK: Handlers /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDUploadRequest. */ @discardableResult open func responseRaw(_ completionHandler: @escaping (_ response: Any?, _ error: NSError?) -> ()) -> CLDUploadRequest{ return self } /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDUploadRequest. */ @discardableResult open func response(_ completionHandler: @escaping (_ result: CLDUploadResult?, _ error: NSError?) -> ()) -> CLDUploadRequest{ return self } /** Set a progress closure that is called periodically during the data transfer. - parameter progress: The closure that is called periodically during the data transfer. - returns: The same instance of CLDUploadRequest. */ @discardableResult open func progress(_ progress: @escaping ((Progress) -> Void)) -> CLDUploadRequest{ return self } } ================================================ FILE: Cloudinary/Classes/Core/Features/Uploader/Requests/CLDUploadRequestWrapper.swift ================================================ // // CLDUploadRequestWrapper.swift // // Copyright (c) 2017 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** A `CLDUploadRequestWrapper` object is a wrapper for instances of `CLDUploadRequest`. This is returned as a promise from several `CLDUploader` functions, in case the actual concrete request cannot yet be created. This is also allows for multiple concrete requests to be represented as one request. This class is used for preprocessing requests as well as uploda large requests. */ internal class CLDUploadRequestWrapper: CLDUploadRequest { private var state = RequestState.started fileprivate var requestsCount: Int! fileprivate var totalLength: Int64! fileprivate var requestsProgress = [CLDUploadRequest: Int64]() fileprivate var totalProgress: Progress? fileprivate var progressHandler: ((Progress) -> Void)? fileprivate var requests = [CLDUploadRequest]() fileprivate var result: CLDUploadResult? fileprivate var error: NSError? fileprivate let queue = DispatchQueue(label: "RequestsHandlingQueue", attributes: .concurrent) fileprivate let closureQueue: OperationQueue = { let operationQueue = OperationQueue() operationQueue.name = "com.cloudinary.CLDUploadRequestWrapper" operationQueue.maxConcurrentOperationCount = 1 operationQueue.isSuspended = true return operationQueue }() /** Once the count and total length of the request are known this method should be called. Without this information the progress closures will not be called. - parameter count: Number of inner requests expected to be added (or already added). - parameter totalLength: Total length, in bytes, of the uploaded resource (can be spread across several inner request for `uploadLarge`) */ internal func setRequestsData(count: Int, totalLength: Int64?) { self.totalLength = totalLength ?? 0 self.requestsCount = count self.totalLength = totalLength ?? 0 self.totalProgress = Progress(totalUnitCount: self.totalLength) } /** Add a requst to be part of the wrapping request. - parameter request: An upload request to add - This is usually a concrete `CLDDefaultUploadRequest` to be part of this wrapper. */ internal func addRequest(_ request: CLDUploadRequest) { queue.sync() { guard self.state != RequestState.cancelled && self.state != RequestState.error else { return } if (self.state == RequestState.suspended) { request.suspend() } request.response() { result, error in guard (self.state != RequestState.cancelled && self.state != RequestState.error) else { return } if (error != nil) { // single part error fails everything self.state = RequestState.error self.cancel() self.requestDone(nil, error) } else if self.requestsCount == 1 || (result?.done ?? false) { // last part arrived successfully self.state = RequestState.success self.requestDone(result, nil) } } request.progress() { innerProgress in guard (self.state != RequestState.cancelled && self.state != RequestState.error) else { return } self.requestsProgress[request] = innerProgress.completedUnitCount if let totalProgress = self.totalProgress { totalProgress.completedUnitCount = self.requestsProgress.values.reduce(0, +) self.progressHandler?(totalProgress) } } self.requests.append(request) } } /** This is used in case the request fails even without any inner upload request (e.g. when the preprocessing fails). Once the error is set here it will be send once the completion closures are set. - parameter error: The NSError to set. */ internal func setRequestError(_ error: NSError) { state = RequestState.error requestDone(nil, error) } fileprivate func requestDone(_ result: CLDUploadResult?, _ error: NSError?) { self.error = error self.result = result requestsProgress.removeAll() requests.removeAll() closureQueue.isSuspended = false } // MARK: - Public /** Resume the request. */ open override func resume() { queue.sync() { state = RequestState.started for request in requests { request.resume() } } } /** Suspend the request. */ open override func suspend() { queue.sync() { state = RequestState.suspended for request in requests { request.suspend() } } } /** Cancel the request. */ open override func cancel() { queue.sync() { state = RequestState.cancelled for request in requests { request.cancel() } } } //MARK: Handlers /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CldUploadRequestWrapper. */ @discardableResult open override func response(_ completionHandler: @escaping (_ result: CLDUploadResult?, _ error: NSError?) -> ()) -> Self { closureQueue.addOperation { completionHandler(self.result, self.error) } return self } /** Set a progress closure that is called periodically during the data transfer. - parameter progressBlock: The closure that is called periodically during the data transfer. - returns: The same instance of CLDUploadRequestWrapper. */ @discardableResult open override func progress(_ progress: @escaping ((Progress) -> Void)) -> Self { self.progressHandler = progress return self } /** Sets a cleanup handler that is called when the request doesn't need it's resources anymore. This is called whether the request succeeded or not. - Parameter handler: The closure to be called once cleanup is necessary. - returns: The same instance of CLDUploadRequestWrapper. */ @discardableResult internal func cleanupHandler(handler: @escaping (_ success: Bool) -> ()) -> Self { closureQueue.addOperation { handler(self.state == RequestState.success) } return self } } fileprivate enum RequestState: Int { case started, suspended, cancelled, error, success } ================================================ FILE: Cloudinary/Classes/Core/Features/Uploader/Results/CLDUploadResult.swift ================================================ // // CLDUploadResult.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation @objcMembers open class CLDUploadResult: CLDBaseResult { // MARK: - Getters open var type: String? { return getParam(.urlType) as? String } open var eager: [CLDEagerResult]? { guard let eagerArr = getParam(.eager) as? [[String : AnyObject]] else { return nil } var eager: [CLDEagerResult] = [] for singleEager in eagerArr { eager.append(CLDEagerResult(json: singleEager)) } return eager } open var publicId: String? { return getParam(.publicId) as? String } open var version: String? { guard let version = getParam(.version) else { return nil } return String(describing: version) } open var url: String? { return getParam(.url) as? String } open var secureUrl: String? { return getParam(.secureUrl) as? String } open var resourceType: String? { return getParam(.resourceType) as? String } open var signature: String? { return getParam(.signature) as? String } open var createdAt: String? { return getParam(.createdAt) as? String } open var length: Double? { return getParam(.length) as? Double } open var tags: [String]? { return getParam(.tags) as? [String] } open var moderation: AnyObject? { return getParam(.moderation) } open var accessControl: ([CLDAccessControlRule])? { if let rules = getParam(.accessControl) as? [[String: String]] { let result:[CLDAccessControlRule] = rules.compactMap{fromJson(object: $0)} if !result.isEmpty { return result } } return nil } open var context: [String:[String:String]]? { var result: [String:[String:String]]? = nil if let c = getParam(.context) as? [String:AnyObject] { result = [:] for k in c.keys { result![k] = c[k] as? [String:String] } } return result; } open var responsiveBreakpoints: [ResponsiveBreakpointsResult]? { var result = [ResponsiveBreakpointsResult]() if let jsonNodes = getParam(UploadResultKey.responsiveBreakpoints) as? [[String: AnyObject]] { for json in jsonNodes { result.append(ResponsiveBreakpointsResult(json: json)) } } return result } // MARK: Image Params open var width: Int? { return getParam(.width) as? Int } open var height: Int? { return getParam(.height) as? Int } open var format: String? { return getParam(.format) as? String } open var exif: [String : String]? { return getParam(.exif) as? [String : String] } open var metadata: [String : String]? { return getParam(.metadata) as? [String : String] } open var faces: AnyObject? { return getParam(.faces) } open var colors: AnyObject? { return getParam(.colors) } open var phash: String? { return getParam(.phash) as? String } open var deleteToken: String? { return getParam(.deleteToken) as? String } open var originalFilename: String? { return getParam(.originalFilename) as? String } open var playbackUrl: String? { return getParam(.playbackUrl) as? String } open var info: CLDInfo? { guard let info = getParam(.info) as? [String : AnyObject] else { return nil } return CLDInfo(json: info) } @available(*, deprecated, message: "Use qualityAnalysisResult instead.") open var qualityAnalysis: [String : AnyObject]? { return getParam(.qualityAnalysis) as? [String : AnyObject] } open var qualityAnalysisResult: CLDQualityAnalysis? { guard let qualityAnalysis = getParam(.qualityAnalysis) as? [String : AnyObject] else { return nil } return CLDQualityAnalysis(json: qualityAnalysis) } // MARK: Video Params open var video: CLDVideo? { guard let video = getParam(.video) as? [String : AnyObject] else { return nil } return CLDVideo(json: video) } open var audio: CLDAudio? { guard let audio = getParam(.audio) as? [String : AnyObject] else { return nil } return CLDAudio(json: audio) } open var frameRte: Double? { return getParam(.frameRate) as? Double } open var bitRate: Int? { return getParam(.bitRate) as? Int } open var duration: Double? { return getParam(.duration) as? Double } open var done: Bool? { return getParam(.done) as? Bool } open var assetFolder: String? { return getParam(.assetFolder) as? String } open var displayName: String? { return getParam(.displayName) as? String } open var accessibilityAnalysis: CLDAccessibilityAnalysisResult? { guard let accessibilityAnalysis = getParam(.accessibilityAnalysis) as? [String : AnyObject] else { return nil } return CLDAccessibilityAnalysisResult(json: accessibilityAnalysis) } open var coordinates: CLDCoordinates? { guard let coordinates = getParam(CommonResultKeys.coordinates) as? [String : AnyObject] else { return nil } return CLDCoordinates(json: coordinates) } // MARK: - Private Helpers fileprivate func getParam(_ param: UploadResultKey) -> AnyObject? { return resultJson[String(describing: param)] } enum UploadResultKey: CustomStringConvertible { case signature, done, deleteToken, responsiveBreakpoints case video, audio, frameRate, bitRate, duration // Video var description: String { switch self { case .signature: return "signature" case .done: return "done" case .deleteToken: return "delete_token" case .responsiveBreakpoints: return "responsive_breakpoints" case .video: return "video" case .audio: return "audio" case .frameRate: return "frame_rate" case .bitRate: return "bit_rate" case .duration: return "duration" } } } } // MARK: - Video Result @objcMembers open class CLDVideo: CLDBaseResult { open var format: String? { return getParam(.pixFormat) as? String } open var codec: String? { return getParam(.codec) as? String } open var level: Int? { return getParam(.level) as? Int } open var bitRate: Int? { return getParam(.bitRate) as? Int } open var metadata: [String: String]? { return getParam(.videoMetadata) as? [String: String] } // MARK: - Private Helpers fileprivate func getParam(_ param: VideoKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum VideoKey: CustomStringConvertible { case pixFormat, codec, level, bitRate, videoMetadata var description: String { switch self { case .pixFormat: return "pix_format" case .codec: return "codec" case .level: return "level" case .bitRate: return "bit_rate" case .videoMetadata: return "metadata" } } } } @objcMembers open class CLDEagerResult: CLDBaseResult { // MARK: - Getters open var url: String? { return getParam(.url) as? String } open var secureUrl: String? { return getParam(.secureUrl) as? String } } @objcMembers open class CLDAudio: CLDBaseResult { open var codec: String? { return getParam(.codec) as? String } open var bitRate: Int? { return getParam(.bitRate) as? Int } open var frequency: Int? { return getParam(.frequency) as? Int } open var channels: Int? { return getParam(.channels) as? Int } open var channelLayout: String? { return getParam(.channelLayout) as? String } // MARK: - Private Helpers fileprivate func getParam(_ param: AudioKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum AudioKey: CustomStringConvertible { case codec, bitRate, frequency, channels, channelLayout var description: String { switch self { case .codec: return "codec" case .bitRate: return "bit_rate" case .frequency: return "frequency" case .channels: return "channels" case .channelLayout: return "channel_layout" } } } } @objc open class ResponsiveBreakpointsResult : CLDBaseResult { // MARK: - Getters open var breakpoints: [ResponsiveBreakpoint]? { var result = [ResponsiveBreakpoint]() if let jsonNodes = getParams(ResponsiveBreakpointResultKey.breakpoints) as? [[String: AnyObject]] { for json in jsonNodes { result.append(ResponsiveBreakpoint(json: json)) } } return result } open var transformation: String? { return getParams(ResponsiveBreakpointResultKey.transformation) as? String } // MARK: - Private Helpers fileprivate func getParams(_ param: ResponsiveBreakpointResultKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum ResponsiveBreakpointResultKey : CustomStringConvertible { case breakpoints, transformation var description: String { switch self { case .breakpoints: return "breakpoints" case .transformation: return "transformation" } } } } @objc open class ResponsiveBreakpoint : CLDBaseResult { open var width : Int? { return getParam(ResponsiveBreakpointKey.width) as? Int } open var height : Int? { return getParam(ResponsiveBreakpointKey.height) as? Int } open var bytes : Int? { return getParam(ResponsiveBreakpointKey.bytes) as? Int } open var url : String? { return getParam(ResponsiveBreakpointKey.url) as? String } open var secureUrl : String? { return getParam(ResponsiveBreakpointKey.secureUrl) as? String } fileprivate func getParam(_ param: ResponsiveBreakpointKey) -> AnyObject? { return resultJson[String(describing: param)] } fileprivate enum ResponsiveBreakpointKey : CustomStringConvertible { case bytes, width, secureUrl, url, height var description: String { switch self { case .bytes: return "bytes" case .width: return "width" case .secureUrl: return "secure_url" case .url: return "url" case .height: return "height" } } } } ================================================ FILE: Cloudinary/Classes/Core/Features/Url/CLDUrl.swift ================================================ // // CLDUrl.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation /** The CLDUrl class represents a URL to a remote asset either on your Cloudinary cloud, or from another remote source. */ @objcMembers open class CLDUrl: NSObject { fileprivate struct CLDUrlConsts { static let CLD_OLD_AKAMAI_CHARED_CDN = "cloudinary-a.akamaihd.net" static let CLD_SHARED_CDN = "res\(CLD_COM)" static let CLD_COM = ".cloudinary.com" } /** The Cloudinary public ID of the resource */ fileprivate var publicId: String? /** The current Cloudinary session configuration. */ fileprivate var config: CLDConfiguration! /** The media source of the URL. default is upload. */ fileprivate var type: String = String(describing: CLDType.upload) /** The resource type of the remote asset that the URL points to. default is image. */ fileprivate var resourceType: String = String(describing: CLDUrlResourceType.image) /** The format of the remote asset that the URL points to. */ fileprivate var format: String? /** The version of the remote asset that the URL points to. */ fileprivate var version: String? /** A suffix to the URL that points to the remote asset (private CDN only, image/upload and raw/upload only). */ fileprivate var suffix: String? /** Indicates whether to use a root path instead of a full path (image/upload only). */ fileprivate var useRootPath: Bool = false /** Indicates whether to use a shorten URL (image/upload only). */ fileprivate var shortenUrl: Bool = false /** The transformation to be apllied on the remote asset. */ fileprivate var transformation: CLDTransformation? /** Indicates whether to add '/v1/' to the URL when the public ID includes folders and a 'version' value was not defined. When no version is explicitly specified and the public id contains folders, a default v1 version is added to the url. This boolean can disable that behaviour. */ fileprivate var forceVersion: Bool = true // MARK: - Init fileprivate override init() { super.init() } internal init(configuration: CLDConfiguration) { config = configuration super.init() } // MARK: - Set Values /** Set the media source of the URL. - parameter publicId: the media source to set. - returns: the same instance of CLDUrl. */ open func setPublicId(_ publicId: String) -> CLDUrl { self.publicId = publicId return self } /** Set the media source of the URL. - parameter type: the media source to set. - returns: the same instance of CLDUrl. */ @objc(setTypeFromType:) open func setType(_ type: CLDType) -> CLDUrl { return setType(String(describing: type)) } /** Set the media source of the URL. - parameter type: the media source to set. - returns: the same instance of CLDUrl. */ open func setType(_ type: String) -> CLDUrl { self.type = type return self } /** Set the resource type of the asset the URL points to. - parameter resourceType: the resource type to set. - returns: the same instance of CLDUrl. */ @objc(setResourceTypeFromUrlResourceType:) open func setResourceType(_ resourceType: CLDUrlResourceType) -> CLDUrl { return setResourceType(String(describing: resourceType)) } /** Set the resource type of the asset the URL points to. - parameter resourceType: the resource type to set. - returns: the same instance of CLDUrl. */ open func setResourceType(_ resourceType: String) -> CLDUrl { self.resourceType = resourceType return self } /** Set the format of the asset the URL points to. - parameter format: the format to set. - returns: the same instance of CLDUrl. */ open func setFormat(_ format: String) -> CLDUrl { self.format = format return self } /** Set the version of the asset the URL points to. - parameter version: the version to set. - returns: the same instance of CLDUrl. */ open func setVersion(_ version: String) -> CLDUrl { self.version = version return self } /** Set the suffix of the URL. (private CDN only, image/upload and raw/upload only). - parameter suffix: the suffix to set. - returns: the same instance of CLDUrl. */ open func setSuffix(_ suffix: String) -> CLDUrl { self.suffix = suffix return self } /** Set whether to use a root path instead of a full path. (image/upload only). - parameter useRootPath: Indicates whether to use a root path instead of a full path. - returns: the same instance of CLDUrl. */ open func setUseRootPath(_ useRootPath: Bool) -> CLDUrl { self.useRootPath = useRootPath return self } /** Set whether to use a shorten URL. (image/upload only). - parameter shortenUrl: Indicates whether to use a shorten URL. - returns: the same instance of CLDUrl. */ open func setShortenUrl(_ shortenUrl: Bool) -> CLDUrl { self.shortenUrl = shortenUrl return self } /** Indicates whether to add '/v1/' to the URL when the public ID includes folders and a 'version' value was not defined. When no version is explicitly specified and the public id contains folders, a default v1 version is added to the url. Set this boolean as false to prevent that behaviour. - parameter forceVersion: Indicates whether to add the version. - returns: the same instance of CLDUrl. */ open func setForceVersion(_ forceVersion: Bool) -> CLDUrl { self.forceVersion = forceVersion return self } /** Set the transformation to be apllied on the remote asset. - parameter transformation: The transformation to be apllied on the remote asset. - returns: the same instance of CLDUrl. */ @discardableResult open func setTransformation(_ transformation: CLDTransformation) -> CLDUrl { self.transformation = transformation return self } // MARK: - Actions /** Generate a string URL representation of the CLDUrl. - parameter signUrl: Indicates whether to generate a signature out of the API secret and add it to the generated URL. Default is false. - returns: The generated string URL representation. */ open func generate(signUrl: Bool = false) -> String? { if let publicId = self.publicId { return generate(publicId, signUrl: signUrl) } else { return nil } } /** Generate a string URL representation of the CLDUrl. - parameter publicId: The remote asset's name (e.g. the public id of an uploaded image). - parameter signUrl: Indicates whether to generate a signature out of the API secret and add it to the generated URL. Default is false. - returns: The generated string URL representation. */ open func generate(_ publicId: String, signUrl: Bool = false) -> String? { if signUrl && config.apiSecret == nil { printLog(.error, text: "Must supply api_secret for signing urls") return nil } var sourceName = publicId var resourceType = self.resourceType var type = self.type var version = self.version ?? String() var format = self.format let preloadedComponentsMatch = GenerateUrlRegex.preloadedRegex.matches(in: sourceName, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, sourceName.count)) if preloadedComponentsMatch.count > 0 { if let preloadedComponents = preloadedComponentsMatch.first { resourceType = (sourceName as NSString).substring(with: preloadedComponents.rangeAt(1)) type = (sourceName as NSString).substring(with: preloadedComponents.rangeAt(2)) version = (sourceName as NSString).substring(with: preloadedComponents.rangeAt(3)) sourceName = (sourceName as NSString).substring(with: preloadedComponents.rangeAt(4)) } } let transformation = self.transformation ?? CLDTransformation() if let unwrappedFormat = format , !unwrappedFormat.isEmpty && type == String(describing: CLDType.fetch) { transformation.setFetchFormat(unwrappedFormat) format = nil } guard let transformationStr = transformation.asString() else { printLog(.error, text: "An invalid transformation was added.") return nil } if forceVersion && version.isEmpty && sourceName.contains("/") && sourceName.range(of: "^v[0-9]+/.*", options: NSString.CompareOptions.regularExpression, range: nil, locale: nil) == nil && sourceName.range(of: "^https?:/.*", options: [NSString.CompareOptions.regularExpression, NSString.CompareOptions.caseInsensitive], range: nil, locale: nil) == nil { version = "1" } if !version.isEmpty { version = "v\(version)" } var toSign: String = String() if !transformationStr.isEmpty { toSign.append("\(transformationStr)/") } if sourceName.range(of: "^https?:/.*", options: [NSString.CompareOptions.regularExpression, NSString.CompareOptions.caseInsensitive], range: nil, locale: nil) != nil { if let encBasename = sourceName.cldSmartEncodeUrl() { toSign.append(encBasename) sourceName = encBasename } } else { if let encBasename = sourceName.removingPercentEncoding?.cldSmartEncodeUrl() { toSign.append(encBasename) sourceName = encBasename } if let suffix = suffix , !suffix.isEmpty { if suffix.range(of: "[/\\.]", options: NSString.CompareOptions.regularExpression, range: nil, locale: nil) != nil { printLog(.error, text: "URL Suffix should not include . or /") return nil } sourceName = "\(sourceName)/\(suffix)" } if let unwrappedFormat = format { sourceName = "\(sourceName).\(unwrappedFormat)" toSign.append(".\(unwrappedFormat)") } } let prefix = finalizePrefix(sourceName) guard let resourceTypeAndType = finalizeResourceTypeAndType(resourceType, type: type) else { return nil } var signature = String() if signUrl { if let apiSecret = config.apiSecret { toSign.append(apiSecret) } if config.longUrlSignature { // long url forces sha256 algorithm let encoded = toSign.sha256_base64() signature = "s--\(encoded[0...31])--" } else { switch config.signatureAlgorithm { case .sha1: let encoded = toSign.sha1_base64() signature = "s--\(encoded[0...7])--" case .sha256: let encoded = toSign.sha256_base64() signature = "s--\(encoded[0...7])--" } } } var url = [prefix, resourceTypeAndType, signature, transformationStr, version , sourceName].joined(separator: "/").replacingOccurrences(of: " ", with: "%20") //Analytics if let urlObject = URL(string: url) { if(config.analytics && urlObject.query == nil) { let analytics = "_a=" + config.analyticsObject.generateAnalyticsSignature() url = url + "?" + analytics } } let regex = try! NSRegularExpression(pattern: "([^:])\\/+", options: NSRegularExpression.Options.caseInsensitive) return regex.stringByReplacingMatches(in: url, options: [], range: NSMakeRange(0, url.count), withTemplate: "$1/") } fileprivate struct GenerateUrlRegex { fileprivate static let preloadedRegexString = "^([^/]+)/([^/]+)/v([0-9]+)/([^#]+)(#[0-9a-f]+)?$" static let preloadedRegex: NSRegularExpression = { return try! NSRegularExpression(pattern:preloadedRegexString, options: NSRegularExpression.Options.caseInsensitive) }() } // MARK: - Helpers fileprivate func finalizePrefix(_ basename: String) -> String { var prefix = String() if config.secure { var secureDistribution = String() if let secureDist = config.secureDistribution , (secureDist != CLDUrlConsts.CLD_OLD_AKAMAI_CHARED_CDN && !secureDist.isEmpty) { secureDistribution = secureDist } else { secureDistribution = config.privateCdn ? "\(config.cloudName!)-\(CLDUrlConsts.CLD_SHARED_CDN)" : CLDUrlConsts.CLD_SHARED_CDN } if config.secureCdnSubdomain { let sharedDomain = "res-\(basename.toCRC32() % 5 + 1)\(CLDUrlConsts.CLD_COM)" secureDistribution = secureDistribution.replacingOccurrences(of: CLDUrlConsts.CLD_SHARED_CDN, with: sharedDomain) } prefix = "https://\(secureDistribution)" } else if let cname = config.cname { prefix = "http://" if config.cdnSubdomain { prefix += "a\(basename.toCRC32() % 5 + 1)." } prefix += cname } else { prefix = "http://" if config.privateCdn { prefix += "\(config.cloudName!)-" } prefix += "res" if config.cdnSubdomain { prefix += "-\(basename.toCRC32() % 5 + 1)" } prefix += CLDUrlConsts.CLD_COM } if !config.privateCdn { prefix += "/\(config.cloudName!)" } return prefix } fileprivate func finalizeResourceTypeAndType(_ resourceType: String, type: String) -> String? { if !config.privateCdn, let urlSuffix = suffix , !urlSuffix.isEmpty { printLog(.error, text: "URL Suffix only supported in private CDN") return nil } var resourceTypeAndType = "\(resourceType)/\(type)" if let urlSuffix = suffix , !urlSuffix.isEmpty { if resourceTypeAndType == "\(String(describing: CLDUrlResourceType.image))/\(String(describing: CLDType.upload))" { resourceTypeAndType = "images" } else if resourceTypeAndType == "\(String(describing: CLDUrlResourceType.image))/\(String(describing: CLDType.private))" { resourceTypeAndType = "private_images" } else if resourceTypeAndType == "\(String(describing: CLDUrlResourceType.raw))/\(String(describing: CLDType.upload))" { resourceTypeAndType = "files" } else { printLog(.error, text: "URL Suffix only supported for image/upload, image/private and raw/upload") return nil } } if useRootPath { if resourceTypeAndType == "\(String(describing: CLDUrlResourceType.image))/\(String(describing: CLDType.upload))" || resourceTypeAndType == "images" { resourceTypeAndType = String() } else { printLog(.error, text: "Root path only supported for image/upload") return nil } } if shortenUrl && resourceTypeAndType == "\(String(describing: CLDUrlResourceType.image))/\(String(describing: CLDType.upload))" { resourceTypeAndType = "iu" } return resourceTypeAndType } } ================================================ FILE: Cloudinary/Classes/Core/Network/Adapter/CLDNetworkAdapter.swift ================================================ // // NetworkAdapter.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation // MARK: CLDNetworkAdapter /** A protocol defining the way the SDK works with its network layer, allowing the implementation of a custom network layer. By default the Cloudinary SDK uses CLDNetworkDelegate() as its network adapter, to use a custom network adapter you must implement the `CLDNetworkAdapter` protocol and send it when creating the CLDCloudinary instance. */ @objc public protocol CLDNetworkAdapter { // MARK: Actions /** Create a network request for the given URL, with the specified headers and body parameters. - parameter url: The URL to make the request to. - parameter headers: A dictionary of the headers to set to the request. - parameter parameters: A dictionary of the parameters to set to the request. - returns: An instance implementing the protocol `CLDNetworkDataRequest`, allowing the options to add response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ func cloudinaryRequest(_ url: String, headers: [String : String], parameters: [String : Any]) -> CLDNetworkDataRequest /** Create a network upload request for the given URL, with the specified headers, body parameters and data. - parameter url: The URL to make the request to. - parameter headers: A dictionary of the headers to set to the request. - parameter parameters: A dictionary of the parameters to set to the request. - parameter data: Can receive either the data to upload or an NSURL to either a local or a remote file to upload. - returns: An instance implementing the protocol `CLDNetworkDataRequest`, allowing the options to add a progress closure that is called periodically during the upload and a response closure to be called once the upload is finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ func uploadToCloudinary(_ url: String, headers: [String : String], parameters: [String : Any], data: Any) -> CLDNetworkDataRequest /** Download a file from the specified url. - parameter url: The URL of the file to download. - returns: An instance implementing the protocol `CLDNetworkDataRequest`, allowing the option to set a closure returning the fetched image when its available. The protocol also allows the options to add a progress closure that is called periodically during the download, as well as cancelling the request. */ func downloadFromCloudinary(_ url: String) -> CLDNetworkDataRequest // MARK: Setters /** Set a completion handler provided by the UIApplicationDelegate `application:handleEventsForBackgroundURLSession:completionHandler:` method. The handler will be called automaticaly once the session finishes its events for background URL session. default is `nil`. */ func setBackgroundCompletionHandler(_ newValue: (() -> ())?) /** The maximum number of queued downloads that can execute at the same time. The default value of this property is NSOperationQueueDefaultMaxConcurrentOperationCount. */ func setMaxConcurrentDownloads(_ maxConcurrentDownloads: Int) // MARK: Getters /** Get the completion handler to be called automaticaly once the session finishes its events for background URL session. default is `nil`. */ func getBackgroundCompletionHandler() -> (() -> ())? } // MARK: - CLDNetworkRequest /** The `CLDNetworkRequest` protocol is returned when creating a network request using one of Cloudinary's API calls. It allows the options to add a response closure to be called once the request has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @objc public protocol CLDNetworkRequest { // MARK: Actions /** Resume the request. */ func resume() /** Suspend the request. */ func suspend() /** Cancel the request. */ func cancel() } // MARK: - CLDNetworkDataRequest /** The `CLDNetworkDataRequest` protocol is returned when creating a data transfer request to Cloudinary, e.g. uploading a file. It allows the options to add a progress closure that is called periodically during the transfer and a response closure to be called once the transfer has finished, as well as performing actions on the request, such as cancelling, suspending or resuming it. */ @objc public protocol CLDNetworkDataRequest: CLDNetworkRequest { //MARK: Handlers /** Set a progress closure that is called periodically during the data transfer. - parameter progress: The closure that is called periodically during the data transfer. - returns: The same instance of CLDNetworkDataRequest. */ @discardableResult func progress(_ progress: ((Progress) -> Void)?) -> CLDNetworkDataRequest /** Set a response closure to be called once the request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the response object or the error. - returns: The same instance of CLDNetworkRequest. */ @discardableResult func response(_ completionHandler: ((_ response: Any?, _ error: NSError?) -> ())?) -> CLDNetworkRequest } // MARK: - CLDFetchImageRequest /** The `CLDFetchImageRequest` protocol is returned when creating a fetch image request. It allows the option to set a closure returning the fetched image when its available. The protocol also allows the options to add a progress closure that is called periodically during the download, as well as cancelling the request. */ @objc public protocol CLDFetchImageRequest: CLDNetworkDataRequest { //MARK: Actions /** Set a response closure to be called once the fetch image request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the retrieved UIImage or the error. - returns: The same instance of CLDFetchImageRequest. */ @discardableResult func responseImage(_ completionHandler: CLDCompletionHandler?) -> CLDFetchImageRequest } /** The `CLDFetchAssetRequest` protocol is returned when creating a fetch asset request. It allows the option to set a closure returning the fetched asset when its available. The protocol also allows the options to add a progress closure that is called periodically during the download, as well as cancelling the request. */ @objc public protocol CLDFetchAssetRequest: CLDNetworkDataRequest { //MARK: Actions /** Set a response closure to be called once the fetch asset request has finished. - parameter completionHandler: The closure to be called once the request has finished, holding either the retrieved Data or the error. - returns: The same instance of CLDFetchAssetRequest. */ @discardableResult func responseAsset(_ completionHandler: CLDAssetCompletionHandler?) -> CLDFetchAssetRequest } ================================================ FILE: Cloudinary/Classes/Core/Network/CLDDefaultNetworkAdapter.swift ================================================ // // CLDDefaultNetworkAdapter.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal class CLDDefaultNetworkAdapter: NSObject, CLDNetworkAdapter { init(configuration: URLSessionConfiguration? = nil) { if let configuration = configuration { manager = CLDNSessionManager(configuration: configuration) } else { let configuration: URLSessionConfiguration = { let configuration = URLSessionConfiguration.background(withIdentifier: SessionProperties.networkIdentifier) configuration.httpAdditionalHeaders = CLDNSessionManager.defaultHTTPHeaders return configuration }() manager = CLDNSessionManager(configuration: configuration) } manager.startRequestsImmediately = false } fileprivate let manager: CLDNSessionManager fileprivate let downloadQueue: OperationQueue = { let operationQueue = OperationQueue() operationQueue.name = "com.cloudinary.CLDDefaultNetworkAdapter" return operationQueue }() internal static let sharedAdapter = CLDDefaultNetworkAdapter() private struct SessionProperties { static let networkIdentifier: String = Bundle.main.bundleIdentifier ?? "" + ".cloudinarySDKbackgroundSession" static let downloadIdentifier: String = "" + ".cloudinarySDKbackgroundDownloadSession" } // MARK: Features internal func cloudinaryRequest(_ url: String, headers: [String: String], parameters: [String: Any]) -> CLDNetworkDataRequest { let req: CLDNDataRequest = manager.request(url, method: .post, parameters: parameters, headers: headers) req.resume() return CLDNetworkDataRequestImpl(request: req) } internal func uploadToCloudinary(_ url: String, headers: [String: String], parameters: [String: Any], data: Any) -> CLDNetworkDataRequest { var parameters = parameters let timeout = parameters.removeValue(forKey: "timeout") as? NSNumber let asyncUploadRequest = CLDAsyncNetworkUploadRequest() manager.upload(multipartFormData: { (multipartFormData) in if let data = data as? Data { multipartFormData.append(data, withName: "file", fileName: "file", mimeType: "application/octet-stream") } else if let url = data as? URL { if url.absoluteString.cldIsRemoteUrl() { if let urlAsData = url.absoluteString.data(using: String.Encoding.utf8) { multipartFormData.append(urlAsData, withName: "file") } } else { multipartFormData.append(url, withName: "file", fileName: url.lastPathComponent, mimeType: "application/octet-stream") } } for key in parameters.keys { if let value = parameters[key], value is [String] { if let valueArr = value as? [String] { for paramValue in valueArr { if let valueData = paramValue.data(using: String.Encoding.utf8) { multipartFormData.append(valueData, withName: key + "[]") } } } } else if let value = parameters[key] as? String { if value.isEmpty { continue } if let valueData = value.data(using: String.Encoding.utf8) { multipartFormData.append(valueData, withName: key) } } else if let value = parameters[key] as? Bool { if let valueData = String(describing: NSNumber(value: value)).data(using: String.Encoding.utf8) { multipartFormData.append(valueData, withName: key) } } } }, usingThreshold: UInt64(), to: url, method: .post, headers: headers, timeout: timeout) { (encodingResult) in switch encodingResult { case .success(let upload, _, _): upload.resume() let uploadRequest = CLDNetworkUploadRequest(request: upload) asyncUploadRequest.networkDataRequest = uploadRequest case .failure(let encodingError): asyncUploadRequest.networkDataRequest = CLDRequestError(error: encodingError) } } return asyncUploadRequest } internal func downloadFromCloudinary(_ url: String) -> CLDNetworkDataRequest { let req = manager.request(url) downloadQueue.addOperation { () -> () in req.resume() } return CLDNetworkDownloadRequest(request: req) } // MARK: - Setters internal func setBackgroundCompletionHandler(_ newValue: (() -> ())?) { manager.backgroundCompletionHandler = newValue } internal func setMaxConcurrentDownloads(_ maxConcurrentDownloads: Int) { downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads } // MARK: - Getters internal func getBackgroundCompletionHandler() -> (() -> ())? { return manager.backgroundCompletionHandler } } ================================================ FILE: Cloudinary/Classes/Core/Network/CLDNetworkCoordinator.swift ================================================ // // NetworkCoordinator.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import UIKit internal class CLDNetworkCoordinator: NSObject { static let DEFAULT_VERSION = "5.2.5" fileprivate struct CLDNetworkCoordinatorConsts { static let BASE_CLOUDINARY_URL = "https://api.cloudinary.com" static let API_KEY = "api_key" } fileprivate var config: CLDConfiguration fileprivate var networkAdapter: CLDNetworkAdapter fileprivate var extraHeaders: [String: String]? // MARK: - Init init(configuration: CLDConfiguration, networkAdapter: CLDNetworkAdapter = CLDDefaultNetworkAdapter.sharedAdapter) { config = configuration self.networkAdapter = networkAdapter } init(configuration: CLDConfiguration, sessionConfiguration: URLSessionConfiguration) { config = configuration self.networkAdapter = CLDDefaultNetworkAdapter(configuration: sessionConfiguration) } // MARK: - Actions internal func callAction(_ action: CLDAPIAction, params: CLDRequestParams) -> CLDNetworkDataRequest { let url = getUrl(action, resourceType: params.resourceType) let headers = getHeaders() params.setTimeout(from: config) let requestParams = getSignedRequestParams(params) return networkAdapter.cloudinaryRequest(url, headers: headers, parameters: requestParams) } internal func upload(_ data: Any, params: CLDUploadRequestParams, extraHeaders: [String:String]?=[:]) -> CLDNetworkDataRequest { let url = getUrl(.Upload, resourceType: params.resourceType) params.setTimeout(from: config) let requestParams = params.signed ? getSignedRequestParams(params) : params.params var headers :[String : String] = getHeaders() headers.cldMerge(self.extraHeaders) //User's configured extra headers headers.cldMerge(extraHeaders) return networkAdapter.uploadToCloudinary(url, headers: headers, parameters: requestParams, data: data) } internal func download(_ url: String) -> CLDNetworkDataRequest { return networkAdapter.downloadFromCloudinary(url) } // MARK: - Helpers fileprivate func getSignedRequestParams(_ requestParams: CLDRequestParams) -> [String : Any] { var params: [String : Any] = requestParams.params guard let apiKey = requestParams.apiKey ?? config.apiKey else { printLog(.error, text: "Must supply api key for a signed request") return params } if let signatureObj = requestParams.signature { params[CLDSignature.SignatureParam.Signature.rawValue] = signatureObj.signature params[CLDSignature.SignatureParam.Timestamp.rawValue] = cldParamValueAsString(value: signatureObj.timestamp) } else if let apiSecret = config.apiSecret { let timestamp = Int(Date().timeIntervalSince1970) params[CLDSignature.SignatureParam.Timestamp.rawValue] = cldParamValueAsString(value: timestamp) let signature = cloudinarySignParamsUsingSecret(getSignParams(from: params), cloudinaryApiSecret: apiSecret, signatureVersion: config.signatureVesion) params[CLDSignature.SignatureParam.Signature.rawValue] = signature } else { printLog(.error, text: "Must supply api secret for a signed request") } params[CLDNetworkCoordinatorConsts.API_KEY] = apiKey return params } fileprivate func getSignParams(from params: [String : Any]) -> [String : Any] { var signatureParams = params signatureParams.removeValue(forKey: CLDConfiguration.ConfigParam.Timeout.description) return signatureParams } fileprivate func getUrl(_ action: CLDAPIAction, resourceType: String?) -> String { var urlComponents: [String] = [] let prefix = config.uploadPrefix ?? CLDNetworkCoordinatorConsts.BASE_CLOUDINARY_URL urlComponents.append(prefix) urlComponents.append("v1_1") urlComponents.append(config.cloudName) if action != CLDAPIAction.DeleteByToken { let rescourceType = resourceType ?? String(describing: CLDUrlResourceType.image) urlComponents.append(rescourceType) } urlComponents.append(action.rawValue) return urlComponents.joined(separator: "/") } fileprivate func getHeaders() -> [String : String] { var headers: [String : String] = [:] var userAgent: String if let userPlatform = config.userPlatform { userAgent = "\(userPlatform.platform)/\(userPlatform.version) CloudinaryiOS/\(CLDNetworkCoordinator.getVersion())" } else { userAgent = "CloudinaryiOS/\(CLDNetworkCoordinator.getVersion())" } userAgent += " (\(UIDevice.current.model); \(UIDevice.current.systemName) \(UIDevice.current.systemVersion))" headers["User-Agent"] = userAgent headers["X-Requested-With"] = "XMLHttpRequest" return headers } static func getVersion() -> String { let version = DEFAULT_VERSION return version } internal enum CLDAPIAction: String, CustomStringConvertible { case Upload = "upload" case Rename = "rename" case Destroy = "destroy" case Tags = "tags" case Explicit = "explicit" case Explode = "explode" case GenerateArchive = "generate_archive" case GenerateSprite = "sprite" case Multi = "multi" case GenerateText = "text" case DeleteByToken = "delete_by_token" var description: String { switch self { case .Upload: return "upload" case .Rename: return "rename" case .Destroy: return "destroy" case .Tags: return "tags" case .Explicit: return "explicit" case .Explode: return "explode" case .GenerateArchive: return "generate_archive" case .GenerateSprite: return "sprite" case .Multi: return "multi" case .GenerateText: return "text" case .DeleteByToken: return "delete_by_token" } } } // MARK: - Public internal func setBackgroundCompletionHandler(_ newValue: (() -> ())?) { networkAdapter.setBackgroundCompletionHandler(newValue) } internal func setMaxConcurrentDownloads(_ maxConcurrentDownloads: Int) { networkAdapter.setMaxConcurrentDownloads(maxConcurrentDownloads) } internal func setExtraHeaders(_ extraHeaders: [String: String]) { self.extraHeaders = extraHeaders } internal func getExtraHeaders() -> [String: String]? { return extraHeaders } } class CLDDownloadCoordinator: CLDNetworkCoordinator { static var enableCache = true static var urlCache = URLCache.init(memoryCapacity: Defines.defaultMaxMemoryCapacity, diskCapacity: Defines.defaultMaxDiskCapacity, diskPath: "") init(configuration: CLDConfiguration) { let downloadConfiguration = URLSessionConfiguration.default downloadConfiguration.httpAdditionalHeaders = CLDNSessionManager.defaultHTTPHeaders downloadConfiguration.urlCache = CLDDownloadCoordinator.urlCache let downloadAdapter = CLDDefaultNetworkAdapter(configuration: downloadConfiguration) super.init(configuration: configuration, networkAdapter: downloadAdapter) } override init(configuration: CLDConfiguration, sessionConfiguration: URLSessionConfiguration) { super.init(configuration: configuration, sessionConfiguration: sessionConfiguration) } override init(configuration: CLDConfiguration, networkAdapter: CLDNetworkAdapter) { super.init(configuration: configuration, networkAdapter: networkAdapter) } } internal struct Defines { static let defaultMaxMemoryCapacity = 30 * 1024 * 1024 // 30 MB static let defaultMaxDiskCapacity = 150 * 1024 * 1024 // 150 MB } ================================================ FILE: Cloudinary/Classes/Core/Network/NetworkRequest/CLDAsyncNetworkUploadRequest.swift ================================================ // // CLDAsyncNetworkUploadRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal class CLDAsyncNetworkUploadRequest: CLDNetworkDataRequest { // Once a value is set it should not be overridden internal var networkDataRequest: CLDNetworkDataRequest? { didSet { if networkDataRequest != nil { closureQueue.isSuspended = false } } } fileprivate let closureQueue: OperationQueue = { let operationQueue = OperationQueue() operationQueue.name = "com.cloudinary.CLDAsyncNetworkUploadRequest" operationQueue.maxConcurrentOperationCount = 1 operationQueue.isSuspended = true return operationQueue }() // MARK: - CLDNetworkDataRequest func resume() { closureQueue.addOperation { self.networkDataRequest?.resume() } } func suspend() { closureQueue.addOperation { self.networkDataRequest?.suspend() } } func cancel() { closureQueue.addOperation { self.networkDataRequest?.cancel() } } func response(_ completionHandler: ((_ response: Any?, _ error: NSError?) -> ())?) -> CLDNetworkRequest { closureQueue.addOperation { self.networkDataRequest?.response(completionHandler) } return self } func progress(_ progress: ((Progress) -> Void)?) -> CLDNetworkDataRequest { closureQueue.addOperation { self.networkDataRequest?.progress(progress) } return self } } ================================================ FILE: Cloudinary/Classes/Core/Network/NetworkRequest/CLDErrorRequest.swift ================================================ // // CLDRequestError.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. import Foundation /// Represents a failure to create the request internal class CLDRequestError: CLDFetchImageRequest { var error: Error // MARK: - Init init(error: Error) { self.error = error } // MARK: - CLDNetworkRequest func resume() { } func suspend() { } func cancel() { } // MARK: - CLDNetworkDataRequest func progress(_ progress: ((Progress) -> Void)?) -> CLDNetworkDataRequest { return self } func response(_ completionHandler: ((_ response: Any?, _ error: NSError?) -> ())?) -> CLDNetworkRequest { completionHandler?(nil, error as NSError?) return self } // MARK: - CLDFetchImageRequest func responseImage(_ completionHandler: CLDCompletionHandler?) -> CLDFetchImageRequest { completionHandler?(nil, error as NSError?) return self } } ================================================ FILE: Cloudinary/Classes/Core/Network/NetworkRequest/CLDGenericNetworkRequest.swift ================================================ // // CLDGenericNetworkRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal class CLDGenericNetworkRequest: NSObject, CLDNetworkRequest { internal let request: T // MARK: - Init internal init(request: T) { self.request = request } //MARK: - State /** Resume the current request. */ func resume() { request.resume() } /** Suspend the current request. */ func suspend() { request.suspend() } /** Cancel the current request. */ func cancel() { request.cancel() } } ================================================ FILE: Cloudinary/Classes/Core/Network/NetworkRequest/CLDNetworkDataRequestImpl.swift ================================================ // // CLDDataNetworkRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal class CLDNetworkDataRequestImpl: CLDGenericNetworkRequest, CLDNetworkDataRequest { @discardableResult public func progress(_ progress: ((Progress) -> Void)?) -> CLDNetworkDataRequest { if let progress = progress{ request.downloadProgress(closure: progress) } return self } //MARK: - Handlers func response(_ completionHandler: ((_ response: Any?, _ error: NSError?) -> ())?) -> CLDNetworkRequest { request.responseJSON { response in if let value = response.result.value as? [String : AnyObject] { if var error = value["error"] as? [String : AnyObject] { let code = response.response?.statusCode ?? CLDError.CloudinaryErrorCode.generalErrorCode.rawValue error["statusCode"] = code as AnyObject let err = CLDError.error(code: code, userInfo: error) completionHandler?(nil, err) } else { completionHandler?(value as AnyObject?, nil) } } else if let err = response.result.error { let error = err as NSError completionHandler?(nil, error) } else { completionHandler?(nil, CLDError.generalError()) } } return self } } ================================================ FILE: Cloudinary/Classes/Core/Network/NetworkRequest/CLDNetworkDownloadRequest.swift ================================================ // // CLDNetworkDownloadRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal class CLDNetworkDownloadRequest: CLDNetworkDataRequestImpl, CLDFetchImageRequest { //MARK: - Handlers func responseImage(_ completionHandler: CLDCompletionHandler?) -> CLDFetchImageRequest { return responseData { (responseData, error, statusCode) -> () in if let data = responseData { if let image = data.cldToUIImageThreadSafe() { completionHandler?(image, nil) } else { let error = CLDError.error(code: .failedCreatingImageFromData, message: "Failed creating an image from the received data.", userInfo: ["statusCode": statusCode]) completionHandler?(nil, error) } } else if let err = error { completionHandler?(nil, err) } else { completionHandler?(nil, CLDError.generalError(userInfo: ["statusCode": statusCode])) } } as! CLDFetchImageRequest } // MARK: - Private @discardableResult internal func responseData(_ completionHandler: ((_ responseData: Data?, _ error: NSError?, _ httpCode: Int?) -> ())?) -> CLDNetworkDataRequest { request.responseData { response in let statusCode = response.response?.statusCode if let downloadedData = response.result.value { if let statusCode = statusCode, self.isAcceptableCode(code: statusCode) { if CLDDownloadCoordinator.enableCache, let result = response.response, let data = response.data, let request = self.request.request, CLDDownloadCoordinator.urlCache.cachedResponse(for: request) == nil { let cachedData = CachedURLResponse(response: result, data: data) CLDDownloadCoordinator.urlCache.storeCachedResponse(cachedData, for: request) } completionHandler?(downloadedData, nil, statusCode) } else { let statusCodeError = CLDError.error(code: .unacceptableStatusCode, message: "request error - unacceptable statusCode - \(statusCode)", userInfo: ["statusCode": statusCode]) completionHandler?(downloadedData, statusCodeError, statusCode) } } else if let err = response.result.error { let error = err as NSError completionHandler?(nil, error, statusCode) } else { completionHandler?(nil, CLDError.generalError(userInfo: ["statusCode": statusCode]), statusCode) } } return self } func isAcceptableCode(code: Int) -> Bool { self.request.acceptableStatusCodes.contains(code) } } ================================================ FILE: Cloudinary/Classes/Core/Network/NetworkRequest/CLDNetworkUploadRequest.swift ================================================ // // CLDNetworkUploadRequest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal class CLDNetworkUploadRequest: CLDNetworkDataRequestImpl { override func progress(_ progress: ((Progress) -> Void)?) -> CLDNetworkDataRequest { if let progress = progress{ request.uploadProgress(closure: progress) } return self } } ================================================ FILE: Cloudinary/Classes/Core/Network/PrivacyInfo.xcprivacy ================================================ NSPrivacyAccessedAPITypes NSPrivacyAccessedAPITypeReasons 0A2A.1 NSPrivacyAccessedAPIType NSPrivacyAccessedAPICategoryFileTimestamp NSPrivacyCollectedDataTypes NSPrivacyCollectedDataTypePurposes NSPrivacyCollectedDataTypePurposeAppFunctionality NSPrivacyCollectedDataType NSPrivacyCollectedDataTypeAudioData NSPrivacyCollectedDataTypeTracking NSPrivacyCollectedDataTypeLinked NSPrivacyCollectedDataTypePurposes NSPrivacyCollectedDataTypePurposeAppFunctionality NSPrivacyCollectedDataType NSPrivacyCollectedDataTypeOtherUserContent NSPrivacyCollectedDataTypeTracking NSPrivacyCollectedDataTypeLinked NSPrivacyCollectedDataTypePurposes NSPrivacyCollectedDataTypePurposeAnalytics NSPrivacyCollectedDataTypePurposeAppFunctionality NSPrivacyCollectedDataTypeTracking NSPrivacyCollectedDataTypeLinked NSPrivacyCollectedDataType NSPrivacyCollectedDataTypePhotosorVideos NSPrivacyTrackingDomains NSPrivacyTracking ================================================ FILE: Cloudinary/Classes/Core/Utils/CLDBuildParamsUtils.swift ================================================ // // CLDBuildParamsUtils.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation // MARK: - Build String Params internal func buildCoordinatesString(_ coordinates: [CLDCoordinate]) -> String { return coordinates.map{$0.description}.joined(separator: "|") } internal func buildEagerString(_ eager: [CLDTransformation]) -> String { return eager.map {$0.asString() ?? ""} .filter {!$0.isEmpty} .joined(separator: "|") } internal func buildContextString(_ context: [String : String]) -> String { return context.map{"\($0)=\(encodeContextValue($1))"}.joined(separator: "|") } internal func encodeContextValue(_ value: String) -> String { return value.replacingOccurrences(of: "|", with: "\\|").replacingOccurrences(of: "=", with: "\\=") } internal func buildHeadersString(_ headers: [String : String]) -> String { return headers.map{"\($0): \($1)\\n"}.joined(separator: "") } ================================================ FILE: Cloudinary/Classes/Core/Utils/CLDCryptoUtils.swift ================================================ // // CLDCryptoUtils.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. import Foundation import CommonCrypto public func cloudinarySignParamsUsingSecret( _ paramsToSign: [String: Any], cloudinaryApiSecret: String, signatureVersion: Int? = 2 ) -> String { let sortedKeys = paramsToSign.keys.sorted() var paramsArr: [String] = [] for key in sortedKeys { guard let value = paramsToSign[key], let encodedValue = encodedParamValue(value) else { continue } paramsArr.append("\(key)=\(encodedValue)") } let toSign = paramsArr.joined(separator: "&") return toSign.sha1_base8(cloudinaryApiSecret) } private func encodedParamValue(_ value: Any) -> String? { var stringValue: String? if let array = value as? [String], !array.isEmpty { stringValue = array.joined(separator: ",") } else if let str = cldParamValueAsString(value: value) { stringValue = str } guard let result = stringValue else { return nil } return result.replacingOccurrences(of: "&", with: "%26") } internal extension String { func sha1_base8(_ secret: String?) -> String { let data = self.data(using: String.Encoding.utf8)! var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH)) let ctx = UnsafeMutablePointer.allocate(capacity: 1) CC_SHA1_Init(ctx) _ = data.withUnsafeBytes { buffer in CC_SHA1_Update(ctx, buffer.baseAddress!, CC_LONG(data.count)) } if let secret = secret { let secretData = secret.data(using: String.Encoding.utf8)! _ = secretData.withUnsafeBytes { buffer in CC_SHA1_Update(ctx, buffer.baseAddress!, CC_LONG(secretData.count)) } } CC_SHA1_Final(&digest, ctx) let hexBytes = digest.map { String(format: "%02hhx", $0) } return hexBytes.joined() } func sha1_base64() -> String { return sha_base64(type: .sha1) } func sha256_base64() -> String { return sha_base64(type: .sha256) } fileprivate func sha_base64(type: shaBase64Type) -> String { var digest = [UInt8](repeating: 0, count:type.count) type.hash(string: self, digest: &digest) let data = Data(bytes: digest, count: MemoryLayout.size * digest.count) let base64 = data.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) let encoded = base64.replacingOccurrences(of: "/", with: "_") .replacingOccurrences(of: "+", with: "-") .replacingOccurrences(of: "=", with: "") return encoded } fileprivate enum shaBase64Type { case sha1 case sha256 var count: Int { get { switch self { case .sha1 : return Int(CC_SHA1_DIGEST_LENGTH) case .sha256: return Int(CC_SHA256_DIGEST_LENGTH) } } } func hash(string: String, digest: UnsafeMutablePointer!) { let cStr = NSString(string: string).utf8String switch self { case .sha1 : CC_SHA1(cStr, CC_LONG(strlen(cStr!)), digest) case .sha256: CC_SHA256(cStr, CC_LONG(strlen(cStr!)), digest) } } } func toCRC32() -> UInt32 { return crc32(self) } func cld_md5() -> String { var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) if let data = self.data(using: String.Encoding.utf8) { _ = data.withUnsafeBytes { buffer in CC_MD5(buffer.baseAddress!, CC_LONG(buffer.count), &digest) } } var digestHex = "" for index in 0.. UInt32 { let crcTable:[UInt32] = [0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d] guard let data = string.data(using: String.Encoding.utf8) else { return 0 } var crc:UInt32 = 0xffffffff var buffer = (data as NSData).bytes.bindMemory(to: UInt8.self, capacity: data.count) for _ in 1...data.count { crc = (crc >> 8) ^ crcTable[Int((crc ^ UInt32(buffer.pointee)) & 0xff)] buffer += 1 } return crc ^ 0xffffffff } ================================================ FILE: Cloudinary/Classes/Core/Utils/CLDDictionaryUtils.swift ================================================ // // CLDDictionaryUtils.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal extension Dictionary { mutating func cldMerge(_ otherDictionary: Dictionary?) { guard let otherDictionary = otherDictionary else { return } for (key,value) in otherDictionary { self.updateValue(value, forKey:key) } } } ================================================ FILE: Cloudinary/Classes/Core/Utils/CLDError.swift ================================================ // // CLDError.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal struct CLDError { fileprivate static let domain = "com.cloudinary.error" enum CloudinaryErrorCode: Int { case generalErrorCode = -7000 case failedCreatingImageFromData = -7001 case failedDownloadingImage = -7002 case failedRetrievingFileInfo = -7003 case preprocessingError = -7004 case failedDownloadingAsset = -7005 case unacceptableStatusCode = -7006 } static func generalError(userInfo: [AnyHashable: Any?]? = nil) -> NSError { return error(code: .generalErrorCode, message: "Something went wrong.", userInfo: userInfo) } static func error(domain: String = CLDError.domain, code: CloudinaryErrorCode, message: String) -> NSError { let userInfo = [NSLocalizedFailureReasonErrorKey: message] return error(domain: domain, code: code.rawValue, userInfo: userInfo) } static func error(code: CloudinaryErrorCode, message: String, userInfo: [AnyHashable: Any?]?) -> NSError { var _userInfo = userInfo _userInfo?[NSLocalizedFailureReasonErrorKey] = message return error(code: code.rawValue, userInfo: userInfo) } static func error(domain: String? = CLDError.domain, code: Int, userInfo: [AnyHashable: Any?]?) -> NSError { var info = [String: Any]() var _domain = CLDError.domain if let domain = domain { _domain = domain } if let userInfo = userInfo { userInfo.forEach {key, value in if let value = value { info[String(describing: key)] = value } } } return NSError(domain: _domain, code: code, userInfo: info) } } ================================================ FILE: Cloudinary/Classes/Core/Utils/CLDFileUtils.swift ================================================ // // // CLDFileUtils.swift // // Copyright (c) 2017 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal class CLDFileUtils { internal static func getFileSize(url: URL)->Int64?{ let attr = try? FileManager.default.attributesOfItem(atPath: url.path) return attr?[FileAttributeKey.size] as? Int64 } internal static func removeFile(file: URL) { try? FileManager.default.removeItem(at: file) } internal static func removeFiles(files: [CLDPartDescriptor]) { for file in files { removeFile (file: file.url) } } internal static func splitFile(url: URL, chunkSize: Int) -> (URL?, [CLDPartDescriptor])?{ let maxBufferSize = 16 * 1024 var outputStream:OutputStream? var success = false var parts = [CLDPartDescriptor]() guard let inputStream = InputStream(url: url) else { return nil } defer { inputStream.close() outputStream?.close() // clean up files if something failed mid-process if (!success) { removeFiles(files: parts) } } inputStream.open() let bufferSize = min(chunkSize, maxBufferSize) var buffer = [UInt8](repeating: 0, count: bufferSize) var currentChunkBytes = 0 var chunkIndex = 0 var baseUrlResult = URL(fileURLWithPath: "") var targetUrl = URL(fileURLWithPath:"") var totalRead:Int64 = 0 let randomBaseFolder = NSUUID().uuidString while inputStream.hasBytesAvailable { let read = inputStream.read(&buffer, maxLength: calcReadSize(currentChunkBytes, chunkSize, bufferSize)) if (read == 0) { continue } if (outputStream == nil){ (baseUrlResult, targetUrl) = getTempFileUrl(fileName: url.lastPathComponent, baseFolder: randomBaseFolder) outputStream = OutputStream(url: targetUrl, append: false) guard (outputStream != nil) else { return nil } outputStream?.open() } currentChunkBytes += read totalRead += Int64(read) outputStream!.write(&buffer, maxLength: read) if (currentChunkBytes >= chunkSize) { // wrap up current chunk: outputStream?.close() outputStream = nil parts.append(CLDPartDescriptor(url: targetUrl, offset: totalRead - Int64(currentChunkBytes), length: currentChunkBytes)) currentChunkBytes = 0 chunkIndex += 1 } } if (outputStream != nil) { // wrap up last chunk's last chunk: parts.append(CLDPartDescriptor(url: targetUrl, offset: totalRead - Int64(currentChunkBytes), length: currentChunkBytes)) } success = true return (baseUrlResult, parts) } fileprivate static func calcReadSize(_ currentChunkBytes: Int, _ chunkSize: Int, _ bufferSize: Int) -> Int { let chunkSpaceLeft = chunkSize - currentChunkBytes let maxLength = min(bufferSize, chunkSpaceLeft) return maxLength } internal static func getTempFileUrl(fileName: String, baseFolder: String) -> (URL, URL) { let randomId = NSUUID().uuidString let baseUrl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(baseFolder) let tempDirURL = baseUrl.appendingPathComponent(randomId) try! FileManager.default.createDirectory(at: tempDirURL, withIntermediateDirectories: true, attributes: nil) return (baseUrl, tempDirURL.appendingPathComponent(fileName)) } } public struct CLDPartDescriptor { public let url: URL public let offset: Int64 public let length: Int } ================================================ FILE: Cloudinary/Classes/Core/Utils/CLDImageGenerator/CLDImageGenerator.swift ================================================ // // CLDImageGenerator.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit // MARK: - CLDImageDrawingInstructions protocol CLDImageDrawingInstructions { var targetSize: CGSize { get } func draw(in context: CGContext) } // MARK: - CLDImageGenerator class CLDImageGenerator: NSObject { class CLDGeneratableImage { let instructions: CLDImageDrawingInstructions init(_ drawingInstructions: CLDImageDrawingInstructions) { self.instructions = drawingInstructions } /// Draws an image according to instructions final func draw() -> UIImage? { UIGraphicsBeginImageContextWithOptions(instructions.targetSize, false, UIScreen.main.scale) guard let context = UIGraphicsGetCurrentContext() else { return nil } context.saveGState() instructions.draw(in: context) context.restoreGState() guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil } UIGraphicsEndImageContext() return image } } /// Generates an image based on an instructions object /// /// - Parameter instructions: Drawing instructions used to generate an image /// - Parameter direction : The direction of the generated image static func generateImage(from instructions: CLDImageDrawingInstructions, for direction: CLDImageGenerator.Direction = .up) -> UIImage? { guard let workImage = CLDGeneratableImage(instructions).draw() else { return nil } return UIImage(cgImage: workImage.cgImage!, scale: workImage.scale, orientation: direction.imageOrientation) } } // MARK: - CLDImageGenerator extension CLDImageGenerator { enum Direction: Int { case left case right case up case down } } // MARK: - CLDImageGenerator.Direction extension CLDImageGenerator.Direction { var imageOrientation: UIImage.Orientation { switch self { case .up : return .up case .down : return .down case .left : return .left case .right: return .right } } } ================================================ FILE: Cloudinary/Classes/Core/Utils/CLDImageUtils.swift ================================================ // // CLDImageUtils.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import UIKit private let lock = NSLock() internal extension Data { func cldToUIImageThreadSafe() -> UIImage? { lock.lock() let image = UIImage(data: self) lock.unlock() return image } } ================================================ FILE: Cloudinary/Classes/Core/Utils/CLDJsonUtils.swift ================================================ // // CLDJsonUtils.swift // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal func fromJson(object: [String: String]) -> T? { if let jsonData = try? JSONSerialization.data(withJSONObject: object), let object = try? getJsonDecoder().decode(T.self, from: jsonData) { return object } return nil } internal func asJsonArray(arr: [T]) -> String { return "[\(arr.map {asJson(object: $0)}.joined(separator: ","))]" } internal func asJson(object: T) -> String { return String(data: try! getJsonEncoder().encode(object), encoding: String.Encoding.utf8)! } fileprivate func getJsonDecoder() -> JSONDecoder { let jsonDecoder = JSONDecoder() jsonDecoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.formatted(getDateFormatter()) return jsonDecoder } fileprivate func getJsonEncoder() -> JSONEncoder { let jsonEncoder = JSONEncoder() jsonEncoder.dateEncodingStrategy = .formatted(getDateFormatter()) return jsonEncoder } fileprivate func getDateFormatter() -> DateFormatter { let iso8601Formatter = DateFormatter() iso8601Formatter.calendar = Calendar(identifier: .iso8601) iso8601Formatter.locale = Locale(identifier: "en_US_POSIX") iso8601Formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXX" iso8601Formatter.timeZone = TimeZone(secondsFromGMT: 0) return iso8601Formatter } ================================================ FILE: Cloudinary/Classes/Core/Utils/CLDLogManager.swift ================================================ // // CLDLogManager.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation private let prefix = "[Cloudinary]" @objc public enum CLDLogLevel: Int { case trace, debug, info, warning, error, none } private enum LevelText: String { case Trace, Debug, Info, Warning, Error, None } private func levelTextForLevel(_ logLevel: CLDLogLevel) -> LevelText { switch(logLevel) { case .trace: return .Trace case .debug: return .Debug case .info: return .Info case .warning: return .Warning case .error: return .Error case .none: return .None } } internal struct CLDLogManager { internal static var minimumLogLevel = CLDLogLevel.none } internal func printLog(_ logLevel : CLDLogLevel, text: T, _ file: String = #file, _ function: String = #function, _ line: Int = #line){ if CLDLogManager.minimumLogLevel.rawValue <= logLevel.rawValue { let filename = (file as NSString).lastPathComponent let levelText = levelTextForLevel(logLevel).rawValue print("\(prefix):[\(levelText)]: \(filename).\(function)[\(line)]: \(text)") } } ================================================ FILE: Cloudinary/Classes/Core/Utils/CLDStringUtils.swift ================================================ // // CLDStringUtils.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation //internal extension Int { // func format(f: String) -> String { // return String(format: "%\(f)d", self) // } //} internal extension Double { func cldFormat(f: String) -> String { return String(format: "%\(f)f", self) } } internal extension Float { func cldFloatFormat() -> String { return cldFormat(f: ".1") } func cldFormat(f: String) -> String { return String(format: "%\(f)f", self) } /** Returns an string using int format if the float is an integer value or the full float if it isn't: 23.5 => "23.5" 23.5123 => "23.5123" 23 => "23" */ func cldCleanFormat() -> String { return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(self); } } internal extension String { func cldBase64Encode() -> String { return (self.data(using: String.Encoding.utf8)?.base64EncodedString())! } func cldBase64UrlEncode() -> String { return cldBase64Encode().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_") } func cldSmartEncodeUrl() -> String? { let customAllowedSet = NSCharacterSet(charactersIn:"!*'\"();@&=+$,?%#[] ").inverted return addingPercentEncoding(withAllowedCharacters: customAllowedSet) } subscript (i: Int) -> Character { return self[startIndex.cldAdvance(i, for: self)] } subscript (r: CountableClosedRange) -> String { get { let startIndex = self.index(self.startIndex, offsetBy: r.lowerBound) let endIndex = self.index(startIndex, offsetBy: r.upperBound - r.lowerBound) return String(self[startIndex...endIndex]) } } func cldStringByAppendingPathComponent(str: String) -> String { return self + ("/\(str)") } func cldAsBool() -> Bool { if self == "true" { return true } if let intValue = Int(self) { return NSNumber(value: intValue).boolValue } return false } func cldAsNSNumber() -> NSNumber? { if let intValue = Int(self) { return NSNumber(value: intValue) } return nil } func cldIsRemoteUrl() -> Bool { return self.range(of: "^ftp:|^https?:|^s3:|^gs:|^data:([\\w-]+\\/[\\w-]+(\\+[\\w-]+)?)?(;[\\w-]+=[\\w-]+)*;base64,([a-zA-Z0-9\\/+\\n=]+)$", options: [NSString.CompareOptions.regularExpression, NSString.CompareOptions.caseInsensitive], range: nil, locale: nil) != nil } func leftPadding(toLength: Int, withPad character: Character) -> String { let newLength = self.count if newLength < toLength { return String(repeatElement(character, count: toLength - newLength)) + self } else { return self.substring(from: index(self.startIndex, offsetBy: newLength - toLength)) } } func toAnalyticsVersionStr() -> String { guard let binaryInt = UInt32(self, radix: 2) else { return "" } switch binaryInt { case 0..<25: guard let value = UnicodeScalar(UnicodeScalar("A").value + binaryInt) else { return "" } return String(UnicodeScalar(value)) case 26..<51: guard let value = UnicodeScalar(UnicodeScalar("a").value + binaryInt - 26) else { return "" } return String(UnicodeScalar(value)) default: guard let value = UnicodeScalar(UnicodeScalar("0").value + binaryInt - 52) else { return "" } return String(UnicodeScalar(value)) } } } internal extension String.Index{ // func successor(in string:String)->String.Index{ // return string.index(after: self) // } // // func predecessor(in string:String)->String.Index{ // return string.index(before: self) // } func cldAdvance(_ offset:Int, `for` string:String)->String.Index{ return string.index(self, offsetBy: offset) } } internal func cldParamValueAsString(value: Any) -> String? { if let valueStr = value as? String { if valueStr.isEmpty { return nil } return valueStr } else if let valueNum = value as? NSNumber { return String(describing: valueNum) } else { printLog(.error, text: "The parameter value must ba a String or a Number") return nil } } extension String { internal func removePrefix(_ prefix: String) -> String { guard self.hasPrefix(prefix) else { return self } return String(self.dropFirst(prefix.count)) } internal func firstIndex(of value: String) -> Int { guard let range: Range = range(of: value) else { return NSNotFound } return distance(from: startIndex, to: range.lowerBound) } } ================================================ FILE: Cloudinary/Classes/ios/Extensions/CLDTransformation+Ios.swift ================================================ // // CLDTransformation+Ios.swift // Cloudinary // // Created on 23/02/2020. // import Foundation import UIKit extension CLDTransformation { /** Deliver the image in the correct device pixel ratio, according to the used device. - returns: The same instance of CLDTransformation. */ @discardableResult open func setDprAuto() -> Self { let scale = Float(UIScreen.main.scale) return setDpr(scale) } } ================================================ FILE: Cloudinary/Classes/ios/Extensions/ExtensionCLDDownloader.swift ================================================ // // ExtensionCLDDownloader.swift // // Copyright (c) 2021 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation extension CLDDownloader { // MARK: - Actions /** Asynchronously fetches a remote image from the specified URL. The image is retrieved from the cache if it exists, otherwise its downloaded and cached. //TODO: REMOVE!!! - parameter url: The image URL to download. - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request has finished, holding either the retrieved UIImage or the error. - returns: A `CLDFetchImageRequest` instance to be used to get the fetched image from, or to get the download progress or cancel the task. */ @discardableResult open func fetchImage(_ url: String, _ progress: ((Progress) -> Void)? = nil, completionHandler: CLDCompletionHandler? = nil) -> CLDFetchImageRequest { let request = CLDFetchImageRequestImpl(url: url, downloadCoordinator: downloadCoordinator) request.responseImage(completionHandler) request.progress(progress) request.fetchImage() return request } /** Asynchronously fetches a remote asset from the specified URL. The asset is retrieved from the cache if it exists, otherwise its downloaded and cached. - parameter url: The asset URL to download. - parameter progress: The closure that is called periodically during the data transfer. - parameter completionHandler: The closure to be called once the request has finished, holding either the retrieved Data or an error. - returns: A `CLDFetchAssetRequest` instance to be used to get the fetched image from, or to get the download progress or cancel the task. */ @discardableResult open func fetchAsset(_ url: String, _ progress: ((Progress) -> Void)? = nil, completionHandler: CLDAssetCompletionHandler? = nil) -> CLDFetchAssetRequest { let request = CLDFetchAssetRequestImpl(url: url, downloadCoordinator: downloadCoordinator) request.responseAsset(completionHandler) request.progress(progress) request.fetchAsset() return request } } ================================================ FILE: Cloudinary/Classes/ios/Extensions/UIButton+Cloudinary.swift ================================================ // // UIButton+Cloudinary.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import UIKit public extension UIButton { /** Download an image asynchronously from the specified URL and set it to the UIButton's image. The image is retrieved from the cache if it exists, otherwise its downloaded and cached. - parameter url: The image URL to download. - parameter state: The UIButton's UIControlState state that uses the specified image. - parameter placeholder: A placeholder image to be set as the image untill the asynchronus download request finishes. */ @objc func cldSetImage(_ url: String, forState state: UIControl.State, cloudinary: CLDCloudinary, placeholder: UIImage? = nil) { fetchImageForUIElement(url, placeholder: placeholder, cloudinary: cloudinary) { [weak self] (image: UIImage) in self?.setImage(image, for: state) } } /** Download an image asynchronously from the specified URL and set it to the UIButton's image. The image is retrieved from the cache if it exists, otherwise its downloaded and cached. - parameter publicId: The remote asset's name (e.g. the public id of an uploaded image). - parameter cloudinary: An instance of CLDCloudinary. - parameter state: The UIButton's UIControlState state that uses the specified image. - parameter signUrl: A boolean parameter indicating whether or not to generate a signiture out of the API secret and add it to the generated URL. Default is false. - parameter transformation: An instance of CLDTransformation. - parameter placeholder: A placeholder image to be set as the background image untill the asynchronus download request finishes. */ @objc func cldSetImage(publicId: String, cloudinary: CLDCloudinary, forState state: UIControl.State, signUrl: Bool = false, transformation: CLDTransformation? = nil, placeholder: UIImage? = nil) { let urlGen = cloudinary.createUrl() if let transformation = transformation { urlGen.setTransformation(transformation) } guard let url = urlGen.generate(publicId, signUrl: signUrl) else { if let placeholder = placeholder { DispatchQueue.main.async { [weak self] in self?.setImage(placeholder, for: state) } } return } fetchImageForUIElement(url, placeholder: placeholder, cloudinary: cloudinary) { [weak self] (image: UIImage) in self?.setImage(image, for: state) } } /** Download an image asynchronously from the specified URL and set it to the UIButton's background image. The image is retrieved from the cache if it exists, otherwise its downloaded and cached. - parameter url: The image URL to download. - parameter state: The UIButton's UIControlState state that uses the specified image. - parameter placeholder: A placeholder image to be set as the background image untill the asynchronus download request finishes. */ @objc func cld_setBackgroundImage(_ url: String, forState state: UIControl.State, cloudinary: CLDCloudinary, placeholder: UIImage? = nil) { let setImageOnMainQueue = { [weak self] (image: UIImage) in DispatchQueue.main.async { self?.setBackgroundImage(image, for: state) } } fetchImageForUIElement(url, placeholder: placeholder, cloudinary: cloudinary, fetchedImageHandler: setImageOnMainQueue) } /** Download an image asynchronously from the specified URL and set it to the UIButton's image. The image is retrieved from the cache if it exists, otherwise its downloaded and cached. - parameter publicId: The remote asset's name (e.g. the public id of an uploaded image). - parameter cloudinary: An instance of CLDCloudinary. - parameter state: The UIButton's UIControlState state that uses the specified image. - parameter signUrl: A boolean parameter indicating whether or not to generate a signiture out of the API secret and add it to the generated URL. Default is false. - parameter transformation: An instance of CLDTransformation. - parameter placeholder: A placeholder image to be set as the background image untill the asynchronus download request finishes. */ @objc func cld_setBackgroundImage(publicId: String, cloudinary: CLDCloudinary, forState state: UIControl.State, signUrl: Bool = false, transformation: CLDTransformation? = nil, placeholder: UIImage? = nil) { let urlGen = cloudinary.createUrl() if let transformation = transformation { urlGen.setTransformation(transformation) } guard let url = urlGen.generate(publicId, signUrl: signUrl) else { if let placeholder = placeholder { DispatchQueue.main.async { [weak self] in self?.setBackgroundImage(placeholder, for: state) } } return } fetchImageForUIElement(url, placeholder: placeholder, cloudinary: cloudinary) { [weak self] (image: UIImage) in self?.setBackgroundImage(image, for: state) } } } ================================================ FILE: Cloudinary/Classes/ios/Extensions/UIImageView+Cloudinary.swift ================================================ // // UIImageView+Cloudinary.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import UIKit public extension UIImageView { /** Download an image asynchronously from the specified URL and set it to the UIImageView's image. The image is retrieved from the cache if it exists, otherwise its downloaded and cached. - parameter url: The image URL to download. - parameter cloudinary: An instance of CLDCloudinary. - parameter placeholder: A placeholder image to be set as the background image untill the asynchronus download request finishes. */ @objc func cldSetImage(_ url: String, cloudinary: CLDCloudinary, placeholder: UIImage? = nil) { fetchImageForUIElement(url, placeholder: placeholder, cloudinary: cloudinary) { [weak self] (image: UIImage) in self?.image = image } } /** Download an image asynchronously from the specified URL and set it to the UIImageView's image. The image is retrieved from the cache if it exists, otherwise its downloaded and cached. - parameter publicId: The remote asset's name (e.g. the public id of an uploaded image). - parameter cloudinary: An instance of CLDCloudinary. - parameter signUrl: A boolean parameter indicating whether or not to generate a signature out of the API secret and add it to the generated URL. Default is false. - parameter resourceType The resource type of the image to download (can be useful to display video frames for thumbnails). - parameter transformation: An instance of CLDTransformation. - parameter placeholder: A placeholder image to be set as the background image until the asynchronus download request finishes. */ @objc func cldSetImage(publicId: String, cloudinary: CLDCloudinary, signUrl: Bool = false, resourceType:CLDUrlResourceType = CLDUrlResourceType.image, transformation: CLDTransformation? = nil, placeholder: UIImage? = nil) { var cloudinary = cloudinary cloudinary.config.analyticsObject.setFeatureFlag(flag: "E") let urlGen = cloudinary.createUrl() if let transformation = transformation { urlGen.setTransformation(transformation) } guard let url = urlGen.setResourceType(resourceType).generate(publicId, signUrl: signUrl) else { if let placeholder = placeholder { DispatchQueue.main.async { [weak self] in self?.image = placeholder } } return } fetchImageForUIElement(url, placeholder: placeholder, cloudinary: cloudinary) { [weak self] (image: UIImage) in self?.image = image } } /** Static var used to generate a unique address for the associated object */ private struct AssociatedKeys { static var cldCurrentUrl = "cldCurrentUrl" } /** Add an associated object to UIImageView so we can track the current url 'attached' to the image view. This is important in case the view is used in an collection adapter where views are recycled, to verify that when the async download finishes the associated url hasn't changed */ internal var cldCurrentUrl:String? { get { return objc_getAssociatedObject(self, &AssociatedKeys.cldCurrentUrl) as? String } set { objc_setAssociatedObject(self, &AssociatedKeys.cldCurrentUrl, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } ================================================ FILE: Cloudinary/Classes/ios/Extensions/UIView+Cloudinary.swift ================================================ // // UIView+Cloudinary.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import UIKit internal extension UIView { func fetchImageForUIElement(_ url: String, placeholder: UIImage?, cloudinary: CLDCloudinary, fetchedImageHandler: @escaping ((_ fetchedImage: UIImage) -> ())) { if let placeholder = placeholder { fetchedImageHandler(placeholder) } DispatchQueue.main.async { self.setInProgressUrl(url) } cloudinary.createDownloader().fetchImage(url, completionHandler: { [weak self] (responseImage, error) in if let img = responseImage { DispatchQueue.main.async { if let view = self, view.isUrlStillRelevant(url) { fetchedImageHandler(img) } } } }) } // set a url as the current request url for this view, if possible func setInProgressUrl(_ url: String?){ // The associated propery `cldCurrentUrl` is only available on UIImageViews. if let imageView = self as? UIImageView { imageView.cldCurrentUrl = url } } // check whether the url is in sync with the last request url on this view, if possible func isUrlStillRelevant(_ url: String) -> Bool { // The associated property `cldCurrentUrl` is only available on UIImageViews, // we do not store the url for other UIViews if let imageView = self as? UIImageView, let lastRequestUrl = imageView.cldCurrentUrl { return url == lastRequestUrl } return true } } ================================================ FILE: Cloudinary/Classes/ios/NetworkRequest/CLDFetchAssetRequestImpl.swift ================================================ // // CLDFetchAssetRequestImpl.swift // // Copyright (c) 2021 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation internal class CLDFetchAssetRequestImpl: CLDFetchAssetRequest { fileprivate let url : String fileprivate let downloadCoordinator: CLDDownloadCoordinator fileprivate let closureQueue: OperationQueue fileprivate var data : Data? fileprivate var error: NSError? // Requests fileprivate var dataDownloadRequest: CLDNetworkDownloadRequest? fileprivate var progress : ((Progress) -> Void)? init(url: String, downloadCoordinator: CLDDownloadCoordinator) { self.url = url self.downloadCoordinator = downloadCoordinator closureQueue = { let operationQueue = OperationQueue() operationQueue.name = "com.cloudinary.CLDFetchAssetRequest" operationQueue.maxConcurrentOperationCount = 1 operationQueue.isSuspended = true return operationQueue }() } // MARK: - Actions func fetchAsset() { DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async { self.downloadData() } } // MARK: Private fileprivate func downloadData() { dataDownloadRequest = downloadCoordinator.download(url) as? CLDNetworkDownloadRequest dataDownloadRequest?.progress(progress) dataDownloadRequest?.responseData { [weak self] (responseData, responseError, statusCode) -> () in if let data = responseData, let err = responseError { self?.data = data self?.error = err } else if let data = responseData { self?.data = data } else if let err = responseError { self?.error = err } else { let error = CLDError.error(code: .failedDownloadingAsset, message: "Failed attempting to download asset.", userInfo: ["statusCode": statusCode]) self?.error = error } self?.closureQueue.isSuspended = false } } // MARK: - CLDFetchDataRequest @discardableResult @objc func responseAsset(_ completionHandler: CLDAssetCompletionHandler?) -> CLDFetchAssetRequest { closureQueue.addOperation { if let data = self.data, let error = self.error { completionHandler?(data, error) } else if let data = self.data { completionHandler?(data, nil) } else if let error = self.error { completionHandler?(nil, error) } else { completionHandler?(nil, CLDError.generalError()) } } return self } @discardableResult @objc func progress(_ progress: ((Progress) -> Void)?) -> CLDNetworkDataRequest { if let downloadRequest = self.dataDownloadRequest { downloadRequest.progress(progress) } else { self.progress = progress } return self } @objc func resume() { dataDownloadRequest?.resume() } @objc func suspend() { dataDownloadRequest?.suspend() } @objc func cancel() { dataDownloadRequest?.cancel() } @objc func response(_ completionHandler: ((_ response: Any?, _ error: NSError?) -> ())?) -> CLDNetworkRequest { responseAsset(completionHandler) return self } } ================================================ FILE: Cloudinary/Classes/ios/NetworkRequest/CLDFetchImageRequestImpl.swift ================================================ // // CLDFetchImageRequestImpl.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import UIKit internal class CLDFetchImageRequestImpl: CLDFetchImageRequest { fileprivate let url: String fileprivate let downloadCoordinator: CLDDownloadCoordinator fileprivate let closureQueue: OperationQueue fileprivate var image: UIImage? fileprivate var error: NSError? // Requests fileprivate var imageDownloadRequest: CLDNetworkDownloadRequest? fileprivate var progress: ((Progress) -> Void)? init(url: String, downloadCoordinator: CLDDownloadCoordinator) { self.url = url self.downloadCoordinator = downloadCoordinator closureQueue = { let operationQueue = OperationQueue() operationQueue.name = "com.cloudinary.CLDFetchImageRequest" operationQueue.maxConcurrentOperationCount = 1 operationQueue.isSuspended = true return operationQueue }() } // MARK: - Actions func fetchImage() { DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async { self.downloadImageAndCacheIt() } } // MARK: Private fileprivate func downloadImageAndCacheIt() { imageDownloadRequest = downloadCoordinator.download(url) as? CLDNetworkDownloadRequest imageDownloadRequest?.progress(progress) imageDownloadRequest?.responseData { [weak self] (responseData, responseError, httpCode) -> () in if let data = responseData, let image = data.cldToUIImageThreadSafe(), let url = self?.url { self?.image = image } else if let err = responseError { self?.error = err } else { let error = CLDError.error(code: .failedDownloadingImage, message: "Failed attempting to download image.", userInfo: ["statusCode": httpCode]) self?.error = error } self?.closureQueue.isSuspended = false } } // MARK: - CLDFetchImageRequest @discardableResult @objc func responseImage(_ completionHandler: CLDCompletionHandler?) -> CLDFetchImageRequest { closureQueue.addOperation { if let image = self.image { completionHandler?(image, nil) } else if let error = self.error { completionHandler?(nil, error) } else { completionHandler?(nil, CLDError.generalError()) } } return self } @discardableResult @objc func progress(_ progress: ((Progress) -> Void)?) -> CLDNetworkDataRequest { if let downloadRequest = self.imageDownloadRequest { downloadRequest.progress(progress) } else { self.progress = progress } return self } @objc func resume() { imageDownloadRequest?.resume() } @objc func suspend() { imageDownloadRequest?.suspend() } @objc func cancel() { imageDownloadRequest?.cancel() } @objc func response(_ completionHandler: ((_ response: Any?, _ error: NSError?) -> ())?) -> CLDNetworkRequest { responseImage(completionHandler) return self } } ================================================ FILE: Cloudinary/Classes/ios/UIViews/CLDResponsiveViewHelper.swift ================================================ // // CLDResponsiveViewHelper.swift // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import UIKit @objcMembers open class CLDResponsiveViewHelper : NSObject{ fileprivate var requestedWidth = 0 fileprivate var requestedHeight = 0 fileprivate var viewSizeKnown = false fileprivate var publicId: String! fileprivate var cloudinary: CLDCloudinary! fileprivate var signUrl: Bool! fileprivate var resourceType:CLDUrlResourceType! fileprivate var responsiveParams : CLDResponsiveParams? fileprivate var baseTransformation: String? fileprivate var placeholder: UIImage? public func cldSetImage(view: UIImageView, publicId: String, cloudinary: CLDCloudinary, signUrl: Bool = false, resourceType: CLDUrlResourceType = CLDUrlResourceType.image, responsiveParams: CLDResponsiveParams, transformation: CLDTransformation? = nil, placeholder: UIImage? = nil) { self.publicId = publicId self.signUrl = signUrl self.cloudinary = cloudinary self.resourceType = resourceType self.responsiveParams = responsiveParams self.baseTransformation = transformation?.asString() self.placeholder = placeholder if (viewSizeKnown){ doResponsive(view) } } open func onViewSizeKnown(view: UIImageView) { viewSizeKnown = true // if no one called cldSetImage we have nothing to do here if let params = responsiveParams { // We fetch an image in two cases: 1) This is the first time, or 2) The view got larger and the deverloper requested to reload the image on resize. if (requestedWidth == 0 && requestedHeight == 0) || (params.shouldReloadOnSizeChange && didGetLarger(view)) { doResponsive(view) } } } fileprivate func didGetLarger(_ view: UIImageView) -> Bool { return getRoundedContentHeight(view) > requestedHeight || getRoundedContentWidth(view) > requestedWidth } fileprivate func doResponsive(_ view: UIImageView){ // Only fetch an image of a responsive transformation was generated successfully: if let transformation = self.chainResponsiveTransformation(view) { view.cldSetImage(publicId: publicId, cloudinary: cloudinary, signUrl: signUrl, resourceType: resourceType, transformation: transformation, placeholder: placeholder) } } fileprivate func chainResponsiveTransformation(_ view: UIImageView) -> CLDTransformation? { guard view.bounds.width > 0 && view.bounds.height > 0 else { // nothing to do return nil } let params = responsiveParams! let responsiveTransformation = CLDTransformation() if let baseTransformation = baseTransformation { responsiveTransformation.setRawTransformation(baseTransformation).chain() } if (params.autoWidth) { requestedWidth = getRoundedContentWidth(view) responsiveTransformation.setWidth(requestedWidth) } else { requestedWidth = 0 } if (params.autoHeight) { requestedHeight = getRoundedContentHeight(view) responsiveTransformation.setHeight(requestedHeight) } else { requestedHeight = 0 } if let crop = params.cropMode { responsiveTransformation.setCrop(crop) } if let gravity = params.gravity { responsiveTransformation.setGravity(gravity) } return responsiveTransformation.setDpr(Float(UIScreen.main.scale)) } fileprivate func getRoundedContentHeight(_ view: UIImageView) -> Int { return trimAndRoundUp(Int(round(view.frame.height - view.layoutMargins.top - view.layoutMargins.bottom))) } fileprivate func getRoundedContentWidth(_ view: UIImageView) -> Int { return trimAndRoundUp(Int(round(view.frame.width - view.layoutMargins.left - view.layoutMargins.right))) } fileprivate func trimAndRoundUp(_ dimension: Int) -> Int { let value = ((dimension - 1) / responsiveParams!.stepSizePoints + 1) * responsiveParams!.stepSizePoints; return max(responsiveParams!.minDimensionPoints, min(value, responsiveParams!.maxDimensionPoints)); } } ================================================ FILE: Cloudinary/Classes/ios/UIViews/CLDUIImageView.swift ================================================ // // CLDUIImageView.swift // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import UIKit @objcMembers open class CLDUIImageView: UIImageView { // Delegate most of the logic to a helper instance. To use responsive downloads in situations // where one cannot use CLDUIImageView a CLDResponsiveViewHelper can be used in any other custom UIVIew // as long as these two methods are used. internal let responsiveHelper: CLDResponsiveViewHelper = CLDResponsiveViewHelper() /** Download an image asynchronously from the specified URL and set it to the UIImageView's image. The image is retrieved from the cache if it exists, otherwise its downloaded and cached. Note: this must be used on the main thread. - parameter publicId: The remote asset's name (e.g. the public id of an uploaded image). - parameter cloudinary: An instance of CLDCloudinary. - parameter signUrl: A boolean parameter indicating whether or not to generate a signature out of the API secret and add it to the generated URL. Default is false. - parameter resourceType The resource type of the image to download (can be useful to display video frames for thumbnails). - parameter responsiveParams An instance of CLDResponsiveParams to configure fetching a pre-scaled image to fit in the UIImageView. - parameter transformation: An instance of CLDTransformation. - parameter placeholder: A placeholder image to be set as the background image until the asynchronus download request finishes. */ @objc public func cldSetImage(publicId: String, cloudinary: CLDCloudinary, signUrl: Bool = false, resourceType: CLDUrlResourceType = CLDUrlResourceType.image, responsiveParams: CLDResponsiveParams, transformation: CLDTransformation? = nil, placeholder: UIImage? = nil) { responsiveHelper.cldSetImage (view: self, publicId: publicId, cloudinary: cloudinary, signUrl: signUrl, resourceType: resourceType, responsiveParams: responsiveParams, transformation : transformation, placeholder: placeholder) } override open func layoutSubviews() { super.layoutSubviews() if (bounds.width > 0 && bounds.height > 0) { // notify the delegate that the view now knows it's own size responsiveHelper.onViewSizeKnown(view: self) } } } ================================================ FILE: Cloudinary/Classes/ios/Uploader/CLDImagePreprocessChain.swift ================================================ // // // CLDImagePreprocessChain.swift // // Copyright (c) 2017 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import UIKit /** The CLDImagePreprocessChain is used to run preprocessing on images before uploading. It support processing, validations and encoders, all fully customizable. */ public class CLDImagePreprocessChain: CLDPreprocessChain { public override init() { } internal override func decodeResource(_ resourceData: Any) throws -> UIImage? { if let url = resourceData as? URL { if let resourceData = try? Data(contentsOf: url) { return UIImage(data: resourceData) } } else if let data = resourceData as? Data { return UIImage(data: data) } return nil } internal override func verifyEncoder() throws { if (encoder == nil) { setEncoder(CLDPreprocessHelpers.defaultImageEncoder) } } } ================================================ FILE: Cloudinary/Classes/ios/Uploader/CLDPreprocessHelpers.swift ================================================ // // // CLDPreprocessHelpers.swift // // Copyright (c) 2017 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import UIKit /** This class contains ready-to-use encoders and processing steps to be used with CLDPreprocessChain */ public class CLDPreprocessHelpers { static let defaultImageEncoder: CLDResourceEncoder = CLDPreprocessHelpers.customImageEncoder(format: EncodingFormat.PNG, quality: 100) /** Get a CLDPreprocessStep closure to send to CLDPreprocessChain. Scales down any image larger than width/height params while retaining the original aspect ratio. If the original image is already within bounds it will be returned unchanged (i.e. a smaller image won't be enlarged). - parameter width: Maximum allowed width - parameter height: Maximum allowed height - returns: A closure to use in a preprocessing chain. */ public static func limit(width: CGFloat, height: CGFloat) -> CLDPreprocessStep { return { image in if (image.size.width > width || image.size.height > height) { return resizeImage(image: image, requiredSize: CGSize(width: width, height: height)) } return image } } /** Get a CLDPreprocessStep closure to send to CLDPreprocessChain. Returns the image cropped to the requested rectangle. This step will validate that the chosen rectangle is within the image's dimensions, otherwise an exception is thrown and the CLDUploadRequest fails. - parameter cropRect: The requested crop rectangle. - returns: A closure to use in a preprocessing chain. */ public static func crop(cropRect: CGRect) -> CLDPreprocessStep { return { image in let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) guard imageRect.contains(cropRect) else { throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Crop dimensions out of bounds") } if let croppedImage = image.cld_crop(cropRect) { return croppedImage } else { throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Image cropping failed") } } } /** Get a CLDPreprocessStep closure to send to CLDPreprocessChain. Rotates any image to the requested degree. - parameter degrees: The requested rotation degree - returns: A closure to use in a preprocessing chain. */ public static func rotate(degrees: Float) -> CLDPreprocessStep { return { image in if let rotatedImage = image.cld_rotate(degrees) { return rotatedImage } return image } } /** Get a CLDResourceEncoder to use in CLDPreprocessChain. This encoder saves the image with the chosen format and quality. - parameter format: Image format to encode the image - parameter quality: Quality to save the image (ignored if the chosen format doesn't support quality) - returns: A closure to use as encoder in a preprocessing chain */ public static func customImageEncoder(format: EncodingFormat, quality: CGFloat) -> CLDResourceEncoder { return { image in if let data = encodeAs(image: image, format: format, quality: quality) { let (_, url) = CLDFileUtils.getTempFileUrl(fileName: NSUUID().uuidString, baseFolder:"imageEncoder") try? data.write(to: url) return url } return nil } } /** Get a CLDPreprocessStep to send to CLDPreprocessChain. This step will validate that a given image's dimensions are within the chosen bounds, otherwise an exception is throw and the CLDUploadRequest fails. - parameter minWidth: Minimum width allowed. - parameter maxWidth: Maximum width allowed. - parameter minHeight: Minimum height allowed. - parameter maxHeight: Maximum height allowed. - returns: A closure to use in a preprocessing chain. */ public static func dimensionsValidator(minWidth: Int, maxWidth: Int, minHeight: Int, maxHeight: Int) -> CLDPreprocessStep { return { image in if let width = image.cgImage?.width, let height = image.cgImage?.height { if (width > maxWidth || width < minWidth || height > maxHeight || height < minHeight) { throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Image dimensions invalid") } } return image } } internal static func encodeAs(image: UIImage, format: EncodingFormat, quality: CGFloat) -> Data? { switch format { case EncodingFormat.JPEG: return image.jpegData(compressionQuality: quality) case EncodingFormat.PNG: return image.pngData() } } internal static func resizeImage(image: UIImage, requiredSize: CGSize) -> UIImage { let widthRatio = requiredSize.width / image.size.width let heightRatio = requiredSize.height / image.size.height let newSize: CGSize if (heightRatio > widthRatio) { newSize = CGSize(width: requiredSize.width, height: round(widthRatio * image.size.height)) } else { newSize = CGSize(width: round(heightRatio * image.size.width), height: requiredSize.height) } var newImage: UIImage UIGraphicsBeginImageContextWithOptions(CGSize(width: newSize.width, height: newSize.height), false, 1.0) image.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)) newImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return newImage } } internal extension UIImage { func cld_crop(_ rect: CGRect) -> UIImage? { UIGraphicsBeginImageContextWithOptions(rect.size, false, self.scale) self.draw(at: CGPoint(x: -rect.origin.x, y: -rect.origin.y)) let croppedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return croppedImage } fileprivate func cld_radians(from degrees: Double) -> CGFloat { return CGFloat(degrees * Double.pi / 180.0); } func cld_rotate(_ degree: Float) -> UIImage? { var newSize = CGRect(origin: CGPoint.zero, size: self.size).applying(CGAffineTransform(rotationAngle: cld_radians(from: Double(degree)))).size // Trim off the extremely small float value to prevent core graphics from rounding it up newSize.width = floor(newSize.width) newSize.height = floor(newSize.height) UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale) let context = UIGraphicsGetCurrentContext()! // Move origin to middle context.translateBy(x: newSize.width/2, y: newSize.height/2) // Rotate around middle context.rotate(by: cld_radians(from: Double(degree))) // Draw the image at its center draw(in: CGRect(x: -self.size.width/2, y: -self.size.height/2, width: self.size.width, height: self.size.height)) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return newImage } } public enum EncodingFormat { case JPEG case PNG } ================================================ FILE: Cloudinary/Classes/ios/Uploader/CLDVideoPreprocessChain.swift ================================================ // // CLDVideoPreprocessChain.swift // // Copyright (c) 2017 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import AVKit public typealias CLDVideoPreprocessStep = (CLDVideoTranscode) throws -> CLDVideoTranscode public class CLDVideoPreprocessChain: CLDPreprocessChain { private var outputURL: URL? public override init() { super.init() } internal override func decodeResource(_ resourceData: Any) throws -> CLDVideoTranscode? { if let url = resourceData as? URL { return CLDVideoTranscode(sourceURL: url) } else if let data = resourceData as? Data { return try handleLargeVideoData(data) } else { throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Resource type should be URL or Data only!") } } private func handleLargeVideoData(_ data: Data) throws -> CLDVideoTranscode? { var tempDirectory: URL! if #available(iOS 10.0, *) { tempDirectory = FileManager.default.temporaryDirectory } else { tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory()) } let tempURL = tempDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("mp4") do { try data.write(to: tempURL) return CLDVideoTranscode(sourceURL: tempURL) } catch { throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Failed to write data to temporary file: \(error.localizedDescription)") } } public func setOutputFormat(format: AVFileType) -> Self { addStep { videoTranscode in try videoTranscode.setOutputFormat(format: format) return videoTranscode } return self } public func setOutputDimensions(dimensions: CGSize) -> Self { addStep { videoTranscode in videoTranscode.setOutputDimensions(dimensions: dimensions) return videoTranscode } return self } public func setCompressionPreset(preset: String) -> Self { addStep { videoTranscode in videoTranscode.setCompressionPreset(preset: preset) return videoTranscode } return self } internal override func execute(resourceData: Any) throws -> URL { try verifyEncoder() guard let videoTranscode = try decodeResource(resourceData) else { throw CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Error decoding resource") } let dispatchGroup = DispatchGroup() var resultURL: URL? var resultError: Error? dispatchGroup.enter() DispatchQueue.global(qos: .background).async { do { var processedTranscode = videoTranscode for preprocess in self.chain { processedTranscode = try preprocess(processedTranscode) } processedTranscode.transcode { success, error in if success, let outputURL = processedTranscode.outputURL { resultURL = outputURL } else { resultError = error ?? CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Error transcoding video") } dispatchGroup.leave() } } catch { resultError = error dispatchGroup.leave() } } dispatchGroup.wait() if let finalOutputURL = resultURL { return finalOutputURL } else { throw resultError ?? CLDError.error(code: CLDError.CloudinaryErrorCode.preprocessingError, message: "Unknown error") } } internal override func verifyEncoder() throws { if encoder == nil { encoder = { _ in self.outputURL } } } } ================================================ FILE: Cloudinary/Classes/ios/Uploader/CLDVideoPreprocessHelpers.swift ================================================ import Foundation import AVKit public class CLDVideoPreprocessHelpers { private var steps: [(CLDVideoTranscode) throws -> CLDVideoTranscode] = [] public func addStep(_ step: @escaping (CLDVideoTranscode) throws -> CLDVideoTranscode) { steps.append(step) } public static func setOutputFormat(format: AVFileType) -> CLDVideoPreprocessStep { return { videoTranscode in do { try videoTranscode.setOutputFormat(format: format) } catch { throw error } return videoTranscode } } public static func setOutputDimensions(dimensions: CGSize) -> CLDVideoPreprocessStep { return { videoTranscode in videoTranscode.setOutputDimensions(dimensions: dimensions) return videoTranscode } } public static func setCompressionPreset(preset: String) -> CLDVideoPreprocessStep { return { videoTranscode in videoTranscode.setCompressionPreset(preset: preset) return videoTranscode } } public static func dimensionsValidator(minWidth: CGFloat, maxWidth: CGFloat, minHeight: CGFloat, maxHeight: CGFloat) -> CLDVideoPreprocessStep { return { videoTranscode in guard let dimensions = videoTranscode.outputDimensions else { throw NSError(domain: "CLDVideoPreprocessHelpers", code: -1, userInfo: [NSLocalizedDescriptionKey: "Dimensions not set"]) } if dimensions.width < minWidth || dimensions.width > maxWidth || dimensions.height < minHeight || dimensions.height > maxHeight { throw VideoPreprocessError.dimensionsOutOfRange } return videoTranscode } } } enum VideoPreprocessError: Error { case dimensionsOutOfRange } ================================================ FILE: Cloudinary/Classes/ios/Uploader/CLDVideoTranscode.swift ================================================ import Foundation import AVFoundation public class CLDVideoTranscode { let sourceURL: URL var outputURL: URL? var outputFormat: AVFileType = .mov var outputDimensions: CGSize? var compressionPreset: String = AVAssetExportPresetPassthrough // Default to passthrough init(sourceURL: URL) { self.sourceURL = sourceURL } func setOutputFormat(format: AVFileType) throws { guard FileManager.default.fileExists(atPath: sourceURL.path) else { throw NSError(domain: "CLDVideoPreprocessHelpers", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid source URL"]) } let asset = AVAsset(url: sourceURL) guard asset.tracks(withMediaType: .video).first != nil else { throw NSError(domain: "CLDVideoPreprocessHelpers", code: -1, userInfo: [NSLocalizedDescriptionKey: "No video track found"]) } self.outputFormat = format } func setOutputDimensions(dimensions: CGSize) { self.outputDimensions = dimensions } func setCompressionPreset(preset: String) { self.compressionPreset = preset } var hasVideoTrack: Bool { let asset = AVAsset(url: sourceURL) return asset.tracks(withMediaType: .video).first != nil } func transcode(completion: @escaping (Bool, Error?) -> Void) { let asset = AVAsset(url: sourceURL) let outputURL = generateOutputURL() do { // Create asset reader let assetReader = try AVAssetReader(asset: asset) guard let videoTrack = asset.tracks(withMediaType: .video).first else { throw NSError(domain: "CLDVideoTranscode", code: -1, userInfo: [NSLocalizedDescriptionKey: "No video track found"]) } // Configure output settings for AVAssetReader let assetReaderOutputSettings = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB ] let assetReaderOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: assetReaderOutputSettings) // Add the output to the reader assetReader.add(assetReaderOutput) // Create asset writer let assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: outputFormat) let videoSettings: [String: Any] = [ AVVideoWidthKey: outputDimensions?.width ?? videoTrack.naturalSize.width, AVVideoHeightKey: outputDimensions?.height ?? videoTrack.naturalSize.height ] let assetWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) assetWriterInput.expectsMediaDataInRealTime = true assetWriter.add(assetWriterInput) // Start reading and writing assetReader.startReading() assetWriter.startWriting() assetWriter.startSession(atSourceTime: .zero) assetWriterInput.requestMediaDataWhenReady(on: DispatchQueue(label: "assetWriterQueue")) { while assetWriterInput.isReadyForMoreMediaData { guard assetReader.status == .reading else { let error = assetReader.error ?? NSError(domain: "CLDVideoTranscode", code: -1, userInfo: [NSLocalizedDescriptionKey: "Asset reader status is not reading"]) completion(false, error) return } if let sampleBuffer = assetReaderOutput.copyNextSampleBuffer() { assetWriterInput.append(sampleBuffer) } else { assetWriterInput.markAsFinished() assetWriter.finishWriting { defer { if self.sourceURL.isFileURL { try? FileManager.default.removeItem(at: self.sourceURL) } } if assetWriter.status == .completed { self.outputURL = assetWriter.outputURL completion(true, nil) } else { completion(false, assetWriter.error) } } break } } } } catch { completion(false, error) } } private func generateOutputURL() -> URL { return URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension(outputFormat.fileExtension) } } private extension AVFileType { var fileExtension: String { switch self { case .mov: return "mov" case .mp4: return "mp4" case .m4v: return "m4v" default: return "mov" } } } ================================================ FILE: Cloudinary/Classes/ios/Video/Analytics/VideoAnalytics.swift ================================================ // // VideoAnalytics.swift // Cloudinary // // Created by Adi Mizrahi on 14/12/2023. // import Foundation public typealias EventDetails = [String: Any] typealias CustomerData = [String: Any] typealias VideoData = [String: Any] typealias ProvidedData = [String: Any] public enum TrackingType: String { case manual = "manual" case auto = "auto" } public enum PlayerKeyPath: String { case status = "status" case timeControlStatus = "timeControlStatus" case duration = "duration" } public enum AnalyticsType { case auto case manual case disabled } struct VideoPlayer { var type: String var version: String } public class VideoEvent { public var trackingType: TrackingType public var eventName: String public var eventTime: Int public var eventDetails: EventDetails init(trackingType: TrackingType, eventName: String, eventDetails: EventDetails? = nil) { self.trackingType = trackingType self.eventName = eventName self.eventTime = Int(Int64((Date().timeIntervalSince1970 * 1000.0).rounded())) self.eventDetails = eventDetails ?? [String: Any]() self.eventDetails[VideoEventJSONKeys.videoPlayer.rawValue] = createVideoPlayerObject() } func createVideoPlayerObject() -> [String: Any] { return [VideoEventJSONKeys.type.rawValue: "ios_player", VideoEventJSONKeys.version.rawValue: CLDNetworkCoordinator.DEFAULT_VERSION] } func createCustomerData(trackingData: [String: String]?, providedData: [String: Any]?) -> [String: Any] { var videoData = VideoData() videoData[VideoEventJSONKeys.cloudName.rawValue] = trackingData?[VideoEventJSONKeys.cloudName.rawValue] ?? "" videoData[VideoEventJSONKeys.publicId.rawValue] = trackingData?[VideoEventJSONKeys.publicId.rawValue] ?? "" var result: [String: Any] = [VideoEventJSONKeys.videoData.rawValue: videoData] if let providedData = providedData, !providedData.isEmpty { var providedDataObject = ProvidedData() providedData.forEach { (key, value) in providedDataObject[key] = value } result[VideoEventJSONKeys.providedData.rawValue] = providedDataObject } return result } func toDictonary() -> [String: Any] { var detailsDictionary: [String: Any] = [:] for (key, value) in eventDetails { detailsDictionary[key] = value } return [ VideoEventJSONKeys.eventName.rawValue: eventName, VideoEventJSONKeys.eventName.rawValue: eventTime, VideoEventJSONKeys.eventDetails.rawValue: detailsDictionary ] } } public class VideoViewStartEvent: VideoEvent { public init(trackingType: TrackingType = .auto, videoUrl: String, trackingData: [String: String]?,providedData: [String: Any]? = nil) { var eventDetails = EventDetails() eventDetails[VideoEventJSONKeys.trackingType.rawValue] = trackingType.rawValue eventDetails[VideoEventJSONKeys.videoUrl.rawValue] = videoUrl let defaultEventName = EventNames.viewStart.rawValue super.init(trackingType: trackingType, eventName: defaultEventName, eventDetails: eventDetails) super.eventDetails[VideoEventJSONKeys.customerData.rawValue] = createCustomerData(trackingData: trackingData, providedData: providedData) } } public class VideoLoadMetadata: VideoEvent { public init(trackingType: TrackingType = .auto, duration: Int, providedData: [String: Any]? = nil) { var eventDetails = EventDetails() eventDetails[VideoEventJSONKeys.trackingType.rawValue] = trackingType.rawValue eventDetails[VideoEventJSONKeys.videoDuration.rawValue] = duration let defaultEventName = EventNames.loadMetadata.rawValue super.init(trackingType: trackingType, eventName: defaultEventName, eventDetails: eventDetails) } } public class VideoViewEnd: VideoEvent { public init(trackingType: TrackingType = .auto, providedData: [String: Any]? = nil) { var eventDetails = EventDetails() eventDetails[VideoEventJSONKeys.trackingType.rawValue] = trackingType.rawValue let defaultEventName = EventNames.viewEnd.rawValue super.init(trackingType: trackingType, eventName: defaultEventName, eventDetails: eventDetails) } } public class VideoPlayEvent: VideoEvent { public init(trackingType: TrackingType = .auto, providedData: [String: Any]? = nil) { let defaultEventName = EventNames.play.rawValue super.init(trackingType: trackingType, eventName: defaultEventName) } } public class VideoPauseEvent: VideoEvent { public init(trackingType: TrackingType = .auto, providedData: [String: Any]? = nil) { let defaultEventName = EventNames.pause.rawValue super.init(trackingType: trackingType, eventName: defaultEventName) } } public enum VideoEventJSONKeys: String { case userId = "userId" case trackingType = "trackingType" case viewId = "viewId" case events = "events" case eventName = "eventName" case eventTime = "eventTime" case eventDetails = "eventDetails" case videoPlayer = "videoPlayer" case videoUrl = "videoUrl" case videoDuration = "videoDuration" case videoPublicId = "videoPublicId" case transformation = "transfromation" case videoExtension = "videoExtension" case customerData = "customerData" case videoData = "videoData" case providedData = "providedData" case cloudName = "cloudName" case publicId = "publicId" case type = "type" case version = "version" } public enum EventNames: String { case viewStart = "viewStart" case viewEnd = "viewEnd" case loadMetadata = "loadMetadata" case play = "play" case pause = "pause" } ================================================ FILE: Cloudinary/Classes/ios/Video/Analytics/VideoEventsManager.swift ================================================ // // VideoEventsManager.swift // Cloudinary // // Created by Adi Mizrahi on 14/12/2023. // import Foundation @objcMembers public class VideoEventsManager { let CLD_ANALYTICS_ENDPOINT_PRODUCTION_URL: String = "https://video-analytics-api.cloudinary.com/v1/video-analytics" public var CLD_ANALYTICS_ENDPOINT_DEVELOPMENT_URL = "" var viewId: String var userId: String! var trackingType: TrackingType = .auto var cloudName: String? var publicId: String? public init() { viewId = UUID().uuidString.lowercased().replacingOccurrences(of: "-", with: "") userId = getUserId() } func getUserId() -> String { if let userId = UserDefaults.standard.string(forKey: "CLDVideoPlayerUserId") { return userId } else { var newUserId = UUID().uuidString newUserId = newUserId.lowercased().replacingOccurrences(of: "-", with: "") UserDefaults.standard.set(newUserId, forKey: "CLDVideoPlayerUserId") return newUserId } } public func sendViewStartEvent(videoUrl: String, providedData: [String: Any]? = nil) { let event = VideoViewStartEvent(trackingType: trackingType, videoUrl: videoUrl, trackingData: ["cloudName": cloudName ?? "", "publicId": publicId ?? ""], providedData: providedData) addEventToQueue(event: event) } public func sendViewEndEvent(providedData: [String: Any]? = nil) { let event = VideoViewEnd(trackingType: trackingType, providedData: providedData) addEventToQueue(event: event) } public func sendLoadMetadataEvent(duration: Int, providedData: [String: Any]? = nil) { let event = VideoLoadMetadata(trackingType: trackingType, duration: duration, providedData: providedData) addEventToQueue(event: event) } public func sendPlayEvent(providedData: [String: Any]? = nil) { let event = VideoPlayEvent(trackingType: trackingType, providedData: providedData) addEventToQueue(event: event) } public func sendPauseEvent(providedData: [String: Any]? = nil) { let event = VideoPauseEvent(trackingType: trackingType, providedData: providedData) addEventToQueue(event: event) } public var eventQueue = [VideoEvent]() private var timer: Timer? func addEventToQueue(event: VideoEvent) { eventQueue.append(event) } @objc public func sendEvents() { guard !eventQueue.isEmpty else { return } let eventsToSend = Array(eventQueue.prefix(eventQueue.count)) sendEventToEndpoint(childEvents: eventsToSend) eventQueue.removeFirst(eventsToSend.count) } private func buildFormDataPart(boundary: String, name: String, value: String? = nil) -> Data { var body = Data() body.append(contentsOf: "--\(boundary)\r\n".utf8) body.append(contentsOf: "Content-Disposition: form-data; name=\"\(name)\"\r\n\r\n".utf8) if let value = value { body.append(contentsOf: "\(value)\r\n".utf8) } return body } private func buildEventsData(childEvents: [VideoEvent]) -> Data? { var body = Data() var eventData: [[String: Any]] = [] for childEvent in childEvents { eventData.append([ VideoEventJSONKeys.eventName.rawValue: childEvent.eventName, VideoEventJSONKeys.eventTime.rawValue: childEvent.eventTime, VideoEventJSONKeys.eventDetails.rawValue: childEvent.eventDetails]) } do { let jsonData = try JSONSerialization.data(withJSONObject: eventData, options: []) body.append(jsonData) body.append(contentsOf: "\r\n".utf8) return body } catch { print("Error creating JSON data: \(error)") return nil } } private func sendEventToEndpoint(childEvents: [VideoEvent]) { var request = URLRequest(url: URL(string: CLD_ANALYTICS_ENDPOINT_PRODUCTION_URL)!) request.httpMethod = "POST" let boundary = "Boundary-\(UUID().uuidString)" request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") request.setValue("ios_video_player_analytics_test", forHTTPHeaderField: "User-Agent") var body = Data() // Add events data body.append(buildFormDataPart(boundary: boundary, name: "userId", value: userId)) body.append(buildFormDataPart(boundary: boundary, name: "viewId", value: viewId)) body.append(buildFormDataPart(boundary: boundary, name: "events")) body.append(buildEventsData(childEvents: childEvents)!) body.append(contentsOf: "--\(boundary)--\r\n".utf8) request.httpBody = body URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { print("Error sending event: \(error)") } else if let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) { print("Event sent successfully") } else { print("Failed to send event. Response: \(response.debugDescription)") } }.resume() } } ================================================ FILE: Cloudinary/Classes/ios/Video/CLDVideoPlayer.swift ================================================ // // CLDVideoPlayer.swift // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import AVKit @available(iOS 10.0, *) @objcMembers open class CLDVideoPlayer: AVPlayer { var automaticStreamingProfile: Bool = false var analytics: Bool = true var isIntialized: Bool = false var loadMetadataSent: Bool = false var publicId: String? var transformation: CLDTransformation? var eventsManager: VideoEventsManager = VideoEventsManager() var providedData: [String: Any]? override init() { super.init() setAnalyticsObservers() } /** Download a video asynchronously from the specified URL and set it to the AVPlayer video. - parameter publicId: The remote asset's name (e.g. the public id of an uploaded image). - parameter cloudinary: An instance of CLDCloudinary. - parameter transformation: An instance of CLDTransformation. - parameter automaticStreamingProfile: A bool to indicate the use of automatic streaming profile default: false */ public init(publicId: String, cloudinary: CLDCloudinary, transformation: CLDTransformation? = nil, automaticStreamingProfile: Bool? = false) { var cloudinary = cloudinary cloudinary.config.analyticsObject.setFeatureFlag(flag: "F") var transformation = transformation var cldUrl = cloudinary.createUrl() if automaticStreamingProfile ?? false && transformation == nil { cldUrl = cldUrl.setFormat("m3u8") transformation = CLDTransformation().setStreamingProfile("auto") } guard let urlString = cldUrl.setResourceType(.video) .setTransformation(transformation ?? CLDTransformation()) .generate(publicId), let url = URL(string: urlString) else { print("Error - could not generate URL for CLDVideoPlayer") super.init() return } super.init(url: url) } /** Initializes a CLDVideoPlayer instance, using a given AVPlayerItem. - parameter item: The player item to put into AVPlayer */ public override init(playerItem item: AVPlayerItem?) { super.init(playerItem: item) } /** Initializes a CLDVideoPlayer instance, using a given URL. - parameter url: The URL to put into AVPlayer */ public override init(url URL: URL) { super.init(url: URL) } /** Initializes a CLDVideoPlayer instance, using a given URL. - parameter url: The string to put into AVPlayer */ public init(url string: String) { guard let url = URL(string: string) else { print("Error - could not generate URL for CLDVideoPlayer") super.init() return } super.init(url: url) } public func setAnalytics(_ analyticsType: AnalyticsType, cloudName: String?, publicId: String?) { switch analyticsType { case .auto: eventsManager.trackingType = .auto eventsManager.cloudName = cloudName ?? "" eventsManager.publicId = publicId ?? self.publicId break case .manual: eventsManager.trackingType = .manual eventsManager.cloudName = cloudName ?? "" eventsManager.publicId = publicId ?? self.publicId break case .disabled: analytics = false } } public func flushEvents() { guard analytics else { return } eventsManager.sendEvents() } public func flushEventsAndCloseSession() { guard analytics else { return } eventsManager.sendViewEndEvent(providedData: providedData) flushEvents() } deinit { guard analytics else { return } removeObserver(self, forKeyPath: PlayerKeyPath.status.rawValue) removeObserver(self, forKeyPath: PlayerKeyPath.timeControlStatus.rawValue) flushEventsAndCloseSession() } func setAnalyticsObservers() { guard analytics else { return } addObserver(self, forKeyPath: PlayerKeyPath.status.rawValue, options: [.new], context: nil) addObserver(self, forKeyPath: PlayerKeyPath.timeControlStatus.rawValue, options: [.new], context: nil) } open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard let path = keyPath, let changes = change else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) return } switch path { case PlayerKeyPath.status.rawValue: if let newStatusNumber = changes[.newKey] as? NSNumber, let newStatus = AVPlayer.Status(rawValue: newStatusNumber.intValue) { handleStatusChanged(newStatus) } case PlayerKeyPath.timeControlStatus.rawValue: if let newStatusNumber = changes[.newKey] as? NSNumber, let newStatus = AVPlayer.TimeControlStatus(rawValue: newStatusNumber.intValue) { handleTimeControlStatusChanged(newStatus) } default: super.observeValue(forKeyPath: path, of: object, change: changes, context: context) } } } @available(iOS 10.0, *) extension CLDVideoPlayer { func handleStatusChanged(_ status: AVPlayer.Status) { switch status { case .readyToPlay: if let assetURL = self.currentItem?.asset as? AVURLAsset { let mediaURL = assetURL.url eventsManager.sendViewStartEvent(videoUrl: mediaURL.absoluteString, providedData: providedData) isIntialized = true loadDurationAsynchronously() } break case .failed: // Playback failed break case .unknown: // Unknown status break @unknown default: break } } private func loadDurationAsynchronously() { guard let currentItem = self.currentItem else { return } let asset = currentItem.asset let durationKey = "duration" // Load the duration key asynchronously asset.loadValuesAsynchronously(forKeys: [durationKey]) { [weak self] in DispatchQueue.main.async { guard let self = self else { return } var error: NSError? let status = asset.statusOfValue(forKey: durationKey, error: &error) switch status { case .loaded: let duration = asset.duration var durationInSeconds = 0 if duration.isValid && duration.isNumeric && !duration.isIndefinite { let durationSeconds = CMTimeGetSeconds(duration) if durationSeconds.isFinite && !durationSeconds.isNaN { durationInSeconds = Int(durationSeconds) } } if !self.loadMetadataSent { self.loadMetadataSent = true self.eventsManager.sendLoadMetadataEvent(duration: durationInSeconds) } case .failed, .cancelled: print("Failed to load duration: \(error?.localizedDescription ?? "Unknown error")") case .loading: // Still loading, could retry or wait break case .unknown: // Unknown status break @unknown default: break } } } } func handleTimeControlStatusChanged(_ status: AVPlayer.TimeControlStatus) { switch status { case .playing: eventsManager.sendPlayEvent(providedData: providedData) case .paused: // Player paused if isIntialized { if self.timeControlStatus == .paused { eventsManager.sendPauseEvent(providedData: providedData) } } case .waitingToPlayAtSpecifiedRate: break @unknown default: break } } public func setProvidedData(data: [String: Any]) { providedData = data } } ================================================ FILE: Cloudinary.podspec ================================================ # # Be sure to run `pod lib lint Cloudinary.podspec' to ensure this is a # valid spec before submitting. # # Any lines starting with a # are optional, but their use is encouraged # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| s.name = 'Cloudinary' s.version = '5.2.5' s.summary = "Cloudinary is a cloud service that offers a solution to a web application's entire image management pipeline." s.description = <<-DESC Easily upload images to the cloud. Automatically perform smart image resizing, cropping and conversion without installing any complex software. Integrate Facebook or Twitter profile image extraction in a snap, in any dimension and style to match your website’s graphics requirements. Images are seamlessly delivered through a fast CDN, and much much more. Cloudinary offers comprehensive APIs and administration capabilities and is easy to integrate with any web application, existing or new. Cloudinary provides URL and HTTP based APIs that can be easily integrated with any Web development framework. DESC s.homepage = 'http://cloudinary.com' s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { "Cloudinary" => "info@cloudinary.com" } s.source = { :git => "https://github.com/cloudinary/cloudinary_ios.git", :tag => s.version.to_s } s.swift_version = '5.0' s.ios.deployment_target = '9.0' s.frameworks = 'UIKit', 'Foundation' s.default_subspec = 'ios' s.subspec 'ios' do |spec| spec.platform = :ios spec.source_files = 'Cloudinary/Classes/**/*.{swift,h}' spec.resource_bundles = { 'Cloudinary' => ['Cloudinary/Classes/Core/Network/PrivacyInfo.xcprivacy'] } end end ================================================ FILE: Cloudinary.xcodeproj/Cloudinary_Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: Cloudinary.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ B690BF5E2BC55992007117AC /* HTTPStatusCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B690BF5D2BC55992007117AC /* HTTPStatusCode.swift */; }; B694AAEC2B308AF100075041 /* CLDAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B694AAEB2B308AF100075041 /* CLDAnalytics.swift */; }; B694AAEF2B308B8400075041 /* VideoAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B694AAEE2B308B8400075041 /* VideoAnalytics.swift */; }; B694AAF12B308B9D00075041 /* VideoEventsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B694AAF02B308B9D00075041 /* VideoEventsManager.swift */; }; B6B4C49A2C560FEE00C9B604 /* CLDVideoPreprocessChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4C4992C560FEE00C9B604 /* CLDVideoPreprocessChain.swift */; }; B6B4C49C2C56100600C9B604 /* CLDVideoTranscode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4C49B2C56100600C9B604 /* CLDVideoTranscode.swift */; }; B6B4C4A12C5615CF00C9B604 /* CLDVideoPreprocessHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4C4A02C5615CF00C9B604 /* CLDVideoPreprocessHelpers.swift */; }; B6C2D4862A724C4200AA0039 /* CLDVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C2D4852A724C4200AA0039 /* CLDVideoPlayer.swift */; }; OBJ_257 /* CLDNConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* CLDNConvertible.swift */; }; OBJ_258 /* CLDNError.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* CLDNError.swift */; }; OBJ_259 /* CLDNMultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* CLDNMultipartFormData.swift */; }; OBJ_260 /* CLDNParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* CLDNParameterEncoding.swift */; }; OBJ_261 /* CLDNRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* CLDNRequest.swift */; }; OBJ_262 /* CLDNResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* CLDNResponse.swift */; }; OBJ_263 /* CLDNResponseSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* CLDNResponseSerialization.swift */; }; OBJ_264 /* CLDNResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* CLDNResult.swift */; }; OBJ_265 /* CLDNSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_18 /* CLDNSessionDelegate.swift */; }; OBJ_266 /* CLDNSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* CLDNSessionManager.swift */; }; OBJ_267 /* CLDNTaskDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* CLDNTaskDelegate.swift */; }; OBJ_268 /* CLDNTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* CLDNTimeline.swift */; }; OBJ_269 /* CLDNValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_22 /* CLDNValidation.swift */; }; OBJ_270 /* DispatchQueue+Cloudinary.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_23 /* DispatchQueue+Cloudinary.swift */; }; OBJ_271 /* CLDCloudinary.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_24 /* CLDCloudinary.swift */; }; OBJ_272 /* CLDCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_25 /* CLDCompatibility.swift */; }; OBJ_273 /* CLDConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* CLDConfiguration.swift */; }; OBJ_274 /* CLDEagerTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_27 /* CLDEagerTransformation.swift */; }; OBJ_275 /* CLDResponsiveParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_28 /* CLDResponsiveParams.swift */; }; OBJ_304 /* CLDDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_73 /* CLDDownloader.swift */; }; OBJ_305 /* CLDBaseNetworkObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_75 /* CLDBaseNetworkObject.swift */; }; OBJ_306 /* CLDConditionExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_76 /* CLDConditionExpression.swift */; }; OBJ_307 /* CLDDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_77 /* CLDDefinitions.swift */; }; OBJ_308 /* CLDExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_78 /* CLDExpression.swift */; }; OBJ_309 /* CLDOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_79 /* CLDOperators.swift */; }; OBJ_310 /* CLDTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_80 /* CLDTransformation.swift */; }; OBJ_311 /* CLDVariable.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_81 /* CLDVariable.swift */; }; OBJ_312 /* CLDFetchLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_83 /* CLDFetchLayer.swift */; }; OBJ_313 /* CLDLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_84 /* CLDLayer.swift */; }; OBJ_314 /* CLDSubtitlesLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_85 /* CLDSubtitlesLayer.swift */; }; OBJ_315 /* CLDTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_86 /* CLDTextLayer.swift */; }; OBJ_316 /* CLDRequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_88 /* CLDRequestParams.swift */; }; OBJ_317 /* CLDRequestParamsHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_90 /* CLDRequestParamsHelpers.swift */; }; OBJ_318 /* CLDRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_92 /* CLDRequest.swift */; }; OBJ_319 /* CLDBaseResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_94 /* CLDBaseResult.swift */; }; OBJ_320 /* CLDAccessibilityAnalysisResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_97 /* CLDAccessibilityAnalysisResult.swift */; }; OBJ_321 /* CLDColorblindAccessibilityAnalysisResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_98 /* CLDColorblindAccessibilityAnalysisResult.swift */; }; OBJ_322 /* CLDBoundingBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_99 /* CLDBoundingBox.swift */; }; OBJ_323 /* CLDDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_100 /* CLDDetection.swift */; }; OBJ_324 /* CLDFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_101 /* CLDFace.swift */; }; OBJ_325 /* CLDInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_102 /* CLDInfo.swift */; }; OBJ_326 /* CLDAdvOcrResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_104 /* CLDAdvOcrResult.swift */; }; OBJ_327 /* CLDOcrBlockResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_105 /* CLDOcrBlockResult.swift */; }; OBJ_328 /* CLDOcrBoundindBlockResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_106 /* CLDOcrBoundindBlockResult.swift */; }; OBJ_329 /* CLDOcrDataResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_107 /* CLDOcrDataResult.swift */; }; OBJ_330 /* CLDOcrDetectedLanguagesResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_108 /* CLDOcrDetectedLanguagesResult.swift */; }; OBJ_331 /* CLDOcrFullTextAnnotationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_109 /* CLDOcrFullTextAnnotationResult.swift */; }; OBJ_332 /* CLDOcrPageResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_110 /* CLDOcrPageResult.swift */; }; OBJ_333 /* CLDOcrParagraphResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_111 /* CLDOcrParagraphResult.swift */; }; OBJ_334 /* CLDOcrPropertyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_112 /* CLDOcrPropertyResult.swift */; }; OBJ_335 /* CLDOcrResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_113 /* CLDOcrResult.swift */; }; OBJ_336 /* CLDOcrSymbolResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_114 /* CLDOcrSymbolResult.swift */; }; OBJ_337 /* CLDOcrTextAnnotationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_115 /* CLDOcrTextAnnotationResult.swift */; }; OBJ_338 /* CLDOcrWordResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_116 /* CLDOcrWordResult.swift */; }; OBJ_339 /* CLDQualityAnalysis.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_118 /* CLDQualityAnalysis.swift */; }; OBJ_340 /* CLDRekognitionFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_119 /* CLDRekognitionFace.swift */; }; OBJ_341 /* CommonResultKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_120 /* CommonResultKeys.swift */; }; OBJ_342 /* CLDManagementApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_122 /* CLDManagementApi.swift */; }; OBJ_343 /* CLDDeleteRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_124 /* CLDDeleteRequest.swift */; }; OBJ_344 /* CLDExplicitRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_125 /* CLDExplicitRequest.swift */; }; OBJ_345 /* CLDExplodeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_126 /* CLDExplodeRequest.swift */; }; OBJ_346 /* CLDMultiRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_127 /* CLDMultiRequest.swift */; }; OBJ_347 /* CLDRenameRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_128 /* CLDRenameRequest.swift */; }; OBJ_348 /* CLDSpriteRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_129 /* CLDSpriteRequest.swift */; }; OBJ_349 /* CLDTagRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_130 /* CLDTagRequest.swift */; }; OBJ_350 /* CLDTextRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_131 /* CLDTextRequest.swift */; }; OBJ_351 /* CLDDeleteByTokenRequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_133 /* CLDDeleteByTokenRequestParams.swift */; }; OBJ_352 /* CLDDestroyRequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_134 /* CLDDestroyRequestParams.swift */; }; OBJ_353 /* CLDExplicitRequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_135 /* CLDExplicitRequestParams.swift */; }; OBJ_354 /* CLDExplodeRequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_136 /* CLDExplodeRequestParams.swift */; }; OBJ_355 /* CLDMultiRequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_137 /* CLDMultiRequestParams.swift */; }; OBJ_356 /* CLDRenameRequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_138 /* CLDRenameRequestParams.swift */; }; OBJ_357 /* CLDSpriteRequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_139 /* CLDSpriteRequestParams.swift */; }; OBJ_358 /* CLDTagsRequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_140 /* CLDTagsRequestParams.swift */; }; OBJ_359 /* CLDTextRequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_141 /* CLDTextRequestParams.swift */; }; OBJ_360 /* CLDDeleteResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_143 /* CLDDeleteResult.swift */; }; OBJ_361 /* CLDExplicitResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_144 /* CLDExplicitResult.swift */; }; OBJ_362 /* CLDExplodeResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_145 /* CLDExplodeResult.swift */; }; OBJ_363 /* CLDMultiResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_146 /* CLDMultiResult.swift */; }; OBJ_364 /* CLDRenameResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_147 /* CLDRenameResult.swift */; }; OBJ_365 /* CLDSpriteResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_148 /* CLDSpriteResult.swift */; }; OBJ_366 /* CLDTagResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_149 /* CLDTagResult.swift */; }; OBJ_367 /* CLDTextResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_150 /* CLDTextResult.swift */; }; OBJ_368 /* CLDUploaderWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_152 /* CLDUploaderWidget.swift */; }; OBJ_369 /* CLDCropOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_154 /* CLDCropOverlayView.swift */; }; OBJ_370 /* CLDCropScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_155 /* CLDCropScrollView.swift */; }; OBJ_371 /* CLDCropScrollViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_156 /* CLDCropScrollViewController.swift */; }; OBJ_372 /* CLDCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_157 /* CLDCropView.swift */; }; OBJ_373 /* CLDCropViewCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_158 /* CLDCropViewCalculator.swift */; }; OBJ_374 /* CLDCropViewUIManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_159 /* CLDCropViewUIManager.swift */; }; OBJ_375 /* CLDWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_161 /* CLDWidgetConfiguration.swift */; }; OBJ_376 /* CLDWidgetAssetContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_163 /* CLDWidgetAssetContainer.swift */; }; OBJ_377 /* CLDWidgetPreviewCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_164 /* CLDWidgetPreviewCollectionCell.swift */; }; OBJ_378 /* BackIconInstructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_166 /* BackIconInstructions.swift */; }; OBJ_379 /* CropIconInstructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_167 /* CropIconInstructions.swift */; }; OBJ_380 /* CropRotateIconInstructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_168 /* CropRotateIconInstructions.swift */; }; OBJ_381 /* DoneIconInstructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_169 /* DoneIconInstructions.swift */; }; OBJ_382 /* RatioLockedIconInstructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_170 /* RatioLockedIconInstructions.swift */; }; OBJ_383 /* RatioOpenedIconInstructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_171 /* RatioOpenedIconInstructions.swift */; }; OBJ_384 /* RotateIconInstructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_172 /* RotateIconInstructions.swift */; }; OBJ_385 /* CLDDisplayLinkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_174 /* CLDDisplayLinkObserver.swift */; }; OBJ_386 /* CLDVideoControlsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_175 /* CLDVideoControlsState.swift */; }; OBJ_387 /* CLDVideoControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_176 /* CLDVideoControlsView.swift */; }; OBJ_388 /* CLDVideoHiddenAndPausedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_177 /* CLDVideoHiddenAndPausedState.swift */; }; OBJ_389 /* CLDVideoHiddenAndPlayingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_178 /* CLDVideoHiddenAndPlayingState.swift */; }; OBJ_390 /* CLDVideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_179 /* CLDVideoPlayerView.swift */; }; OBJ_391 /* CLDVideoShownAndPausedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_180 /* CLDVideoShownAndPausedState.swift */; }; OBJ_392 /* CLDVideoShownAndPlayingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_181 /* CLDVideoShownAndPlayingState.swift */; }; OBJ_393 /* CLDVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_182 /* CLDVideoView.swift */; }; OBJ_394 /* CLDWidgetEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_184 /* CLDWidgetEditViewController.swift */; }; OBJ_395 /* CLDWidgetPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_185 /* CLDWidgetPreviewViewController.swift */; }; OBJ_396 /* CLDWidgetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_186 /* CLDWidgetViewController.swift */; }; OBJ_397 /* CLDUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_188 /* CLDUploader.swift */; }; OBJ_398 /* CLDPreprocessChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_190 /* CLDPreprocessChain.swift */; }; OBJ_399 /* CLDUploadRequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_192 /* CLDUploadRequestParams.swift */; }; OBJ_400 /* CLDDefaultUploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_194 /* CLDDefaultUploadRequest.swift */; }; OBJ_401 /* CLDUploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_195 /* CLDUploadRequest.swift */; }; OBJ_402 /* CLDUploadRequestWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_196 /* CLDUploadRequestWrapper.swift */; }; OBJ_403 /* CLDUploadResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_198 /* CLDUploadResult.swift */; }; OBJ_404 /* CLDUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_200 /* CLDUrl.swift */; }; OBJ_405 /* CLDNetworkAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_203 /* CLDNetworkAdapter.swift */; }; OBJ_406 /* CLDDefaultNetworkAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_204 /* CLDDefaultNetworkAdapter.swift */; }; OBJ_407 /* CLDNetworkCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_205 /* CLDNetworkCoordinator.swift */; }; OBJ_408 /* CLDAsyncNetworkUploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_207 /* CLDAsyncNetworkUploadRequest.swift */; }; OBJ_409 /* CLDErrorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_208 /* CLDErrorRequest.swift */; }; OBJ_410 /* CLDGenericNetworkRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_209 /* CLDGenericNetworkRequest.swift */; }; OBJ_411 /* CLDNetworkDataRequestImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_210 /* CLDNetworkDataRequestImpl.swift */; }; OBJ_412 /* CLDNetworkDownloadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_211 /* CLDNetworkDownloadRequest.swift */; }; OBJ_413 /* CLDNetworkUploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_212 /* CLDNetworkUploadRequest.swift */; }; OBJ_414 /* CLDBuildParamsUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_214 /* CLDBuildParamsUtils.swift */; }; OBJ_415 /* CLDCryptoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_215 /* CLDCryptoUtils.swift */; }; OBJ_416 /* CLDDictionaryUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_216 /* CLDDictionaryUtils.swift */; }; OBJ_417 /* CLDError.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_217 /* CLDError.swift */; }; OBJ_418 /* CLDFileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_218 /* CLDFileUtils.swift */; }; OBJ_420 /* CLDImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_221 /* CLDImageGenerator.swift */; }; OBJ_421 /* CLDImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_222 /* CLDImageUtils.swift */; }; OBJ_422 /* CLDJsonUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_223 /* CLDJsonUtils.swift */; }; OBJ_423 /* CLDLogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_224 /* CLDLogManager.swift */; }; OBJ_424 /* CLDStringUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_225 /* CLDStringUtils.swift */; }; OBJ_425 /* CLDTransformation+Ios.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_228 /* CLDTransformation+Ios.swift */; }; OBJ_426 /* ExtensionCLDDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_229 /* ExtensionCLDDownloader.swift */; }; OBJ_427 /* UIButton+Cloudinary.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_230 /* UIButton+Cloudinary.swift */; }; OBJ_428 /* UIImageView+Cloudinary.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_231 /* UIImageView+Cloudinary.swift */; }; OBJ_429 /* UIView+Cloudinary.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_232 /* UIView+Cloudinary.swift */; }; OBJ_430 /* CLDFetchAssetRequestImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_234 /* CLDFetchAssetRequestImpl.swift */; }; OBJ_431 /* CLDFetchImageRequestImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_235 /* CLDFetchImageRequestImpl.swift */; }; OBJ_432 /* CLDResponsiveViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_237 /* CLDResponsiveViewHelper.swift */; }; OBJ_433 /* CLDUIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_238 /* CLDUIImageView.swift */; }; OBJ_434 /* CLDImagePreprocessChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_240 /* CLDImagePreprocessChain.swift */; }; OBJ_435 /* CLDPreprocessHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_241 /* CLDPreprocessHelpers.swift */; }; OBJ_442 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ B690BF5D2BC55992007117AC /* HTTPStatusCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPStatusCode.swift; path = CacheSystem/Enum/HTTPStatusCode.swift; sourceTree = ""; }; B694AAEB2B308AF100075041 /* CLDAnalytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLDAnalytics.swift; sourceTree = ""; }; B694AAEE2B308B8400075041 /* VideoAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoAnalytics.swift; sourceTree = ""; }; B694AAF02B308B9D00075041 /* VideoEventsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoEventsManager.swift; sourceTree = ""; }; B6B4C4992C560FEE00C9B604 /* CLDVideoPreprocessChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoPreprocessChain.swift; sourceTree = ""; }; B6B4C49B2C56100600C9B604 /* CLDVideoTranscode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoTranscode.swift; sourceTree = ""; }; B6B4C4A02C5615CF00C9B604 /* CLDVideoPreprocessHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoPreprocessHelpers.swift; sourceTree = ""; }; B6C2D4852A724C4200AA0039 /* CLDVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoPlayer.swift; sourceTree = ""; }; "Cloudinary::Cloudinary::Product" /* Cloudinary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Cloudinary.framework; sourceTree = BUILT_PRODUCTS_DIR; }; OBJ_10 /* CLDNConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNConvertible.swift; sourceTree = ""; }; OBJ_100 /* CLDDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDDetection.swift; sourceTree = ""; }; OBJ_101 /* CLDFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDFace.swift; sourceTree = ""; }; OBJ_102 /* CLDInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDInfo.swift; sourceTree = ""; }; OBJ_104 /* CLDAdvOcrResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDAdvOcrResult.swift; sourceTree = ""; }; OBJ_105 /* CLDOcrBlockResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOcrBlockResult.swift; sourceTree = ""; }; OBJ_106 /* CLDOcrBoundindBlockResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOcrBoundindBlockResult.swift; sourceTree = ""; }; OBJ_107 /* CLDOcrDataResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOcrDataResult.swift; sourceTree = ""; }; OBJ_108 /* CLDOcrDetectedLanguagesResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOcrDetectedLanguagesResult.swift; sourceTree = ""; }; OBJ_109 /* CLDOcrFullTextAnnotationResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOcrFullTextAnnotationResult.swift; sourceTree = ""; }; OBJ_11 /* CLDNError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNError.swift; sourceTree = ""; }; OBJ_110 /* CLDOcrPageResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOcrPageResult.swift; sourceTree = ""; }; OBJ_111 /* CLDOcrParagraphResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOcrParagraphResult.swift; sourceTree = ""; }; OBJ_112 /* CLDOcrPropertyResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOcrPropertyResult.swift; sourceTree = ""; }; OBJ_113 /* CLDOcrResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOcrResult.swift; sourceTree = ""; }; OBJ_114 /* CLDOcrSymbolResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOcrSymbolResult.swift; sourceTree = ""; }; OBJ_115 /* CLDOcrTextAnnotationResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOcrTextAnnotationResult.swift; sourceTree = ""; }; OBJ_116 /* CLDOcrWordResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOcrWordResult.swift; sourceTree = ""; }; OBJ_118 /* CLDQualityAnalysis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDQualityAnalysis.swift; sourceTree = ""; }; OBJ_119 /* CLDRekognitionFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDRekognitionFace.swift; sourceTree = ""; }; OBJ_12 /* CLDNMultipartFormData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNMultipartFormData.swift; sourceTree = ""; }; OBJ_120 /* CommonResultKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonResultKeys.swift; sourceTree = ""; }; OBJ_122 /* CLDManagementApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDManagementApi.swift; sourceTree = ""; }; OBJ_124 /* CLDDeleteRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDDeleteRequest.swift; sourceTree = ""; }; OBJ_125 /* CLDExplicitRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDExplicitRequest.swift; sourceTree = ""; }; OBJ_126 /* CLDExplodeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDExplodeRequest.swift; sourceTree = ""; }; OBJ_127 /* CLDMultiRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDMultiRequest.swift; sourceTree = ""; }; OBJ_128 /* CLDRenameRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDRenameRequest.swift; sourceTree = ""; }; OBJ_129 /* CLDSpriteRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDSpriteRequest.swift; sourceTree = ""; }; OBJ_13 /* CLDNParameterEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNParameterEncoding.swift; sourceTree = ""; }; OBJ_130 /* CLDTagRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDTagRequest.swift; sourceTree = ""; }; OBJ_131 /* CLDTextRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDTextRequest.swift; sourceTree = ""; }; OBJ_133 /* CLDDeleteByTokenRequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDDeleteByTokenRequestParams.swift; sourceTree = ""; }; OBJ_134 /* CLDDestroyRequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDDestroyRequestParams.swift; sourceTree = ""; }; OBJ_135 /* CLDExplicitRequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDExplicitRequestParams.swift; sourceTree = ""; }; OBJ_136 /* CLDExplodeRequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDExplodeRequestParams.swift; sourceTree = ""; }; OBJ_137 /* CLDMultiRequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDMultiRequestParams.swift; sourceTree = ""; }; OBJ_138 /* CLDRenameRequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDRenameRequestParams.swift; sourceTree = ""; }; OBJ_139 /* CLDSpriteRequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDSpriteRequestParams.swift; sourceTree = ""; }; OBJ_14 /* CLDNRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNRequest.swift; sourceTree = ""; }; OBJ_140 /* CLDTagsRequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDTagsRequestParams.swift; sourceTree = ""; }; OBJ_141 /* CLDTextRequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDTextRequestParams.swift; sourceTree = ""; }; OBJ_143 /* CLDDeleteResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDDeleteResult.swift; sourceTree = ""; }; OBJ_144 /* CLDExplicitResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDExplicitResult.swift; sourceTree = ""; }; OBJ_145 /* CLDExplodeResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDExplodeResult.swift; sourceTree = ""; }; OBJ_146 /* CLDMultiResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDMultiResult.swift; sourceTree = ""; }; OBJ_147 /* CLDRenameResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDRenameResult.swift; sourceTree = ""; }; OBJ_148 /* CLDSpriteResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDSpriteResult.swift; sourceTree = ""; }; OBJ_149 /* CLDTagResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDTagResult.swift; sourceTree = ""; }; OBJ_15 /* CLDNResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNResponse.swift; sourceTree = ""; }; OBJ_150 /* CLDTextResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDTextResult.swift; sourceTree = ""; }; OBJ_152 /* CLDUploaderWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDUploaderWidget.swift; sourceTree = ""; }; OBJ_154 /* CLDCropOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDCropOverlayView.swift; sourceTree = ""; }; OBJ_155 /* CLDCropScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDCropScrollView.swift; sourceTree = ""; }; OBJ_156 /* CLDCropScrollViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDCropScrollViewController.swift; sourceTree = ""; }; OBJ_157 /* CLDCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDCropView.swift; sourceTree = ""; }; OBJ_158 /* CLDCropViewCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDCropViewCalculator.swift; sourceTree = ""; }; OBJ_159 /* CLDCropViewUIManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDCropViewUIManager.swift; sourceTree = ""; }; OBJ_16 /* CLDNResponseSerialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNResponseSerialization.swift; sourceTree = ""; }; OBJ_161 /* CLDWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDWidgetConfiguration.swift; sourceTree = ""; }; OBJ_163 /* CLDWidgetAssetContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDWidgetAssetContainer.swift; sourceTree = ""; }; OBJ_164 /* CLDWidgetPreviewCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDWidgetPreviewCollectionCell.swift; sourceTree = ""; }; OBJ_166 /* BackIconInstructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackIconInstructions.swift; sourceTree = ""; }; OBJ_167 /* CropIconInstructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CropIconInstructions.swift; sourceTree = ""; }; OBJ_168 /* CropRotateIconInstructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CropRotateIconInstructions.swift; sourceTree = ""; }; OBJ_169 /* DoneIconInstructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoneIconInstructions.swift; sourceTree = ""; }; OBJ_17 /* CLDNResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNResult.swift; sourceTree = ""; }; OBJ_170 /* RatioLockedIconInstructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatioLockedIconInstructions.swift; sourceTree = ""; }; OBJ_171 /* RatioOpenedIconInstructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatioOpenedIconInstructions.swift; sourceTree = ""; }; OBJ_172 /* RotateIconInstructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotateIconInstructions.swift; sourceTree = ""; }; OBJ_174 /* CLDDisplayLinkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDDisplayLinkObserver.swift; sourceTree = ""; }; OBJ_175 /* CLDVideoControlsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoControlsState.swift; sourceTree = ""; }; OBJ_176 /* CLDVideoControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoControlsView.swift; sourceTree = ""; }; OBJ_177 /* CLDVideoHiddenAndPausedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoHiddenAndPausedState.swift; sourceTree = ""; }; OBJ_178 /* CLDVideoHiddenAndPlayingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoHiddenAndPlayingState.swift; sourceTree = ""; }; OBJ_179 /* CLDVideoPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoPlayerView.swift; sourceTree = ""; }; OBJ_18 /* CLDNSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNSessionDelegate.swift; sourceTree = ""; }; OBJ_180 /* CLDVideoShownAndPausedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoShownAndPausedState.swift; sourceTree = ""; }; OBJ_181 /* CLDVideoShownAndPlayingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoShownAndPlayingState.swift; sourceTree = ""; }; OBJ_182 /* CLDVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoView.swift; sourceTree = ""; }; OBJ_184 /* CLDWidgetEditViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDWidgetEditViewController.swift; sourceTree = ""; }; OBJ_185 /* CLDWidgetPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDWidgetPreviewViewController.swift; sourceTree = ""; }; OBJ_186 /* CLDWidgetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDWidgetViewController.swift; sourceTree = ""; }; OBJ_188 /* CLDUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDUploader.swift; sourceTree = ""; }; OBJ_19 /* CLDNSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNSessionManager.swift; sourceTree = ""; }; OBJ_190 /* CLDPreprocessChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDPreprocessChain.swift; sourceTree = ""; }; OBJ_192 /* CLDUploadRequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDUploadRequestParams.swift; sourceTree = ""; }; OBJ_194 /* CLDDefaultUploadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDDefaultUploadRequest.swift; sourceTree = ""; }; OBJ_195 /* CLDUploadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDUploadRequest.swift; sourceTree = ""; }; OBJ_196 /* CLDUploadRequestWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDUploadRequestWrapper.swift; sourceTree = ""; }; OBJ_198 /* CLDUploadResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDUploadResult.swift; sourceTree = ""; }; OBJ_20 /* CLDNTaskDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNTaskDelegate.swift; sourceTree = ""; }; OBJ_200 /* CLDUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDUrl.swift; sourceTree = ""; }; OBJ_203 /* CLDNetworkAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNetworkAdapter.swift; sourceTree = ""; }; OBJ_204 /* CLDDefaultNetworkAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDDefaultNetworkAdapter.swift; sourceTree = ""; }; OBJ_205 /* CLDNetworkCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNetworkCoordinator.swift; sourceTree = ""; }; OBJ_207 /* CLDAsyncNetworkUploadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDAsyncNetworkUploadRequest.swift; sourceTree = ""; }; OBJ_208 /* CLDErrorRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDErrorRequest.swift; sourceTree = ""; }; OBJ_209 /* CLDGenericNetworkRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDGenericNetworkRequest.swift; sourceTree = ""; }; OBJ_21 /* CLDNTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNTimeline.swift; sourceTree = ""; }; OBJ_210 /* CLDNetworkDataRequestImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNetworkDataRequestImpl.swift; sourceTree = ""; }; OBJ_211 /* CLDNetworkDownloadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNetworkDownloadRequest.swift; sourceTree = ""; }; OBJ_212 /* CLDNetworkUploadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNetworkUploadRequest.swift; sourceTree = ""; }; OBJ_214 /* CLDBuildParamsUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDBuildParamsUtils.swift; sourceTree = ""; }; OBJ_215 /* CLDCryptoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDCryptoUtils.swift; sourceTree = ""; }; OBJ_216 /* CLDDictionaryUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDDictionaryUtils.swift; sourceTree = ""; }; OBJ_217 /* CLDError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDError.swift; sourceTree = ""; }; OBJ_218 /* CLDFileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDFileUtils.swift; sourceTree = ""; }; OBJ_22 /* CLDNValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDNValidation.swift; sourceTree = ""; }; OBJ_221 /* CLDImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDImageGenerator.swift; sourceTree = ""; }; OBJ_222 /* CLDImageUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDImageUtils.swift; sourceTree = ""; }; OBJ_223 /* CLDJsonUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDJsonUtils.swift; sourceTree = ""; }; OBJ_224 /* CLDLogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDLogManager.swift; sourceTree = ""; }; OBJ_225 /* CLDStringUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDStringUtils.swift; sourceTree = ""; }; OBJ_228 /* CLDTransformation+Ios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLDTransformation+Ios.swift"; sourceTree = ""; }; OBJ_229 /* ExtensionCLDDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionCLDDownloader.swift; sourceTree = ""; }; OBJ_23 /* DispatchQueue+Cloudinary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Cloudinary.swift"; sourceTree = ""; }; OBJ_230 /* UIButton+Cloudinary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Cloudinary.swift"; sourceTree = ""; }; OBJ_231 /* UIImageView+Cloudinary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Cloudinary.swift"; sourceTree = ""; }; OBJ_232 /* UIView+Cloudinary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Cloudinary.swift"; sourceTree = ""; }; OBJ_234 /* CLDFetchAssetRequestImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDFetchAssetRequestImpl.swift; sourceTree = ""; }; OBJ_235 /* CLDFetchImageRequestImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDFetchImageRequestImpl.swift; sourceTree = ""; }; OBJ_237 /* CLDResponsiveViewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDResponsiveViewHelper.swift; sourceTree = ""; }; OBJ_238 /* CLDUIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDUIImageView.swift; sourceTree = ""; }; OBJ_24 /* CLDCloudinary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDCloudinary.swift; sourceTree = ""; }; OBJ_240 /* CLDImagePreprocessChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDImagePreprocessChain.swift; sourceTree = ""; }; OBJ_241 /* CLDPreprocessHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDPreprocessHelpers.swift; sourceTree = ""; }; OBJ_245 /* tools */ = {isa = PBXFileReference; lastKnownFileType = folder; path = tools; sourceTree = SOURCE_ROOT; }; OBJ_246 /* Example */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Example; sourceTree = SOURCE_ROOT; }; OBJ_247 /* Cloudinary */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Cloudinary; sourceTree = SOURCE_ROOT; }; OBJ_248 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; OBJ_249 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; OBJ_25 /* CLDCompatibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDCompatibility.swift; sourceTree = ""; }; OBJ_250 /* Cloudinary.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cloudinary.podspec; sourceTree = ""; }; OBJ_251 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; OBJ_26 /* CLDConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDConfiguration.swift; sourceTree = ""; }; OBJ_27 /* CLDEagerTransformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDEagerTransformation.swift; sourceTree = ""; }; OBJ_28 /* CLDResponsiveParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDResponsiveParams.swift; sourceTree = ""; }; OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; OBJ_73 /* CLDDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDDownloader.swift; sourceTree = ""; }; OBJ_75 /* CLDBaseNetworkObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDBaseNetworkObject.swift; sourceTree = ""; }; OBJ_76 /* CLDConditionExpression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDConditionExpression.swift; sourceTree = ""; }; OBJ_77 /* CLDDefinitions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDDefinitions.swift; sourceTree = ""; }; OBJ_78 /* CLDExpression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDExpression.swift; sourceTree = ""; }; OBJ_79 /* CLDOperators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDOperators.swift; sourceTree = ""; }; OBJ_80 /* CLDTransformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDTransformation.swift; sourceTree = ""; }; OBJ_81 /* CLDVariable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVariable.swift; sourceTree = ""; }; OBJ_83 /* CLDFetchLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDFetchLayer.swift; sourceTree = ""; }; OBJ_84 /* CLDLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDLayer.swift; sourceTree = ""; }; OBJ_85 /* CLDSubtitlesLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDSubtitlesLayer.swift; sourceTree = ""; }; OBJ_86 /* CLDTextLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDTextLayer.swift; sourceTree = ""; }; OBJ_88 /* CLDRequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDRequestParams.swift; sourceTree = ""; }; OBJ_90 /* CLDRequestParamsHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDRequestParamsHelpers.swift; sourceTree = ""; }; OBJ_92 /* CLDRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDRequest.swift; sourceTree = ""; }; OBJ_94 /* CLDBaseResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDBaseResult.swift; sourceTree = ""; }; OBJ_97 /* CLDAccessibilityAnalysisResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDAccessibilityAnalysisResult.swift; sourceTree = ""; }; OBJ_98 /* CLDColorblindAccessibilityAnalysisResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDColorblindAccessibilityAnalysisResult.swift; sourceTree = ""; }; OBJ_99 /* CLDBoundingBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDBoundingBox.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ OBJ_436 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 0; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ B694AAED2B308B7600075041 /* Analytics */ = { isa = PBXGroup; children = ( B694AAEE2B308B8400075041 /* VideoAnalytics.swift */, B694AAF02B308B9D00075041 /* VideoEventsManager.swift */, ); path = Analytics; sourceTree = ""; }; B6C2D4842A724C2F00AA0039 /* Video */ = { isa = PBXGroup; children = ( B694AAED2B308B7600075041 /* Analytics */, B6C2D4852A724C4200AA0039 /* CLDVideoPlayer.swift */, ); path = Video; sourceTree = ""; }; OBJ_103 /* CLDOcr */ = { isa = PBXGroup; children = ( OBJ_104 /* CLDAdvOcrResult.swift */, OBJ_105 /* CLDOcrBlockResult.swift */, OBJ_106 /* CLDOcrBoundindBlockResult.swift */, OBJ_107 /* CLDOcrDataResult.swift */, OBJ_108 /* CLDOcrDetectedLanguagesResult.swift */, OBJ_109 /* CLDOcrFullTextAnnotationResult.swift */, OBJ_110 /* CLDOcrPageResult.swift */, OBJ_111 /* CLDOcrParagraphResult.swift */, OBJ_112 /* CLDOcrPropertyResult.swift */, OBJ_113 /* CLDOcrResult.swift */, OBJ_114 /* CLDOcrSymbolResult.swift */, OBJ_115 /* CLDOcrTextAnnotationResult.swift */, OBJ_116 /* CLDOcrWordResult.swift */, ); path = CLDOcr; sourceTree = ""; }; OBJ_117 /* CLDQualityAnalysis */ = { isa = PBXGroup; children = ( OBJ_118 /* CLDQualityAnalysis.swift */, ); path = CLDQualityAnalysis; sourceTree = ""; }; OBJ_121 /* ManagementApi */ = { isa = PBXGroup; children = ( OBJ_122 /* CLDManagementApi.swift */, OBJ_123 /* Requests */, OBJ_132 /* RequestsParams */, OBJ_142 /* Results */, ); path = ManagementApi; sourceTree = ""; }; OBJ_123 /* Requests */ = { isa = PBXGroup; children = ( OBJ_124 /* CLDDeleteRequest.swift */, OBJ_125 /* CLDExplicitRequest.swift */, OBJ_126 /* CLDExplodeRequest.swift */, OBJ_127 /* CLDMultiRequest.swift */, OBJ_128 /* CLDRenameRequest.swift */, OBJ_129 /* CLDSpriteRequest.swift */, OBJ_130 /* CLDTagRequest.swift */, OBJ_131 /* CLDTextRequest.swift */, ); path = Requests; sourceTree = ""; }; OBJ_132 /* RequestsParams */ = { isa = PBXGroup; children = ( OBJ_133 /* CLDDeleteByTokenRequestParams.swift */, OBJ_134 /* CLDDestroyRequestParams.swift */, OBJ_135 /* CLDExplicitRequestParams.swift */, OBJ_136 /* CLDExplodeRequestParams.swift */, OBJ_137 /* CLDMultiRequestParams.swift */, OBJ_138 /* CLDRenameRequestParams.swift */, OBJ_139 /* CLDSpriteRequestParams.swift */, OBJ_140 /* CLDTagsRequestParams.swift */, OBJ_141 /* CLDTextRequestParams.swift */, ); path = RequestsParams; sourceTree = ""; }; OBJ_142 /* Results */ = { isa = PBXGroup; children = ( OBJ_143 /* CLDDeleteResult.swift */, OBJ_144 /* CLDExplicitResult.swift */, OBJ_145 /* CLDExplodeResult.swift */, OBJ_146 /* CLDMultiResult.swift */, OBJ_147 /* CLDRenameResult.swift */, OBJ_148 /* CLDSpriteResult.swift */, OBJ_149 /* CLDTagResult.swift */, OBJ_150 /* CLDTextResult.swift */, ); path = Results; sourceTree = ""; }; OBJ_151 /* UploadWidget */ = { isa = PBXGroup; children = ( OBJ_152 /* CLDUploaderWidget.swift */, OBJ_153 /* CropView */, OBJ_160 /* WidgetConfiguration */, OBJ_162 /* WidgetElements */, OBJ_165 /* WidgetImages */, OBJ_173 /* WidgetVideo */, OBJ_183 /* WidgetViewControllers */, ); path = UploadWidget; sourceTree = ""; }; OBJ_153 /* CropView */ = { isa = PBXGroup; children = ( OBJ_154 /* CLDCropOverlayView.swift */, OBJ_155 /* CLDCropScrollView.swift */, OBJ_156 /* CLDCropScrollViewController.swift */, OBJ_157 /* CLDCropView.swift */, OBJ_158 /* CLDCropViewCalculator.swift */, OBJ_159 /* CLDCropViewUIManager.swift */, ); path = CropView; sourceTree = ""; }; OBJ_160 /* WidgetConfiguration */ = { isa = PBXGroup; children = ( OBJ_161 /* CLDWidgetConfiguration.swift */, ); path = WidgetConfiguration; sourceTree = ""; }; OBJ_162 /* WidgetElements */ = { isa = PBXGroup; children = ( OBJ_163 /* CLDWidgetAssetContainer.swift */, OBJ_164 /* CLDWidgetPreviewCollectionCell.swift */, ); path = WidgetElements; sourceTree = ""; }; OBJ_165 /* WidgetImages */ = { isa = PBXGroup; children = ( OBJ_166 /* BackIconInstructions.swift */, OBJ_167 /* CropIconInstructions.swift */, OBJ_168 /* CropRotateIconInstructions.swift */, OBJ_169 /* DoneIconInstructions.swift */, OBJ_170 /* RatioLockedIconInstructions.swift */, OBJ_171 /* RatioOpenedIconInstructions.swift */, OBJ_172 /* RotateIconInstructions.swift */, ); path = WidgetImages; sourceTree = ""; }; OBJ_173 /* WidgetVideo */ = { isa = PBXGroup; children = ( OBJ_174 /* CLDDisplayLinkObserver.swift */, OBJ_175 /* CLDVideoControlsState.swift */, OBJ_176 /* CLDVideoControlsView.swift */, OBJ_177 /* CLDVideoHiddenAndPausedState.swift */, OBJ_178 /* CLDVideoHiddenAndPlayingState.swift */, OBJ_179 /* CLDVideoPlayerView.swift */, OBJ_180 /* CLDVideoShownAndPausedState.swift */, OBJ_181 /* CLDVideoShownAndPlayingState.swift */, OBJ_182 /* CLDVideoView.swift */, ); path = WidgetVideo; sourceTree = ""; }; OBJ_183 /* WidgetViewControllers */ = { isa = PBXGroup; children = ( OBJ_184 /* CLDWidgetEditViewController.swift */, OBJ_185 /* CLDWidgetPreviewViewController.swift */, OBJ_186 /* CLDWidgetViewController.swift */, ); path = WidgetViewControllers; sourceTree = ""; }; OBJ_187 /* Uploader */ = { isa = PBXGroup; children = ( OBJ_188 /* CLDUploader.swift */, OBJ_189 /* Preupload */, OBJ_191 /* RequestParams */, OBJ_193 /* Requests */, OBJ_197 /* Results */, ); path = Uploader; sourceTree = ""; }; OBJ_189 /* Preupload */ = { isa = PBXGroup; children = ( OBJ_190 /* CLDPreprocessChain.swift */, ); path = Preupload; sourceTree = ""; }; OBJ_191 /* RequestParams */ = { isa = PBXGroup; children = ( OBJ_192 /* CLDUploadRequestParams.swift */, ); path = RequestParams; sourceTree = ""; }; OBJ_193 /* Requests */ = { isa = PBXGroup; children = ( OBJ_194 /* CLDDefaultUploadRequest.swift */, OBJ_195 /* CLDUploadRequest.swift */, OBJ_196 /* CLDUploadRequestWrapper.swift */, ); path = Requests; sourceTree = ""; }; OBJ_197 /* Results */ = { isa = PBXGroup; children = ( OBJ_198 /* CLDUploadResult.swift */, ); path = Results; sourceTree = ""; }; OBJ_199 /* Url */ = { isa = PBXGroup; children = ( OBJ_200 /* CLDUrl.swift */, ); path = Url; sourceTree = ""; }; OBJ_201 /* Network */ = { isa = PBXGroup; children = ( OBJ_202 /* Adapter */, OBJ_204 /* CLDDefaultNetworkAdapter.swift */, OBJ_205 /* CLDNetworkCoordinator.swift */, OBJ_206 /* NetworkRequest */, ); path = Network; sourceTree = ""; }; OBJ_202 /* Adapter */ = { isa = PBXGroup; children = ( OBJ_203 /* CLDNetworkAdapter.swift */, ); path = Adapter; sourceTree = ""; }; OBJ_206 /* NetworkRequest */ = { isa = PBXGroup; children = ( OBJ_207 /* CLDAsyncNetworkUploadRequest.swift */, OBJ_208 /* CLDErrorRequest.swift */, OBJ_209 /* CLDGenericNetworkRequest.swift */, OBJ_210 /* CLDNetworkDataRequestImpl.swift */, OBJ_211 /* CLDNetworkDownloadRequest.swift */, OBJ_212 /* CLDNetworkUploadRequest.swift */, ); path = NetworkRequest; sourceTree = ""; }; OBJ_213 /* Utils */ = { isa = PBXGroup; children = ( OBJ_214 /* CLDBuildParamsUtils.swift */, OBJ_215 /* CLDCryptoUtils.swift */, OBJ_216 /* CLDDictionaryUtils.swift */, OBJ_217 /* CLDError.swift */, OBJ_218 /* CLDFileUtils.swift */, OBJ_220 /* CLDImageGenerator */, OBJ_222 /* CLDImageUtils.swift */, OBJ_223 /* CLDJsonUtils.swift */, OBJ_224 /* CLDLogManager.swift */, OBJ_225 /* CLDStringUtils.swift */, ); path = Utils; sourceTree = ""; }; OBJ_220 /* CLDImageGenerator */ = { isa = PBXGroup; children = ( OBJ_221 /* CLDImageGenerator.swift */, ); path = CLDImageGenerator; sourceTree = ""; }; OBJ_226 /* ios */ = { isa = PBXGroup; children = ( B6C2D4842A724C2F00AA0039 /* Video */, OBJ_227 /* Extensions */, OBJ_233 /* NetworkRequest */, OBJ_236 /* UIViews */, OBJ_239 /* Uploader */, ); path = ios; sourceTree = ""; }; OBJ_227 /* Extensions */ = { isa = PBXGroup; children = ( OBJ_228 /* CLDTransformation+Ios.swift */, OBJ_229 /* ExtensionCLDDownloader.swift */, OBJ_230 /* UIButton+Cloudinary.swift */, OBJ_231 /* UIImageView+Cloudinary.swift */, OBJ_232 /* UIView+Cloudinary.swift */, ); path = Extensions; sourceTree = ""; }; OBJ_233 /* NetworkRequest */ = { isa = PBXGroup; children = ( OBJ_234 /* CLDFetchAssetRequestImpl.swift */, OBJ_235 /* CLDFetchImageRequestImpl.swift */, ); path = NetworkRequest; sourceTree = ""; }; OBJ_236 /* UIViews */ = { isa = PBXGroup; children = ( OBJ_237 /* CLDResponsiveViewHelper.swift */, OBJ_238 /* CLDUIImageView.swift */, ); path = UIViews; sourceTree = ""; }; OBJ_239 /* Uploader */ = { isa = PBXGroup; children = ( OBJ_240 /* CLDImagePreprocessChain.swift */, OBJ_241 /* CLDPreprocessHelpers.swift */, B6B4C4992C560FEE00C9B604 /* CLDVideoPreprocessChain.swift */, B6B4C49B2C56100600C9B604 /* CLDVideoTranscode.swift */, B6B4C4A02C5615CF00C9B604 /* CLDVideoPreprocessHelpers.swift */, ); path = Uploader; sourceTree = ""; }; OBJ_242 /* Tests */ = { isa = PBXGroup; children = ( ); name = Tests; sourceTree = SOURCE_ROOT; }; OBJ_243 /* Products */ = { isa = PBXGroup; children = ( "Cloudinary::Cloudinary::Product" /* Cloudinary.framework */, ); name = Products; sourceTree = BUILT_PRODUCTS_DIR; }; OBJ_29 /* Features */ = { isa = PBXGroup; children = ( B690BF5D2BC55992007117AC /* HTTPStatusCode.swift */, OBJ_72 /* Downloader */, OBJ_74 /* Helpers */, OBJ_121 /* ManagementApi */, OBJ_151 /* UploadWidget */, OBJ_187 /* Uploader */, OBJ_199 /* Url */, ); path = Features; sourceTree = ""; }; OBJ_5 = { isa = PBXGroup; children = ( OBJ_6 /* Package.swift */, OBJ_7 /* Sources */, OBJ_242 /* Tests */, OBJ_243 /* Products */, OBJ_245 /* tools */, OBJ_246 /* Example */, OBJ_247 /* Cloudinary */, OBJ_248 /* LICENSE */, OBJ_249 /* CHANGELOG.md */, OBJ_250 /* Cloudinary.podspec */, OBJ_251 /* README.md */, ); sourceTree = ""; }; OBJ_7 /* Sources */ = { isa = PBXGroup; children = ( OBJ_8 /* Core */, OBJ_226 /* ios */, ); name = Sources; path = Cloudinary/Classes; sourceTree = SOURCE_ROOT; }; OBJ_72 /* Downloader */ = { isa = PBXGroup; children = ( OBJ_73 /* CLDDownloader.swift */, ); path = Downloader; sourceTree = ""; }; OBJ_74 /* Helpers */ = { isa = PBXGroup; children = ( OBJ_75 /* CLDBaseNetworkObject.swift */, OBJ_76 /* CLDConditionExpression.swift */, OBJ_77 /* CLDDefinitions.swift */, OBJ_78 /* CLDExpression.swift */, OBJ_79 /* CLDOperators.swift */, OBJ_80 /* CLDTransformation.swift */, OBJ_81 /* CLDVariable.swift */, OBJ_82 /* Layers */, OBJ_87 /* RequestParams */, OBJ_91 /* Requests */, OBJ_93 /* Results */, ); path = Helpers; sourceTree = ""; }; OBJ_8 /* Core */ = { isa = PBXGroup; children = ( OBJ_9 /* BaseNetwork */, OBJ_24 /* CLDCloudinary.swift */, B694AAEB2B308AF100075041 /* CLDAnalytics.swift */, OBJ_25 /* CLDCompatibility.swift */, OBJ_26 /* CLDConfiguration.swift */, OBJ_27 /* CLDEagerTransformation.swift */, OBJ_28 /* CLDResponsiveParams.swift */, OBJ_29 /* Features */, OBJ_201 /* Network */, OBJ_213 /* Utils */, ); path = Core; sourceTree = ""; }; OBJ_82 /* Layers */ = { isa = PBXGroup; children = ( OBJ_83 /* CLDFetchLayer.swift */, OBJ_84 /* CLDLayer.swift */, OBJ_85 /* CLDSubtitlesLayer.swift */, OBJ_86 /* CLDTextLayer.swift */, ); path = Layers; sourceTree = ""; }; OBJ_87 /* RequestParams */ = { isa = PBXGroup; children = ( OBJ_88 /* CLDRequestParams.swift */, OBJ_89 /* Helpers */, ); path = RequestParams; sourceTree = ""; }; OBJ_89 /* Helpers */ = { isa = PBXGroup; children = ( OBJ_90 /* CLDRequestParamsHelpers.swift */, ); path = Helpers; sourceTree = ""; }; OBJ_9 /* BaseNetwork */ = { isa = PBXGroup; children = ( OBJ_10 /* CLDNConvertible.swift */, OBJ_11 /* CLDNError.swift */, OBJ_12 /* CLDNMultipartFormData.swift */, OBJ_13 /* CLDNParameterEncoding.swift */, OBJ_14 /* CLDNRequest.swift */, OBJ_15 /* CLDNResponse.swift */, OBJ_16 /* CLDNResponseSerialization.swift */, OBJ_17 /* CLDNResult.swift */, OBJ_18 /* CLDNSessionDelegate.swift */, OBJ_19 /* CLDNSessionManager.swift */, OBJ_20 /* CLDNTaskDelegate.swift */, OBJ_21 /* CLDNTimeline.swift */, OBJ_22 /* CLDNValidation.swift */, OBJ_23 /* DispatchQueue+Cloudinary.swift */, ); path = BaseNetwork; sourceTree = ""; }; OBJ_91 /* Requests */ = { isa = PBXGroup; children = ( OBJ_92 /* CLDRequest.swift */, ); path = Requests; sourceTree = ""; }; OBJ_93 /* Results */ = { isa = PBXGroup; children = ( OBJ_94 /* CLDBaseResult.swift */, OBJ_95 /* Helpers */, ); path = Results; sourceTree = ""; }; OBJ_95 /* Helpers */ = { isa = PBXGroup; children = ( OBJ_96 /* AccessibilityAnalysis */, OBJ_99 /* CLDBoundingBox.swift */, OBJ_100 /* CLDDetection.swift */, OBJ_101 /* CLDFace.swift */, OBJ_102 /* CLDInfo.swift */, OBJ_103 /* CLDOcr */, OBJ_117 /* CLDQualityAnalysis */, OBJ_119 /* CLDRekognitionFace.swift */, OBJ_120 /* CommonResultKeys.swift */, ); path = Helpers; sourceTree = ""; }; OBJ_96 /* AccessibilityAnalysis */ = { isa = PBXGroup; children = ( OBJ_97 /* CLDAccessibilityAnalysisResult.swift */, OBJ_98 /* CLDColorblindAccessibilityAnalysisResult.swift */, ); path = AccessibilityAnalysis; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ "Cloudinary::Cloudinary" /* Cloudinary */ = { isa = PBXNativeTarget; buildConfigurationList = OBJ_253 /* Build configuration list for PBXNativeTarget "Cloudinary" */; buildPhases = ( OBJ_256 /* Sources */, OBJ_436 /* Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Cloudinary; productName = Cloudinary; productReference = "Cloudinary::Cloudinary::Product" /* Cloudinary.framework */; productType = "com.apple.product-type.framework"; }; "Cloudinary::SwiftPMPackageDescription" /* CloudinaryPackageDescription */ = { isa = PBXNativeTarget; buildConfigurationList = OBJ_438 /* Build configuration list for PBXNativeTarget "CloudinaryPackageDescription" */; buildPhases = ( OBJ_441 /* Sources */, ); buildRules = ( ); dependencies = ( ); name = CloudinaryPackageDescription; productName = CloudinaryPackageDescription; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ OBJ_1 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftMigration = 9999; LastUpgradeCheck = 9999; }; buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Cloudinary" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, ); mainGroup = OBJ_5; productRefGroup = OBJ_243 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( "Cloudinary::Cloudinary" /* Cloudinary */, "Cloudinary::SwiftPMPackageDescription" /* CloudinaryPackageDescription */, ); }; /* End PBXProject section */ /* Begin PBXSourcesBuildPhase section */ OBJ_256 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( OBJ_257 /* CLDNConvertible.swift in Sources */, OBJ_258 /* CLDNError.swift in Sources */, OBJ_259 /* CLDNMultipartFormData.swift in Sources */, OBJ_260 /* CLDNParameterEncoding.swift in Sources */, OBJ_261 /* CLDNRequest.swift in Sources */, OBJ_262 /* CLDNResponse.swift in Sources */, OBJ_263 /* CLDNResponseSerialization.swift in Sources */, OBJ_264 /* CLDNResult.swift in Sources */, OBJ_265 /* CLDNSessionDelegate.swift in Sources */, OBJ_266 /* CLDNSessionManager.swift in Sources */, OBJ_267 /* CLDNTaskDelegate.swift in Sources */, OBJ_268 /* CLDNTimeline.swift in Sources */, OBJ_269 /* CLDNValidation.swift in Sources */, OBJ_270 /* DispatchQueue+Cloudinary.swift in Sources */, OBJ_271 /* CLDCloudinary.swift in Sources */, OBJ_272 /* CLDCompatibility.swift in Sources */, OBJ_273 /* CLDConfiguration.swift in Sources */, OBJ_274 /* CLDEagerTransformation.swift in Sources */, OBJ_275 /* CLDResponsiveParams.swift in Sources */, B694AAF12B308B9D00075041 /* VideoEventsManager.swift in Sources */, OBJ_304 /* CLDDownloader.swift in Sources */, OBJ_305 /* CLDBaseNetworkObject.swift in Sources */, OBJ_306 /* CLDConditionExpression.swift in Sources */, OBJ_307 /* CLDDefinitions.swift in Sources */, OBJ_308 /* CLDExpression.swift in Sources */, OBJ_309 /* CLDOperators.swift in Sources */, OBJ_310 /* CLDTransformation.swift in Sources */, OBJ_311 /* CLDVariable.swift in Sources */, B6B4C49C2C56100600C9B604 /* CLDVideoTranscode.swift in Sources */, OBJ_312 /* CLDFetchLayer.swift in Sources */, OBJ_313 /* CLDLayer.swift in Sources */, OBJ_314 /* CLDSubtitlesLayer.swift in Sources */, OBJ_315 /* CLDTextLayer.swift in Sources */, OBJ_316 /* CLDRequestParams.swift in Sources */, OBJ_317 /* CLDRequestParamsHelpers.swift in Sources */, OBJ_318 /* CLDRequest.swift in Sources */, B6C2D4862A724C4200AA0039 /* CLDVideoPlayer.swift in Sources */, OBJ_319 /* CLDBaseResult.swift in Sources */, OBJ_320 /* CLDAccessibilityAnalysisResult.swift in Sources */, OBJ_321 /* CLDColorblindAccessibilityAnalysisResult.swift in Sources */, OBJ_322 /* CLDBoundingBox.swift in Sources */, OBJ_323 /* CLDDetection.swift in Sources */, OBJ_324 /* CLDFace.swift in Sources */, OBJ_325 /* CLDInfo.swift in Sources */, OBJ_326 /* CLDAdvOcrResult.swift in Sources */, OBJ_327 /* CLDOcrBlockResult.swift in Sources */, OBJ_328 /* CLDOcrBoundindBlockResult.swift in Sources */, OBJ_329 /* CLDOcrDataResult.swift in Sources */, OBJ_330 /* CLDOcrDetectedLanguagesResult.swift in Sources */, OBJ_331 /* CLDOcrFullTextAnnotationResult.swift in Sources */, OBJ_332 /* CLDOcrPageResult.swift in Sources */, OBJ_333 /* CLDOcrParagraphResult.swift in Sources */, OBJ_334 /* CLDOcrPropertyResult.swift in Sources */, OBJ_335 /* CLDOcrResult.swift in Sources */, OBJ_336 /* CLDOcrSymbolResult.swift in Sources */, OBJ_337 /* CLDOcrTextAnnotationResult.swift in Sources */, B6B4C49A2C560FEE00C9B604 /* CLDVideoPreprocessChain.swift in Sources */, OBJ_338 /* CLDOcrWordResult.swift in Sources */, OBJ_339 /* CLDQualityAnalysis.swift in Sources */, OBJ_340 /* CLDRekognitionFace.swift in Sources */, OBJ_341 /* CommonResultKeys.swift in Sources */, OBJ_342 /* CLDManagementApi.swift in Sources */, OBJ_343 /* CLDDeleteRequest.swift in Sources */, OBJ_344 /* CLDExplicitRequest.swift in Sources */, OBJ_345 /* CLDExplodeRequest.swift in Sources */, OBJ_346 /* CLDMultiRequest.swift in Sources */, OBJ_347 /* CLDRenameRequest.swift in Sources */, OBJ_348 /* CLDSpriteRequest.swift in Sources */, OBJ_349 /* CLDTagRequest.swift in Sources */, OBJ_350 /* CLDTextRequest.swift in Sources */, OBJ_351 /* CLDDeleteByTokenRequestParams.swift in Sources */, OBJ_352 /* CLDDestroyRequestParams.swift in Sources */, OBJ_353 /* CLDExplicitRequestParams.swift in Sources */, OBJ_354 /* CLDExplodeRequestParams.swift in Sources */, OBJ_355 /* CLDMultiRequestParams.swift in Sources */, OBJ_356 /* CLDRenameRequestParams.swift in Sources */, OBJ_357 /* CLDSpriteRequestParams.swift in Sources */, OBJ_358 /* CLDTagsRequestParams.swift in Sources */, OBJ_359 /* CLDTextRequestParams.swift in Sources */, OBJ_360 /* CLDDeleteResult.swift in Sources */, OBJ_361 /* CLDExplicitResult.swift in Sources */, OBJ_362 /* CLDExplodeResult.swift in Sources */, OBJ_363 /* CLDMultiResult.swift in Sources */, OBJ_364 /* CLDRenameResult.swift in Sources */, OBJ_365 /* CLDSpriteResult.swift in Sources */, OBJ_366 /* CLDTagResult.swift in Sources */, OBJ_367 /* CLDTextResult.swift in Sources */, OBJ_368 /* CLDUploaderWidget.swift in Sources */, OBJ_369 /* CLDCropOverlayView.swift in Sources */, OBJ_370 /* CLDCropScrollView.swift in Sources */, OBJ_371 /* CLDCropScrollViewController.swift in Sources */, OBJ_372 /* CLDCropView.swift in Sources */, OBJ_373 /* CLDCropViewCalculator.swift in Sources */, OBJ_374 /* CLDCropViewUIManager.swift in Sources */, OBJ_375 /* CLDWidgetConfiguration.swift in Sources */, OBJ_376 /* CLDWidgetAssetContainer.swift in Sources */, OBJ_377 /* CLDWidgetPreviewCollectionCell.swift in Sources */, OBJ_378 /* BackIconInstructions.swift in Sources */, B694AAEC2B308AF100075041 /* CLDAnalytics.swift in Sources */, OBJ_379 /* CropIconInstructions.swift in Sources */, OBJ_380 /* CropRotateIconInstructions.swift in Sources */, OBJ_381 /* DoneIconInstructions.swift in Sources */, OBJ_382 /* RatioLockedIconInstructions.swift in Sources */, OBJ_383 /* RatioOpenedIconInstructions.swift in Sources */, OBJ_384 /* RotateIconInstructions.swift in Sources */, OBJ_385 /* CLDDisplayLinkObserver.swift in Sources */, OBJ_386 /* CLDVideoControlsState.swift in Sources */, OBJ_387 /* CLDVideoControlsView.swift in Sources */, OBJ_388 /* CLDVideoHiddenAndPausedState.swift in Sources */, B694AAEF2B308B8400075041 /* VideoAnalytics.swift in Sources */, OBJ_389 /* CLDVideoHiddenAndPlayingState.swift in Sources */, OBJ_390 /* CLDVideoPlayerView.swift in Sources */, OBJ_391 /* CLDVideoShownAndPausedState.swift in Sources */, OBJ_392 /* CLDVideoShownAndPlayingState.swift in Sources */, OBJ_393 /* CLDVideoView.swift in Sources */, OBJ_394 /* CLDWidgetEditViewController.swift in Sources */, OBJ_395 /* CLDWidgetPreviewViewController.swift in Sources */, B690BF5E2BC55992007117AC /* HTTPStatusCode.swift in Sources */, OBJ_396 /* CLDWidgetViewController.swift in Sources */, OBJ_397 /* CLDUploader.swift in Sources */, OBJ_398 /* CLDPreprocessChain.swift in Sources */, OBJ_399 /* CLDUploadRequestParams.swift in Sources */, OBJ_400 /* CLDDefaultUploadRequest.swift in Sources */, OBJ_401 /* CLDUploadRequest.swift in Sources */, OBJ_402 /* CLDUploadRequestWrapper.swift in Sources */, OBJ_403 /* CLDUploadResult.swift in Sources */, OBJ_404 /* CLDUrl.swift in Sources */, OBJ_405 /* CLDNetworkAdapter.swift in Sources */, OBJ_406 /* CLDDefaultNetworkAdapter.swift in Sources */, OBJ_407 /* CLDNetworkCoordinator.swift in Sources */, OBJ_408 /* CLDAsyncNetworkUploadRequest.swift in Sources */, B6B4C4A12C5615CF00C9B604 /* CLDVideoPreprocessHelpers.swift in Sources */, OBJ_409 /* CLDErrorRequest.swift in Sources */, OBJ_410 /* CLDGenericNetworkRequest.swift in Sources */, OBJ_411 /* CLDNetworkDataRequestImpl.swift in Sources */, OBJ_412 /* CLDNetworkDownloadRequest.swift in Sources */, OBJ_413 /* CLDNetworkUploadRequest.swift in Sources */, OBJ_414 /* CLDBuildParamsUtils.swift in Sources */, OBJ_415 /* CLDCryptoUtils.swift in Sources */, OBJ_416 /* CLDDictionaryUtils.swift in Sources */, OBJ_417 /* CLDError.swift in Sources */, OBJ_418 /* CLDFileUtils.swift in Sources */, OBJ_420 /* CLDImageGenerator.swift in Sources */, OBJ_421 /* CLDImageUtils.swift in Sources */, OBJ_422 /* CLDJsonUtils.swift in Sources */, OBJ_423 /* CLDLogManager.swift in Sources */, OBJ_424 /* CLDStringUtils.swift in Sources */, OBJ_425 /* CLDTransformation+Ios.swift in Sources */, OBJ_426 /* ExtensionCLDDownloader.swift in Sources */, OBJ_427 /* UIButton+Cloudinary.swift in Sources */, OBJ_428 /* UIImageView+Cloudinary.swift in Sources */, OBJ_429 /* UIView+Cloudinary.swift in Sources */, OBJ_430 /* CLDFetchAssetRequestImpl.swift in Sources */, OBJ_431 /* CLDFetchImageRequestImpl.swift in Sources */, OBJ_432 /* CLDResponsiveViewHelper.swift in Sources */, OBJ_433 /* CLDUIImageView.swift in Sources */, OBJ_434 /* CLDImagePreprocessChain.swift in Sources */, OBJ_435 /* CLDPreprocessHelpers.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; OBJ_441 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( OBJ_442 /* Package.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ OBJ_254 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CURRENT_PROJECT_VERSION = 1; DRIVERKIT_DEPLOYMENT_TARGET = 19.0; ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks", ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Cloudinary.xcodeproj/Cloudinary_Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = Cloudinary; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_VERSION = 5.0; TARGET_NAME = Cloudinary; TVOS_DEPLOYMENT_TARGET = 9.0; WATCHOS_DEPLOYMENT_TARGET = 2.0; }; name = Debug; }; OBJ_255 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CURRENT_PROJECT_VERSION = 1; DRIVERKIT_DEPLOYMENT_TARGET = 19.0; ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks", ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Cloudinary.xcodeproj/Cloudinary_Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = Cloudinary; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_VERSION = 5.0; TARGET_NAME = Cloudinary; TVOS_DEPLOYMENT_TARGET = 9.0; WATCHOS_DEPLOYMENT_TARGET = 2.0; }; name = Release; }; OBJ_3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_ARC = YES; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_NS_ASSERTIONS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "SWIFT_PACKAGE=1", "DEBUG=1", ); MACOSX_DEPLOYMENT_TARGET = 10.10; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; USE_HEADERMAP = NO; }; name = Debug; }; OBJ_4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_ARC = YES; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_OPTIMIZATION_LEVEL = s; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "SWIFT_PACKAGE=1", ); MACOSX_DEPLOYMENT_TARGET = 10.10; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; USE_HEADERMAP = NO; }; name = Release; }; OBJ_439 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { LD = /usr/bin/true; OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/ManifestAPI -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk -package-description-version 5.1.0"; SWIFT_VERSION = 5.0; }; name = Debug; }; OBJ_440 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { LD = /usr/bin/true; OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/ManifestAPI -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk -package-description-version 5.1.0"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ OBJ_2 /* Build configuration list for PBXProject "Cloudinary" */ = { isa = XCConfigurationList; buildConfigurations = ( OBJ_3 /* Debug */, OBJ_4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; OBJ_253 /* Build configuration list for PBXNativeTarget "Cloudinary" */ = { isa = XCConfigurationList; buildConfigurations = ( OBJ_254 /* Debug */, OBJ_255 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; OBJ_438 /* Build configuration list for PBXNativeTarget "CloudinaryPackageDescription" */ = { isa = XCConfigurationList; buildConfigurations = ( OBJ_439 /* Debug */, OBJ_440 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = OBJ_1 /* Project object */; } ================================================ FILE: Cloudinary.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Cloudinary.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Cloudinary.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded ================================================ FILE: Cloudinary.xcodeproj/xcshareddata/xcschemes/Cloudinary-Package.xcscheme ================================================ ================================================ FILE: Example/Cloudinary/AppDelegate.swift ================================================ // // AppDelegate.swift // iOS_Geekle_Conference // // Created by Adi Mizrahi on 11/09/2023. // import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } func applicationDidEnterBackground(_ application: UIApplication) { CoreDataHelper.shared.saveContext() } func applicationWillTerminate(_ application: UIApplication) { CoreDataHelper.shared.saveContext() } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "app_icon 1.heic", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/Inner/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/Inner/ski.imageset/Contents.json ================================================ { "images" : [ { "filename" : "smart-crop-4 (4) 1.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/Inner/sofa.imageset/Contents.json ================================================ { "images" : [ { "filename" : "sofa.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/Video/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/Video/instagram.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Group.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/Video/tiktok.imageset/Contents.json ================================================ { "images" : [ { "filename" : "tiktok-white-icon.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/Video/youtube.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Youtube_shorts_icon 1.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/background_normalizing.imageset/Contents.json ================================================ { "images" : [ { "filename" : "background_normalizing.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/color_alternation.imageset/Contents.json ================================================ { "images" : [ { "filename" : "color_alternation.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/delivery-city.imageset/Contents.json ================================================ { "images" : [ { "filename" : "optimization_hero_gezokr.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/localization_branding.imageset/Contents.json ================================================ { "images" : [ { "filename" : "localization_branding.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Delivery/smart_cropping.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Rectangle 3 (1).png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Upload/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Upload/question_mark.imageset/Contents.json ================================================ { "images" : [ { "filename" : "ic_help_outline_24px.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Upload/upload_missing.imageset/Contents.json ================================================ { "images" : [ { "filename" : "picturepile-4.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/TikTok/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/TikTok/tiktok_bar_icon.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Group 5.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/TikTok/tiktok_comments.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Group 2.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/TikTok/tiktok_discover.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Vector (3).svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/TikTok/tiktok_home.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Vector (2).svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/TikTok/tiktok_inbox.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Group 3.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/TikTok/tiktok_like.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Vector (4).svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/TikTok/tiktok_more.imageset/Contents.json ================================================ { "images" : [ { "filename" : "icon_more.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/TikTok/tiktok_music.imageset/Contents.json ================================================ { "images" : [ { "filename" : "music animation.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/TikTok/tiktok_note.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Vector (1).svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/TikTok/tiktok_share.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Vector (5).svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Video/TikTok/tiktok_social_icon.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Avatar.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Widgets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/Widgets/house.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Frame_871_ao5o4r_2_viaeyi.heic", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/back_arrow.imageset/Contents.json ================================================ { "images" : [ { "filename" : "left _arrow.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/car-speed-limiter.imageset/Contents.json ================================================ { "images" : [ { "filename" : "car-speed-limiter 1.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/cloudinary_logo.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Layer 1.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/info_icon.imageset/Contents.json ================================================ { "images" : [ { "filename" : "info_icon.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/splash.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Image.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/splash_without_logo.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Splashwithoutlogo.jpg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/upload.imageset/Contents.json ================================================ { "images" : [ { "filename" : "upload-outline 1.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/video.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Frame 12.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } ================================================ FILE: Example/Cloudinary/Assets.xcassets/widgets.imageset/Contents.json ================================================ { "images" : [ { "filename" : "widgets-outline 1.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } ================================================ FILE: Example/Cloudinary/CldModel.xcdatamodeld/CldModel.xcdatamodel/contents ================================================ ================================================ FILE: Example/Cloudinary/Cloudinary_Example-Bridging-Header.h ================================================ // // Use this file to import your target's public headers that you would like to expose to Swift. // ================================================ FILE: Example/Cloudinary/Colors.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Colors.xcassets/gradient-first.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xC5", "green" : "0x48", "red" : "0x34" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xC5", "green" : "0x48", "red" : "0x34" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Colors.xcassets/gradient-second.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xFA", "green" : "0xBB", "red" : "0x28" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xFA", "green" : "0xBB", "red" : "0x28" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Colors.xcassets/primary.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xF7", "green" : "0x98", "red" : "0x46" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xF7", "green" : "0x98", "red" : "0x46" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Colors.xcassets/secondary.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0x5A", "green" : "0x47", "red" : "0x3F" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0x5A", "green" : "0x47", "red" : "0x3F" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Colors.xcassets/size_green.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xAE", "green" : "0xFC", "red" : "0xB2" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xAE", "green" : "0xFC", "red" : "0xB2" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Colors.xcassets/size_red.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0x59", "green" : "0x59", "red" : "0xFF" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0x59", "green" : "0x59", "red" : "0xFF" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Colors.xcassets/surface.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0x2D", "green" : "0x24", "red" : "0x20" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0x2D", "green" : "0x24", "red" : "0x20" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Colors.xcassets/text_not_selected.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xFE", "green" : "0xFF", "red" : "0xFF" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xFE", "green" : "0xFF", "red" : "0xFF" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/Cloudinary/Controllers/Base/BaseViewController.swift ================================================ // // BaseViewController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 04/01/2024. // import Foundation import UIKit class BaseViewController: UIViewController { @IBOutlet weak var vwContainer: UIView! @IBOutlet weak var lbTitle: UILabel! @IBOutlet weak var vwBack: UIView! var type: BaseControllerType! var innerIndex: Int = 0 var currentController: UIViewController! override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setTitle() setController() setBackButton() } private func setBackButton() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(backClicked)) vwBack.addGestureRecognizer(tapGesture) } private func setTitle() { switch type { case .Optimization: lbTitle.text = "Optimization" break case .Transform: lbTitle.text = "Transform" break case .Upload: lbTitle.text = "Upload" break case .UploadLarge: lbTitle.text = "Upload Large" break case .FetchUpload: lbTitle.text = "Fetch Upload" break case .UploadWidget: lbTitle.text = "Upload Widget" break case .ImageWidget: lbTitle.text = "Image Widget" break case .UseCases: lbTitle.text = "Use Cases" break case .none: break } } private func setController() { switch type { case .Optimization: currentController = UIStoryboard(name: "Optimization", bundle: nil).instantiateViewController(identifier: "OptimizationViewController") as! OptimizationViewController currentController.view.frame = vwContainer.bounds (currentController as! OptimizationViewController).type = .Optimization addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) case .Transform: currentController = UIStoryboard(name: "Transform", bundle: nil).instantiateViewController(identifier: "TransformViewController") as! TransformViewController (currentController as! TransformViewController).innerIndex = innerIndex currentController.view.frame = vwContainer.bounds addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) case .UseCases: currentController = UIStoryboard(name: "UseCases", bundle: nil).instantiateViewController(identifier: "UseCasesViewController") as! UseCasesViewController currentController.view.frame = vwContainer.bounds (currentController as! UseCasesViewController).innerIndex = innerIndex addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) case .Upload: currentController = (UIStoryboard(name: "UploadChoice", bundle: nil).instantiateViewController(identifier: "UploadChoiceController") as! UploadChoiceController) currentController.view.frame = vwContainer.bounds (currentController as! UploadChoiceController).type = .Upload addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) case .UploadLarge: currentController = (UIStoryboard(name: "UploadChoice", bundle: nil).instantiateViewController(identifier: "UploadChoiceController") as! UploadChoiceController) currentController.view.frame = vwContainer.bounds (currentController as! UploadChoiceController).type = .UploadLarge addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) case .FetchUpload: currentController = UIStoryboard(name: "Optimization", bundle: nil).instantiateViewController(identifier: "OptimizationViewController") as! OptimizationViewController currentController.view.frame = vwContainer.bounds (currentController as! OptimizationViewController).publicId = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Leonardo_da_Vinci_%281452-1519%29_-_The_Last_Supper_%281495-1498%29.jpg/1600px-Leonardo_da_Vinci_%281452-1519%29_-_The_Last_Supper_%281495-1498%29.jpg?20150402075721" (currentController as! OptimizationViewController).type = .FetchUpload addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) case .UploadWidget: if (currentController == nil || (currentController as? UploadWidgetViewController == nil)) { currentController = UIStoryboard(name: "UploadWidget", bundle: nil).instantiateViewController(identifier: "UploadWidgetViewController") currentController.view.frame = vwContainer.bounds addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) } case .ImageWidget: currentController = UIStoryboard(name: "ImageWidget", bundle: nil).instantiateViewController(identifier: "ImageWidgetViewController") currentController.view.frame = vwContainer.bounds addChild(currentController) vwContainer.addSubview(currentController.view) case .none: break } } @objc private func backClicked() { self.dismiss(animated: true) } } enum BaseControllerType { case Optimization case Transform case Upload case UploadLarge case FetchUpload case UploadWidget case ImageWidget case UseCases } public enum UploadViewType { case Upload case UploadLarge case UploadWidget } public enum OptimizationViewType { case Optimization case FetchUpload } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/DeliveryViewController.swift ================================================ // // DeliveryViewController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 28/12/2023. // import Foundation import UIKit class DeliveryViewController: UIViewController { @IBOutlet weak var vwOptimization: UIView! @IBOutlet weak var vwTransform: UIView! @IBOutlet weak var vwUseCases: UIView! @IBOutlet weak var lbTitle: UILabel! @IBOutlet weak var cvTransformation: UICollectionView! @IBOutlet weak var cvUseCases: UICollectionView! var transfromationCollectionController: DeliveryTransformCollectionController! var useCasesCollectionController: DeliveryUseCasesCollectionController! override func viewDidLoad() { super.viewDidLoad() setOptimizationView() setTransformationCollectionView() setUseCasesCollectionView() } private func setOptimizationView() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(optimizationClicked)) vwOptimization.addGestureRecognizer(tapGesture) } @objc private func optimizationClicked() { if let controller = UIStoryboard(name: "Base", bundle: nil).instantiateViewController(identifier: "BaseViewController") as? BaseViewController { controller.type = .Optimization controller.modalPresentationStyle = .fullScreen self.present(controller, animated: true, completion: nil) } } @objc private func transformClicked(_ index: Int) { if let controller = UIStoryboard(name: "Base", bundle: nil).instantiateViewController(identifier: "BaseViewController") as? BaseViewController { controller.type = .Transform controller.innerIndex = index controller.modalPresentationStyle = .fullScreen self.present(controller, animated: true, completion: nil) } } @objc private func useCasesClicked(_ index: Int) { if let controller = UIStoryboard(name: "Base", bundle: nil).instantiateViewController(identifier: "BaseViewController") as? BaseViewController { controller.innerIndex = index controller.type = .UseCases controller.modalPresentationStyle = .fullScreen self.present(controller, animated: true, completion: nil) } } private func setTransformationCollectionView() { transfromationCollectionController = DeliveryTransformCollectionController(self) cvTransformation.dataSource = transfromationCollectionController cvTransformation.delegate = transfromationCollectionController } private func setUseCasesCollectionView() { useCasesCollectionController = DeliveryUseCasesCollectionController(self) cvUseCases.delegate = useCasesCollectionController cvUseCases.dataSource = useCasesCollectionController } } extension DeliveryViewController: DeliveryTransformCollectionDelegate { func transformCellSelected(_ index: Int) { transformClicked(index) } } extension DeliveryViewController: DeliverUseCaseCollectionDelegate { func useCaseCellSelected(_ index: Int) { useCasesClicked(index) } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Optimization/OptimizationViewController.swift ================================================ // // OptimizationViewController.swift // iOS_Geekle_Conference // // Created by Adi Mizrahi on 18/09/2023. // import Foundation import UIKit import Cloudinary class OptimizationViewController: UIViewController { @IBOutlet weak var vwBack: UIView! @IBOutlet weak var lbOriginal: UILabel! @IBOutlet weak var lbOptimized: UILabel! @IBOutlet weak var ivOriginal: CLDUIImageView! @IBOutlet weak var ivOptimized: CLDUIImageView! @IBOutlet weak var lbOriginalFormat: UILabel! @IBOutlet weak var lbOriginalDimensions: UILabel! @IBOutlet weak var lbOriginalSize: UILabel! @IBOutlet weak var lbOptimizedFormat: UILabel! @IBOutlet weak var lbOptimizedDimensions: UILabel! @IBOutlet weak var lbOptimizedSize: UILabel! var type: OptimizationViewType = .Optimization var publicId: String = "Demo%20app%20content/optimization_optimized" var cloudinary = CloudinaryHelper.shared.cloudinary override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setOriginalImageView() setOptimizedImageView() if type == .FetchUpload { EventsHandler.shared.logEvent(event: EventObject(name: "Fetch")) } else { EventsHandler.shared.logEvent(event: EventObject(name: "Transform")) } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) loadImageData() } func setBackButton() { let gesture = UITapGestureRecognizer(target: self, action: #selector(stepBack)) vwBack.addGestureRecognizer(gesture) } func setOriginalImageView() { var url = cloudinary.createUrl().generate(publicId) if type == .FetchUpload { url = cloudinary.createUrl().setType("fetch").generate(publicId) } let view = UIView() ivOriginal.cldSetImage(url!, cloudinary: cloudinary) lbOriginalFormat.isHidden = true lbOriginalDimensions.isHidden = true lbOriginalSize.isHidden = true } func setOptimizedImageView() { var url = cloudinary.createUrl().setTransformation(CLDTransformation().setQuality("auto").setFetchFormat("heic").setDpr("auto").setWidth(0.4).setCrop("scale")).generate(publicId) if type == .FetchUpload { url = cloudinary.createUrl().setType("fetch").setTransformation(CLDTransformation().setQuality("auto").setFetchFormat("auto").setDpr("auto").setWidth(0.7).setCrop("scale")).generate(publicId) } ivOptimized.cldSetImage(url!, cloudinary: cloudinary) lbOptimizedFormat.isHidden = true lbOptimizedDimensions.isHidden = true lbOptimizedSize.isHidden = true } @objc func loadImageData() { var originalUrl = cloudinary.createUrl().generate(publicId)! if type == .FetchUpload { originalUrl = cloudinary.createUrl().setType("fetch").generate(publicId)! } FileUtils.getImageInfo(URL(string: originalUrl)!) { format,size, dimensions in self.lbOriginalFormat.text = "\(format.uppercased()) ⏺" self.lbOriginalDimensions.text = "\(Int(dimensions.width))x\(Int(dimensions.height)) ⏺" self.lbOriginalSize.text = "\(size)MB" self.lbOriginalFormat.isHidden = false self.lbOriginalDimensions.isHidden = false self.lbOriginalSize.isHidden = false } var optimizedUrl = cloudinary.createUrl().setTransformation(CLDTransformation().setQuality("auto").setFetchFormat("auto").setDpr("auto").setWidth(0.4).setCrop("scale")).generate(publicId)! if type == .FetchUpload { optimizedUrl = cloudinary.createUrl().setType("fetch").setTransformation(CLDTransformation().setQuality("auto").setFetchFormat("auto").setDpr("auto").setWidth(0.7).setCrop("scale")).generate(publicId)! } FileUtils.getImageInfo(URL(string: optimizedUrl)!) { format, size, dimensions in self.lbOptimizedFormat.text = "\(format.uppercased()) ⏺" self.lbOptimizedDimensions.text = "\(Int(dimensions.width))x\(Int(dimensions.height)) ⏺" self.lbOptimizedSize.text = "\(size)MB" self.lbOptimizedFormat.isHidden = false self.lbOptimizedDimensions.isHidden = false self.lbOptimizedSize.isHidden = false } } @objc func stepBack() { self.dismiss(animated: true) } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Transformation/DeliveryTransformCollectionController.swift ================================================ // // DeliveryTransformCollectionController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 30/01/2024. // import Foundation import UIKit protocol DeliveryTransformCollectionDelegate { func transformCellSelected(_ index: Int) } class DeliveryTransformCollectionController: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { var delegate: DeliveryTransformCollectionDelegate init(_ delegate: DeliveryTransformCollectionDelegate) { self.delegate = delegate } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 4 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "transfromationCell", for: indexPath) as! TransformationCell cell.setCellContent(indexPath.row) return cell } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: 224, height: 135) } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { delegate.transformCellSelected(indexPath.row) } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Transformation/Reveal Image/RevealImageController.swift ================================================ // // RevealImageController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 30/01/2024. // import Foundation import UIKit class RevealImageController: UIViewController { @IBOutlet weak var ivMain: RevealImageView! func setMainImageView(rightImage: String?, leftImage: String?) { ImageHelper.getImageFromURL(URL(string: rightImage!)!) { image in DispatchQueue.main.async { self.ivMain.rightImage = image } } ImageHelper.getImageFromURL(URL(string: leftImage!)!) { image in DispatchQueue.main.async { self.ivMain.leftImage = image } } } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Transformation/Smart Cropping/SmartCroppingController.swift ================================================ // // SmartCroppingController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 30/01/2024. // import Foundation import UIKit class SmartCroppingController: UIViewController { } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Transformation/TransformCollectionCell.swift ================================================ // // TransformCollectionCell.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 08/01/2024. // import Foundation import UIKit import Cloudinary class TransformCollectionCell: UICollectionViewCell { @IBOutlet weak var lbMain: UILabel! @IBOutlet weak var ivMain: CLDUIImageView! func setCellBy(index: Int) { switch index { case 0: ivMain.cldSetImage(publicId: "Demo%20app%20content/content-aware-crop-4-ski_louxkt", cloudinary: CloudinaryHelper.shared.cloudinary, transformation: CLDTransformation().setCrop("thumb")) lbMain.text = "Smart Cropping" case 1: ivMain.cldSetImage(publicId: "Demo%20app%20content/layers-fashion-2_1_xsfbvm", cloudinary: CloudinaryHelper.shared.cloudinary, transformation: CLDTransformation().setCrop("thumb")) lbMain.text = "Localization & branding" case 2: ivMain.cldSetImage(publicId: "Demo%20app%20content/bgr-furniture-1_isnptj", cloudinary: CloudinaryHelper.shared.cloudinary, transformation: CLDTransformation().setCrop("thumb")) lbMain.text = "Background normalizing" case 3: ivMain.cldSetImage(publicId: "Demo%20app%20content/recolor-tshirt-5_omapls", cloudinary: CloudinaryHelper.shared.cloudinary, transformation: CLDTransformation().setCrop("thumb")) lbMain.text = "Color Alternation" default: break } } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Transformation/TransformCollectionController.swift ================================================ // // TransformCollectionController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 08/01/2024. // import Foundation import UIKit import Cloudinary protocol TransformCollectionDelegate { func cellSelected(_ index: Int) } class TransformCollectionController: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { var delegate: TransformCollectionDelegate var selectedCellIndex = 0 init(_ delegate: TransformCollectionDelegate) { self.delegate = delegate } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 4 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TransformCollectionCell", for: indexPath) as! TransformCollectionCell cell.setCellBy(index: indexPath.row) setSelectedCell(cell, index: indexPath.row) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let previousSelectedIndexPath = selectedCellIndex selectedCellIndex = indexPath.row delegate.cellSelected(indexPath.row) updateSelectedCellAppearance(collectionView, indexPath) updateSelectedCellAppearance(collectionView, IndexPath(row: previousSelectedIndexPath, section: 0)) } func updateSelectedCellAppearance(_ collectionView: UICollectionView, _ indexPath: IndexPath) { if let cell = collectionView.cellForItem(at: indexPath) as? TransformCollectionCell { setSelectedCell(cell, index: indexPath.row) } } func setSelectedCell(_ cell: TransformCollectionCell, index: Int) { if index == selectedCellIndex { cell.isSelected = true cell.layer.borderColor = UIColor(named: "primary")?.cgColor cell.layer.borderWidth = 3 cell.layer.cornerRadius = 4 } else { cell.layer.borderWidth = 0 } } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: 84, height: 116) } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Transformation/TransformViewController.swift ================================================ // // TransformViewController.swift // iOS_Geekle_Conference // // Created by Adi Mizrahi on 12/09/2023. // import Foundation import UIKit import Cloudinary class TransformViewController: UIViewController { @IBOutlet weak var cvMain: UICollectionView! @IBOutlet weak var vwContainer: UIView! var currentController: UIViewController! var innerIndex: Int = 0 var collectionController: TransformCollectionController! override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setCollectionView() setContainerView(innerIndexToType(innerIndex)) EventsHandler.shared.logEvent(event: EventObject(name: "Inner Trasnform")) } private func innerIndexToType(_ index: Int) -> TransformContainerType { switch index { case 0: return .SmartCropping case 1: return .TextLayer case 2: return .BackgroundRemoval case 3: return .ReColor default: return .SmartCropping } } private func setContainerView(_ type: TransformContainerType) { removeCurrentController() switch type { case .SmartCropping: currentController = UIStoryboard(name: "SmartCropping", bundle: nil).instantiateViewController(identifier: "SmartCroppingController") currentController.view.frame = vwContainer.bounds addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) collectionController.selectedCellIndex = 0 case .TextLayer: currentController = UIStoryboard(name: "RevealImage", bundle: nil).instantiateViewController(identifier: "RevealImageController") currentController.view.frame = vwContainer.bounds addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) (currentController as! RevealImageController).setMainImageView(rightImage: CloudinaryHelper.shared.cloudinary.createUrl().generate("Demo%20app%20content/layers-fashion-2_1_xsfbvm"), leftImage: CloudinaryHelper.shared.cloudinary.createUrl().setTransformation(CLDTransformation() .setOverlay("text:Arial_72:NEW%2520COLLECTION").setColor("white").chain() .setFlags("layer_apply").setGravity("center")).generate("Demo%20app%20content/layers-fashion-2_1_xsfbvm")) collectionController.selectedCellIndex = 1 case .BackgroundRemoval: currentController = UIStoryboard(name: "RevealImage", bundle: nil).instantiateViewController(identifier: "RevealImageController") as! RevealImageController currentController.view.frame = vwContainer.bounds addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) (currentController as! RevealImageController).setMainImageView(rightImage: CloudinaryHelper.shared.cloudinary.createUrl().generate("Demo%20app%20content/bgr-furniture-1_isnptj"), leftImage: CloudinaryHelper.shared.cloudinary.createUrl().setTransformation(CLDTransformation().setEffect("background_removal")).generate("Demo%20app%20content/bgr-furniture-1_isnptj")) collectionController.selectedCellIndex = 2 case .ReColor: currentController = UIStoryboard(name: "RevealImage", bundle: nil).instantiateViewController(identifier: "RevealImageController") as! RevealImageController currentController.view.frame = vwContainer.bounds addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) (currentController as! RevealImageController).setMainImageView(rightImage: CloudinaryHelper.shared.cloudinary.createUrl().generate("Demo%20app%20content/recolor-tshirt-5_omapls"), leftImage: CloudinaryHelper.shared.cloudinary.createUrl().setTransformation(CLDTransformation().setEffect("gen_recolor:prompt_t-shirt;to-color_8fbc8f")).generate("Demo%20app%20content/recolor-tshirt-5_omapls")) collectionController.selectedCellIndex = 3 } } private func setCollectionView() { collectionController = TransformCollectionController(self) cvMain.delegate = collectionController cvMain.dataSource = collectionController let flow = cvMain.collectionViewLayout as! UICollectionViewFlowLayout let itemSpacing: CGFloat = 10 let itemsInOneLine: CGFloat = 10 flow.sectionInset = UIEdgeInsets(top: 10, left: 16, bottom: 0, right: 16) let width = UIScreen.main.bounds.size.width - itemSpacing * CGFloat(itemsInOneLine - 1) flow.itemSize = CGSize(width: floor(width/itemsInOneLine), height: width/itemsInOneLine) flow.minimumInteritemSpacing = 10 flow.minimumLineSpacing = itemSpacing } private func removeCurrentController() { currentController?.willMove(toParent: nil) currentController?.view.removeFromSuperview() currentController?.removeFromParent() } } extension TransformViewController: TransformCollectionDelegate { func cellSelected(_ index: Int) { switch index { case 0: setContainerView(.SmartCropping) case 1: setContainerView(.TextLayer) case 2: setContainerView(.BackgroundRemoval) case 3: setContainerView(.ReColor) default: break } } } enum TransformContainerType { case SmartCropping case TextLayer case BackgroundRemoval case ReColor } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Transformation/TransformationCell.swift ================================================ // // TransformationCell.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 30/01/2024. // import Foundation import UIKit class TransformationCell: UICollectionViewCell { @IBOutlet weak var ivMain: UIImageView! @IBOutlet weak var lbMain: UILabel! func setCellContent(_ index: Int) { switch index { case 0: ivMain.image = UIImage(named: "smart_cropping") lbMain.text = "Smart Cropping" case 1: ivMain.image = UIImage(named: "localization_branding") lbMain.text = "Localization & Branding" case 2: ivMain.image = UIImage(named: "background_normalizing") lbMain.text = "Background normalizing" case 3: ivMain.image = UIImage(named: "color_alternation") lbMain.text = "Color alternation" default: break } } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Use Cases/DeliveryUseCasesCollectionController.swift ================================================ // // DeliveryUseCasesCollectionController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 30/01/2024. // import Foundation import UIKit protocol DeliverUseCaseCollectionDelegate { func useCaseCellSelected(_ index: Int) } class DeliveryUseCasesCollectionController: NSObject, UICollectionViewDelegate, UICollectionViewDataSource { var delegate: DeliverUseCaseCollectionDelegate init(_ delegate: DeliverUseCaseCollectionDelegate) { self.delegate = delegate } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 4 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "useCaseCell", for: indexPath) as! UseCaseCell cell.setCellBy(index: indexPath.row) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { delegate.useCaseCellSelected(indexPath.row) } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Use Cases/Screens/ConditionalProductViewController.swift ================================================ // // ConditionalProductViewController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 22/02/2024. // import Foundation import UIKit import Cloudinary class ConditionalProductViewController: UIViewController { @IBOutlet weak var ivTopLeft: CLDUIImageView! @IBOutlet weak var ivTopRight: CLDUIImageView! @IBOutlet weak var ivBottom: CLDUIImageView! override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setImageViews() } private func setImageViews() { ivTopLeft.cldSetImage(publicId: "Group_15_jda5ms", cloudinary: CloudinaryHelper.shared.cloudinary) ivTopRight.cldSetImage(publicId: "tshirt4_1_si0swc", cloudinary: CloudinaryHelper.shared.cloudinary) ivBottom.cldSetImage(publicId: "tshirt4_1_si0swc", cloudinary: CloudinaryHelper.shared.cloudinary, transformation: CLDTransformation().setOverlayWithLayer(CLDLayer().setPublicId(publicId: "Group_15_jda5ms")).setGravity(.northWest).setWidth(0.4).setX(10).setY(10)) } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Use Cases/Screens/IntegrateAIViewController.swift ================================================ // // IntegrateAIViewController.swift // Cloudinary_Example // // Created by Adi Mizrahi on 26/02/2024. // Copyright © 2024 CocoaPods. All rights reserved. // import Foundation import UIKit import Cloudinary import AVKit class IntegrateAIViewController: UIViewController { @IBOutlet weak var vwVideoView: UIView! var videoPublicId: String = "" var player: CLDVideoPlayer! override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setVideoView() } private func setVideoView() { player = CLDVideoPlayer(publicId: videoPublicId, cloudinary: CloudinaryHelper.shared.cloudinary) let playerLayer = AVPlayerLayer(player: player) playerLayer.frame = vwVideoView.bounds playerLayer.videoGravity = .resizeAspectFill vwVideoView.layer.addSublayer(playerLayer) // Add observer for AVPlayerItemDidPlayToEndTime NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem) player.play() } @objc private func playerItemDidReachEnd(notification: Notification) { if let playerItem = notification.object as? AVPlayerItem { // Seek to the start of the video to loop it playerItem.seek(to: .zero) player.play() } } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Use Cases/Screens/NormalizingProductAssetsViewController.swift ================================================ // // NormalizingProductAssetsViewController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 21/02/2024. // import Foundation import UIKit import Cloudinary class NormalizingProductAssetsViewController: UIViewController { @IBOutlet weak var ivTopLeft: CLDUIImageView! @IBOutlet weak var ivTopRight: CLDUIImageView! @IBOutlet weak var ivMidRight: CLDUIImageView! @IBOutlet weak var ivBottomLeft: CLDUIImageView! @IBOutlet weak var ivBottomMid: CLDUIImageView! @IBOutlet weak var ivBottomRight: CLDUIImageView! override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setImageViews() } private func setImageViews() { ivTopLeft.cldSetImage(publicId: "pexels-aditya-aiyar-1407354_tiw4bv", cloudinary: CloudinaryHelper.shared.cloudinary) ivTopRight.cldSetImage(publicId: "pexels-mnz-1670766_n9hfoi", cloudinary: CloudinaryHelper.shared.cloudinary) ivMidRight.cldSetImage(publicId: "pexels-wendy-wei-12511453_b4shho", cloudinary: CloudinaryHelper.shared.cloudinary) ivBottomLeft.cldSetImage(publicId: "Rectangle_1434_fcnobi", cloudinary: CloudinaryHelper.shared.cloudinary) ivBottomMid.cldSetImage(publicId: "Rectangle_1435_mwtszu", cloudinary: CloudinaryHelper.shared.cloudinary) ivBottomRight.cldSetImage(publicId: "Rectangle_1436_kdsfld", cloudinary: CloudinaryHelper.shared.cloudinary) } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Use Cases/UseCaseCell.swift ================================================ // // UseCaseCell.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 30/01/2024. // import Foundation import UIKit class UseCaseCell: UICollectionViewCell { @IBOutlet weak var lbMain: UILabel! @IBOutlet weak var vwGradient: GradientView! func setCellBy(index: Int) { switch index { case 0: lbMain.text = "Normalizing Product Assets - Sizing" case 1: lbMain.text = "Conditional Product Badging" case 2: lbMain.text = "Adapt video to any screen" case 3: lbMain.text = "Integrate AI-generated backgrounds" default: break } } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Use Cases/UseCaseCollectionCell.swift ================================================ // // UseCaseCollectionCell.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 21/02/2024. // import Foundation import UIKit class UseCaseCollectionCell: UICollectionViewCell { @IBOutlet weak var lbMain: UILabel! func setCellBy(index: Int) { switch index { case 0: lbMain.text = "Normalizing Product Assets - Sizing" case 1: lbMain.text = "Conditional Product Badging" case 2: lbMain.text = "Adapt video to any screen" case 3: lbMain.text = "Integrate AI-generated backgrounds" default: break } } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Use Cases/UseCasesCollectionController.swift ================================================ // // UseCasesCollectionController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 21/02/2024. // import Foundation import UIKit protocol UseCasesCollectionDelegate { func cellSelected(_ index: Int) } class UseCasesCollectionController: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { var delegate: UseCasesCollectionDelegate var selectedCellIndex = 0 init(_ delegate: UseCasesCollectionDelegate) { self.delegate = delegate } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 4 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "UseCaseCollectionCell", for: indexPath) as! UseCaseCollectionCell cell.setCellBy(index: indexPath.row) setSelectedCell(cell, index: indexPath.row) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { selectedCellIndex = indexPath.row delegate.cellSelected(indexPath.row) collectionView.reloadData() } private func setSelectedCell(_ cell: UseCaseCollectionCell, index: Int) { if index == selectedCellIndex { cell.isSelected = true cell.layer.borderColor = UIColor(named: "primary")?.cgColor cell.layer.borderWidth = 3 cell.layer.cornerRadius = 4 } else { cell.layer.borderWidth = 0 } } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: 80, height: 116) } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Transform/Use Cases/UseCasesViewController.swift ================================================ // // UseCasesViewController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 21/02/2024. // import Foundation import UIKit class UseCasesViewController: UIViewController { @IBOutlet weak var vwContainer: UIView! @IBOutlet weak var cvMain: UICollectionView! var innerIndex: Int = 0 var currentController: UIViewController! var collectionController: UseCasesCollectionController! override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) setCollectionView() setContainerView(innerIndexToType(innerIndex)) EventsHandler.shared.logEvent(event: EventObject(name: "Use Cases")) } private func innerIndexToType(_ index: Int) -> UseCaseContainerType { switch index { case 0: return .NormalizingProductAssets case 1: return .ConditionalProduct case 2: return .AdaptVideo case 3: return .IntegrateAI default: return .NormalizingProductAssets } } private func setCollectionView() { collectionController = UseCasesCollectionController(self) cvMain.delegate = collectionController cvMain.dataSource = collectionController let flow = cvMain.collectionViewLayout as! UICollectionViewFlowLayout let itemSpacing: CGFloat = 10 let itemsInOneLine: CGFloat = 10 flow.sectionInset = UIEdgeInsets(top: 10, left: 12, bottom: 0, right: 16) let width = UIScreen.main.bounds.size.width - itemSpacing * CGFloat(itemsInOneLine - 1) flow.itemSize = CGSize(width: floor(width/itemsInOneLine), height: width/itemsInOneLine) flow.minimumInteritemSpacing = 10 flow.minimumLineSpacing = itemSpacing } private func setContainerView(_ type: UseCaseContainerType) { removeCurrentController() switch type { case .NormalizingProductAssets: currentController = UIStoryboard(name: "NormalizingProductAssets", bundle: nil).instantiateViewController(identifier: "NormalizingProductAssetsViewController") currentController.view.frame = vwContainer.bounds addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) collectionController.selectedCellIndex = 0 case .ConditionalProduct: currentController = UIStoryboard(name: "ConditionalProduct", bundle: nil).instantiateViewController(identifier: "ConditionalProductViewController") currentController.view.frame = vwContainer.bounds addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) collectionController.selectedCellIndex = 1 break case .AdaptVideo: currentController = UIStoryboard(name: "IntegrateAI", bundle: nil).instantiateViewController(identifier: "IntegrateAIViewController") (currentController as! IntegrateAIViewController).videoPublicId = "DevApp_Adapt_Video_02_diett8" currentController.view.frame = vwContainer.bounds addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) collectionController.selectedCellIndex = 2 case .IntegrateAI: currentController = UIStoryboard(name: "IntegrateAI", bundle: nil).instantiateViewController(identifier: "IntegrateAIViewController") (currentController as! IntegrateAIViewController).videoPublicId = "DevApp_Generative_Fill_01_fneqxw" currentController.view.frame = vwContainer.bounds addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) collectionController.selectedCellIndex = 3 } } private func removeCurrentController() { currentController?.willMove(toParent: nil) currentController?.view.removeFromSuperview() currentController?.removeFromParent() } } extension UseCasesViewController: UseCasesCollectionDelegate { func cellSelected(_ index: Int) { switch index { case 0: setContainerView(.NormalizingProductAssets) case 1: setContainerView(.ConditionalProduct) case 2: setContainerView(.AdaptVideo) case 3: setContainerView(.IntegrateAI) default: break } } } enum UseCaseContainerType { case NormalizingProductAssets case ConditionalProduct case AdaptVideo case IntegrateAI } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Upload/InnerUploadFrame.swift ================================================ // // InnerUploadFrame.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 28/12/2023. // import Foundation import UIKit class InnerUploadFrame: UIView { @IBOutlet weak var lbTitle: UILabel! @IBOutlet weak var lbSubtitle: UILabel! @IBOutlet weak var vwGradientView: GradientView! override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { let nibName = String(describing: type(of: self)) if let view = Bundle.main.loadNibNamed(nibName, owner: self, options: nil)?.first as? UIView { view.frame = bounds addSubview(view) } vwGradientView.layer.cornerRadius = vwGradientView.frame.height / 2 } func setTitle(title: String) { lbTitle.text = title } func setSubtitle(subtitle: String) { lbSubtitle.text = subtitle } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Upload/Single Upload/SingleUploadCell.swift ================================================ // // SingleUploadCell.swift // Cloudinary_Example // // Created by Adi Mizrahi on 18/07/2024. // Copyright © 2024 CocoaPods. All rights reserved. // import Foundation import UIKit import Cloudinary class SingleUploadCell: UICollectionViewCell { @IBOutlet weak var ivMain: CLDUIImageView! func setImage(_ url: String, _ cloudinary: CLDCloudinary) { var urlString = url if urlString.contains("video") { urlString = replaceExtension(urlString: urlString) ?? "" ivMain.cldSetImage(urlString, cloudinary: cloudinary) return } guard let url = URL(string: urlString) else { return } let publicId = url.lastPathComponent ivMain.cldSetImage(publicId: publicId, cloudinary: cloudinary, transformation: CLDTransformation().setCrop("thumb")) } func replaceExtension(urlString: String) -> String? { guard let url = URL(string: urlString) else { return nil // Invalid URL } // Get the last path component let lastComponent = url.lastPathComponent // Get the path extension let pathExtension = (lastComponent as NSString).pathExtension // Replace the path extension with ".jpg" let newLastPathComponent = (lastComponent as NSString).deletingPathExtension + ".jpg" // Create a new URL instance with the updated path component if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) { urlComponents.path = urlComponents.path.replacingOccurrences(of: lastComponent, with: newLastPathComponent) // Get the updated URL string if let updatedURLString = urlComponents.string { return updatedURLString } } return urlString // Return original URL if there's any failure in updating the URL string } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Upload/Single Upload/SingleUploadCollectionController.swift ================================================ // // SingleUploadCollectionController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 12/05/2024. // import Foundation import UIKit import Cloudinary protocol SingleUploadCollectionDelegate { func presentController(_ controller: UIViewController) } class SingleUploadCollectionController: NSObject, UICollectionViewDelegate, UICollectionViewDataSource { var delegate: SingleUploadCollectionDelegate var data: [AssetItems] let cloudinary = CLDCloudinary(configuration: CLDConfiguration(cloudName: CloudinaryHelper.shared.getUploadCloud()!, secure: true)) weak var collectionView: UICollectionView? // Weak reference to collection view init(delegate: SingleUploadCollectionDelegate, collectionView: UICollectionView) { self.collectionView = collectionView self.data = CoreDataHelper.shared.fetchData() ?? [] self.delegate = delegate super.init() collectionView.delegate = self collectionView.dataSource = self } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return data.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "uploadCell", for: indexPath) as! SingleUploadCell cell.setImage(data[indexPath.row].url, cloudinary) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let controller = UIStoryboard(name: "SingleUploadPreview", bundle: nil).instantiateViewController(withIdentifier: "SingleUploadPreview") as! SingleUploadPreview controller.url = data[indexPath.row].url delegate.presentController(controller) } func refreshData() { let newData = CoreDataHelper.shared.fetchData() ?? [] let oldData = data data = newData // Update data // Compute changes var insertions: [IndexPath] = [] var deletions: [IndexPath] = [] var moves: [(from: IndexPath, to: IndexPath)] = [] // Find insertions and deletions for (index, newItem) in newData.enumerated() { if let oldIndex = oldData.firstIndex(where: { $0.id == newItem.id }) { if index != oldIndex { moves.append((from: IndexPath(item: oldIndex, section: 0), to: IndexPath(item: index, section: 0))) } } else { insertions.append(IndexPath(item: index, section: 0)) } } for (index, oldItem) in oldData.enumerated() { if !newData.contains(where: { $0.id == oldItem.id }) { deletions.append(IndexPath(item: index, section: 0)) } } // Apply changes to collection view collectionView?.performBatchUpdates({ if !insertions.isEmpty { collectionView?.insertItems(at: insertions) } if !deletions.isEmpty { collectionView?.deleteItems(at: deletions) } for move in moves { collectionView?.moveItem(at: move.from, to: move.to) } }, completion: nil) } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Upload/Single Upload/SingleUploadCollectionLayout.swift ================================================ // // SingleUploadCollectionLayout.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 12/05/2024. // import Foundation import UIKit class SingleUploadCollectionLayout: UICollectionViewFlowLayout { var singleItemAttributes: UICollectionViewLayoutAttributes? override func prepare() { super.prepare() guard let collectionView = collectionView else { return } // Calculate item size to occupy one-third of the collection view's width let itemWidth = (collectionView.bounds.width - minimumInteritemSpacing * 2 - minimumLineSpacing * 2) / 3 itemSize = CGSize(width: itemWidth, height: itemWidth) // Calculate horizontal inset to center items let inset = (collectionView.bounds.width - CGFloat(3) * itemWidth - minimumInteritemSpacing * 2) / 2 // Set section inset sectionInset = UIEdgeInsets(top: sectionInset.top, left: inset, bottom: sectionInset.bottom, right: inset) // Adjust alignment for single item if collectionView.numberOfItems(inSection: 0) == 1 { let indexPath = IndexPath(item: 0, section: 0) singleItemAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) singleItemAttributes?.frame.origin = CGPoint(x: sectionInset.left, y: sectionInset.top) } else { singleItemAttributes = nil } } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { if let singleItemAttributes = singleItemAttributes { return [singleItemAttributes] } else { return super.layoutAttributesForElements(in: rect) } } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Upload/Single Upload/SingleUploadPreview.swift ================================================ // // SingleUploadPreview.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 12/05/2024. // import Foundation import UIKit import Cloudinary import AVKit class SingleUploadPreview: UIViewController { var url: String! var cloudinary = CLDCloudinary(configuration: CLDConfiguration(cloudName: CloudinaryHelper.shared.getUploadCloud()!)) @IBOutlet weak var vwShare: UIView! @IBOutlet weak var vwImage: UIView! @IBOutlet weak var ivMain: CLDUIImageView! override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setShareView() if url.contains("video") { let player = CLDVideoPlayer(url: url) let playerController = AVPlayerViewController() playerController.player = player addChild(playerController) playerController.videoGravity = .resizeAspect vwImage.addSubview(playerController.view) playerController.view.frame = vwImage.bounds playerController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] playerController.didMove(toParent: self) player.play() } else { ivMain.cldSetImage(url, cloudinary: cloudinary) } } private func setShareView() { let gesture = UITapGestureRecognizer(target: self, action: #selector(share)) vwShare.addGestureRecognizer(gesture) } @objc private func share() { // Check if the image is available if let image = ivMain.image { // If image is available, share the image let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: nil) // Present the activity view controller let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first if let viewController = window!.rootViewController { activityViewController.popoverPresentationController?.sourceView = viewController.view self.present(activityViewController, animated: true, completion: nil) } } else { // If image is not available, share the URL guard let url = self.url else { return } let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil) // Present the activity view controller let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first if let viewController = window!.rootViewController { activityViewController.popoverPresentationController?.sourceView = viewController.view self.present(activityViewController, animated: true, completion: nil) } } } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Upload/Single Upload/SingleUploadViewController.swift ================================================ // // SingleUploadViewController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 11/01/2024. // import Foundation import UIKit import Photos import Cloudinary import AVKit class SingleUploadViewController: UIViewController { @IBOutlet weak var vwImage: UIView! @IBOutlet weak var vwOpenGallery: UIView! @IBOutlet weak var lbButton: UILabel! @IBOutlet weak var cvMain: UICollectionView! weak var delegate: UploadChoiceControllerDelegate! var uploadWidget: CLDUploaderWidget! var collectionController: SingleUploadCollectionController! var collectionLayout: SingleUploadCollectionLayout! var url: String? var cloudinary = CLDCloudinary(configuration: CLDConfiguration(cloudName: CloudinaryHelper.shared.getUploadCloud()!)) var uploadLoadingView: UploadLoadingView? var type: UploadViewType = .Upload private var imagePicker: UIImagePickerController! override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setOpenGalleryView() setMainCollectionView() switch type { case .Upload: EventsHandler.shared.logEvent(event: EventObject(name: "Upload")) case .UploadLarge: EventsHandler.shared.logEvent(event: EventObject(name: "Upload Large")) case .UploadWidget: EventsHandler.shared.logEvent(event: EventObject(name: "Upload Widget")) } } private func setMainCollectionView() { collectionController = SingleUploadCollectionController(delegate: self, collectionView: cvMain) collectionLayout = SingleUploadCollectionLayout() cvMain.delegate = collectionController cvMain.dataSource = collectionController cvMain.collectionViewLayout = collectionLayout } private func setOpenGalleryView() { vwOpenGallery.layer.cornerRadius = vwOpenGallery.frame.height / 2 let tapGesture = UITapGestureRecognizer(target: self, action: #selector(openGalleryClicked)) vwOpenGallery.addGestureRecognizer(tapGesture) } @objc private func openGalleryClicked() { if type == .UploadWidget { } else { if imagePicker == nil { imagePicker = UIImagePickerController() } if UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum){ imagePicker.delegate = self if type == .UploadLarge { imagePicker.mediaTypes = ["public.movie"] } imagePicker.sourceType = .photoLibrary imagePicker.allowsEditing = false present(imagePicker, animated: true, completion: nil) } } } private func addUploadingView() { let loadingViewSize = CGSize(width: 180, height: 110) let loadingViewOrigin = CGPoint(x: (vwImage.frame.width - loadingViewSize.width) / 2, y: (vwImage.frame.height - loadingViewSize.height) / 2) uploadLoadingView = UploadLoadingView(frame: CGRect(origin: loadingViewOrigin, size: loadingViewSize)) uploadLoadingView!.startAnimation() vwImage.addSubview(uploadLoadingView!) } private func removeUploadingView() { if let uploadLoadingView = uploadLoadingView { AnimationHelper.animateOut(view: uploadLoadingView) } } func uploadImage(_ image: UIImage) { addUploadingView() let data = image.pngData() cloudinary.createUploader().upload(data: data!, uploadPreset: "ios_sample", completionHandler: { response, error in if let response = response { CoreDataHelper.shared.insertData(AssetModel(deliveryType: response.type ?? "upload", assetType: response.resourceType ?? "image", transformation: "", publicId: response.publicId ?? "", url: response.secureUrl ?? "")) } DispatchQueue.main.async { UIView.animate(withDuration: 0.3) { self.collectionController.refreshData() self.cvMain.reloadData() } self.removeUploadingView() } }) } func uploadVideo(_ url: URL) { addUploadingView() let params = CLDUploadRequestParams() params.setResourceType("video") cloudinary.createUploader().upload(url: url as URL, uploadPreset: "ios_sample", params: params, completionHandler: { response, error in if let response = response { CoreDataHelper.shared.insertData(AssetModel(deliveryType: response.type ?? "upload", assetType: response.resourceType ?? "video", transformation: "", publicId: response.publicId ?? "", url: response.secureUrl ?? "")) } DispatchQueue.main.async { UIView.animate(withDuration: 0.3) { self.collectionController.refreshData() self.cvMain.reloadData() } self.removeUploadingView() } }) } } extension SingleUploadViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { picker.dismiss(animated: true, completion: nil) if let image = info[.originalImage] as? UIImage { uploadImage(image) } if let url = info[.mediaURL] as? URL { uploadVideo(url) } } } extension SingleUploadViewController: CLDUploaderWidgetDelegate { func uploadWidget(_ widget: CLDUploaderWidget, willCall uploadRequests: [CLDUploadRequest]) { addUploadingView() uploadRequests[0].response( { response, error in self.cvMain.reloadData() self.removeUploadingView() } ) } func widgetDidCancel(_ widget: CLDUploaderWidget) { } func uploadWidgetDidDismiss() { } } extension SingleUploadViewController: SingleUploadCollectionDelegate { func presentController(_ controller: UIViewController) { self.present(controller, animated: true) } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Upload/Upload Does Not Exist/UploadDoesNotExistController.swift ================================================ // // UploadDoesNotExistController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 30/01/2024. // import Foundation import UIKit import Cloudinary class UploadDoesNotExistController: UIViewController { @IBOutlet weak var vwUploadContainer: UIView! @IBOutlet weak var vwUpload: UIView! @IBOutlet weak var aiLoading: UIActivityIndicatorView! @IBOutlet weak var lbUploadButton: UILabel! @IBOutlet weak var lbUploadContainer: UILabel! private var imagePicker: UIImagePickerController! weak var delegate: UploadChoiceControllerDelegate! var uploadWidget: CLDUploaderWidget! var type: UploadViewType = .Upload var cloudinary = CLDCloudinary(configuration: CLDConfiguration(cloudName: CloudinaryHelper.shared.getUploadCloud()!)) override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) switch type { case .Upload: EventsHandler.shared.logEvent(event: EventObject(name: "Upload")) case .UploadLarge: lbUploadButton.text = "Upload Video" lbUploadContainer.text = "Upload your video now and let the magic begin!" EventsHandler.shared.logEvent(event: EventObject(name: "Upload Large")) case .UploadWidget: EventsHandler.shared.logEvent(event: EventObject(name: "Upload Widget")) } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) setUploadImageView() } func setUploadImageView() { vwUpload.layer.cornerRadius = vwUpload.frame.height / 2 let tapGesture = UITapGestureRecognizer(target: self, action: #selector(uploadClicked)) vwUpload.addGestureRecognizer(tapGesture) } @objc private func uploadClicked() { if type == .UploadWidget { openUploadWidget() } else { if imagePicker == nil { imagePicker = UIImagePickerController() } if UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum){ if type == .UploadLarge { imagePicker.mediaTypes = ["public.movie"] } imagePicker.delegate = self imagePicker.sourceType = .photoLibrary imagePicker.allowsEditing = false present(imagePicker, animated: true, completion: nil) } } } private func openUploadWidget() { let configuration = CLDWidgetConfiguration( uploadType: CLDUploadType(signed: false, preset: "ios_sample")) uploadWidget = CLDUploaderWidget( cloudinary: cloudinary, configuration: configuration, images: nil, delegate: self) uploadWidget.presentWidget(from: self) } private func showUploadingView() { vwUploadContainer.isHidden = true aiLoading.isHidden = false } private func hideUploadingView() { self.aiLoading.isHidden = true } func uploadImage(_ image: UIImage) { showUploadingView() let data = image.pngData() cloudinary.createUploader().upload(data: data!, uploadPreset: "ios_sample", completionHandler: { response, error in guard let response = response else { return } CoreDataHelper.shared.insertData(AssetModel(deliveryType: response.type ?? "upload", assetType: response.resourceType ?? "image", transformation: "", publicId: response.publicId ?? "", url: response.secureUrl ?? "")) DispatchQueue.main.async { self.delegate.switchToController(.UploadExist, url: response.secureUrl) self.hideUploadingView() } }) } func uploadVideo(_ url: NSURL) { showUploadingView() let params = CLDUploadRequestParams() params.setResourceType("video") cloudinary.createUploader().upload(url: url as URL, uploadPreset: "ios_sample", params: params, completionHandler: { response, error in DispatchQueue.main.async { self.delegate.switchToController(.UploadExist, url: response?.secureUrl) self.hideUploadingView() } }) } } extension UploadDoesNotExistController: UINavigationControllerDelegate, UIImagePickerControllerDelegate { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { picker.dismiss(animated: true, completion: nil) if let image = info[.originalImage] as? UIImage { uploadImage(image) } if let url = info[.mediaURL] as? NSURL { uploadVideo(url) } } } extension UploadDoesNotExistController: CLDUploaderWidgetDelegate { func uploadWidget(_ widget: CLDUploaderWidget, willCall uploadRequests: [CLDUploadRequest]) { DispatchQueue.main.async { self.showUploadingView() } uploadRequests[0].response( { response, error in self.delegate.switchToController(.UploadExist, url: response?.secureUrl) self.hideUploadingView() } ) } func widgetDidCancel(_ widget: CLDUploaderWidget) { } func uploadWidgetDidDismiss() { } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Upload/UploadChoiceController.swift ================================================ // // UploadChoiceController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 30/01/2024. // import Foundation import UIKit protocol UploadChoiceControllerDelegate: AnyObject { func switchToController(_ uploadState: UploadChoiceState, url: String?) func dismissController() } class UploadChoiceController: UIViewController { @IBOutlet weak var vwContainer: UIView! var currentController: UIViewController! var type: UploadViewType = .Upload override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if CloudinaryHelper.shared.getUploadCloud() == nil { setContainerView(.NoCloudName) } else { if CoreDataHelper.shared.fetchData()?.count ?? 0 <= 0 { setContainerView(.NoUpload) } else { setContainerView(.UploadExist) } } } private func setContainerView(_ uploadState: UploadChoiceState, url: String? = nil) { removeCurrentController() switch uploadState { case .NoCloudName: currentController = UIStoryboard(name: "UploadNoCloud", bundle: nil).instantiateViewController(identifier: "UploadNoCloudController") (currentController as! UploadNoCloudController).delegate = self // currentController.modalPresentationStyle = .fullScreen self.present(currentController, animated: true) break case .NoUpload: currentController = UIStoryboard(name: "UploadDoesNotExist", bundle: nil).instantiateViewController(identifier: "UploadDoesNotExistController") currentController.view.frame = vwContainer.bounds (currentController as! UploadDoesNotExistController).type = type (currentController as! UploadDoesNotExistController).delegate = self addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) case .UploadExist: currentController = UIStoryboard(name: "SingleUpload", bundle: nil).instantiateViewController(identifier: "SingleUploadViewController") currentController.view.frame = vwContainer.bounds (currentController as! SingleUploadViewController).type = type (currentController as! SingleUploadViewController).delegate = self (currentController as! SingleUploadViewController).url = url addChild(currentController) vwContainer.addSubview(currentController.view) currentController.didMove(toParent: self) } } private func removeCurrentController() { currentController?.willMove(toParent: nil) currentController?.view.removeFromSuperview() currentController?.removeFromParent() } } extension UploadChoiceController: UploadChoiceControllerDelegate { func switchToController(_ uploadState: UploadChoiceState, url: String?) { setContainerView(uploadState, url: url) } func dismissController() { self.dismiss(animated: true) } } enum UploadChoiceState { case NoCloudName case NoUpload case UploadExist } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Upload/UploadNoCloudController.swift ================================================ // // UploadNoCloudController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 04/02/2024. // import Foundation import UIKit class UploadNoCloudController: UIViewController { @IBOutlet weak var tfCloudName: UITextField! @IBOutlet weak var vwGetStarted: UIView! @IBOutlet weak var vwCantFindCloud: UIView! @IBOutlet weak var vwClose: UIView! weak var delegate: UploadChoiceControllerDelegate! override func viewDidLoad() { super.viewDidLoad() setTextField() setGetStratedView() setCantFindCloudView() setCloseView() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) EventsHandler.shared.logEvent(event: EventObject(name: "Upload No Cloud")) } private func setTextField() { tfCloudName.delegate = self } private func setGetStratedView() { let gesture = UITapGestureRecognizer(target: self, action: #selector(getStratedTap)) vwGetStarted.addGestureRecognizer(gesture) } private func setCloseView() { vwClose.isUserInteractionEnabled = true let tapGesture = UITapGestureRecognizer(target: self, action: #selector(closeView)) vwClose.addGestureRecognizer(tapGesture) } private func setCantFindCloudView() { let gesture = UITapGestureRecognizer(target: self, action: #selector(cantFindCloudClicked)) vwCantFindCloud.addGestureRecognizer(gesture) } @objc private func cantFindCloudClicked() { if let url = URL(string: "https://cloudinary.com/documentation/how_to_integrate_cloudinary#create_and_explore_your_account") { UIApplication.shared.open(url, options: [:], completionHandler: nil) } } @objc private func getStratedTap() { CloudinaryHelper.shared.setUploadCloud(tfCloudName.text) delegate.switchToController(.NoUpload, url: nil) self.dismiss(animated: true) } @objc private func closeView() { self.dismiss(animated: true) { self.delegate.dismissController() } } } extension UploadNoCloudController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { getStratedTap() tfCloudName.resignFirstResponder() return false } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Upload/UploadViewController.swift ================================================ // // UploadViewController.swift // iOS_Geekle_Conference // // Created by Adi Mizrahi on 18/09/2023. // import Foundation import UIKit import Cloudinary class UploadViewController: UIViewController { @IBOutlet weak var lbTitle: UILabel! @IBOutlet weak var vwUpload: InnerUploadFrame! @IBOutlet weak var vwUploadLarge: InnerUploadFrame! @IBOutlet weak var vwFetchUpload: InnerUploadFrame! override func viewDidLoad() { super.viewDidLoad() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setViews() EventsHandler.shared.logEvent(event: EventObject(name: "Upload")) } private func setViews() { setUploadView() setUploadLargeView() setFetchUploadView() } private func setUploadView() { vwUpload.setTitle(title: "Upload") vwUpload.setSubtitle(subtitle: "Everything starts when you uploads a file") let tapGesture = UITapGestureRecognizer(target: self, action: #selector(uploadClicked)) vwUpload.addGestureRecognizer(tapGesture) } private func setUploadLargeView() { vwUploadLarge.setTitle(title: "Upload large file") vwUploadLarge.setSubtitle(subtitle: "Share your big files up to 100GB") let tapGesture = UITapGestureRecognizer(target: self, action: #selector(uploadLargeClicked)) vwUploadLarge.addGestureRecognizer(tapGesture) } private func setFetchUploadView() { vwFetchUpload.setTitle(title: "Fetch Upload") vwFetchUpload.setSubtitle(subtitle: "Upload image from any URL to your cloud") let tapGesture = UITapGestureRecognizer(target: self, action: #selector(fetchUploadClicked)) vwFetchUpload.addGestureRecognizer(tapGesture) } @objc private func uploadClicked() { if let controller = UIStoryboard(name: "Base", bundle: nil).instantiateViewController(identifier: "BaseViewController") as? BaseViewController { controller.type = .Upload controller.modalPresentationStyle = .fullScreen self.present(controller, animated: true, completion: nil) } } @objc private func uploadLargeClicked() { if let controller = UIStoryboard(name: "Base", bundle: nil).instantiateViewController(identifier: "BaseViewController") as? BaseViewController { controller.type = .UploadLarge controller.modalPresentationStyle = .fullScreen self.present(controller, animated: true, completion: nil) } } @objc private func fetchUploadClicked() { if let controller = UIStoryboard(name: "Base", bundle: nil).instantiateViewController(identifier: "BaseViewController") as? BaseViewController { controller.type = .FetchUpload controller.modalPresentationStyle = .fullScreen self.present(controller, animated: true, completion: nil) } } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Video/Video Feed/VideoFeedCell.swift ================================================ // // VideoFeedCell.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 31/01/2024. // import Foundation import UIKit class VideoFeedCell: UICollectionViewCell { @IBOutlet weak var vwGradientView: GradientView! @IBOutlet weak var ivMain: UIImageView! func setImage(_ index: Int) { self.layer.cornerRadius = 4 self.layer.masksToBounds = false vwGradientView.layer.cornerRadius = 4 vwGradientView.layer.masksToBounds = false switch index { case 0: ivMain.image = UIImage(named: "tiktok") case 1: ivMain.image = UIImage(named: "instagram") case 2: ivMain.image = UIImage(named: "youtube") default: break } } } ================================================ FILE: Example/Cloudinary/Controllers/Inner Views/Video/VideoFeedCollectionController.swift ================================================ // // VideoFeedCollectionController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 31/01/2024. // import Foundation import UIKit protocol VideoFeedCollectionDelegate { func cellClicked(_ index: Int) } class VideoFeedCollectionController: NSObject, UICollectionViewDelegate, UICollectionViewDataSource { var delegate: VideoFeedCollectionDelegate init(_ delegate: VideoFeedCollectionDelegate) { self.delegate = delegate } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 3 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "videoFeedCell", for: indexPath) as! VideoFeedCell cell.setImage(indexPath.row) cell.layer.cornerRadius = 4 cell.layer.masksToBounds = false cell.contentView.layer.cornerRadius = 4 cell.contentView.layer.masksToBounds = false return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { delegate.cellClicked(indexPath.row) } } ================================================ FILE: Example/Cloudinary/Controllers/MainViewController.swift ================================================ // // ViewController.swift // iOS_Geekle_Conference // // Created by Adi Mizrahi on 11/09/2023. // import UIKit class MainViewController: UIViewController { @IBOutlet weak var vwToolbar: UIView! @IBOutlet weak var vwContainer: UIView! var selectedOption: ToolbarOptions = .Delivery var currentViewController: UIViewController! override func viewDidLoad() { super.viewDidLoad() setToolbar() setContainerView(option: .Delivery) } private func setToolbar() { let bottomToolbar = Toolbar(frame: vwToolbar.bounds, delegate: self) vwToolbar.addSubview(bottomToolbar) } private func setContainerView(option: ToolbarOptions) { var controller: UIViewController? switch option { case .Delivery: controller = UIStoryboard(name: "Delivery", bundle: nil).instantiateViewController(identifier: "DeliveryViewController") break case .Upload: controller = UIStoryboard(name: "Upload", bundle: nil).instantiateViewController(identifier: "UploadViewController") break case .Widgets: controller = UIStoryboard(name: "Widgets", bundle: nil).instantiateViewController(identifier: "WidgetsViewController") break case .Video: controller = UIStoryboard(name: "Video", bundle: nil).instantiateViewController(identifier: "VideoViewController") break } guard let newController = controller else { return } addChild(newController) AnimationHelper.animateTabController(vwContainer, newController, currentViewController: currentViewController, completion: { self.currentViewController?.removeFromParent() self.currentViewController = newController newController.didMove(toParent: self) }) } } extension MainViewController: ToolbarDelegate { func deliverySelected() { if(selectedOption != .Delivery) { selectedOption = .Delivery setContainerView(option: .Delivery) } } func uploadSelected() { if(selectedOption != .Upload) { selectedOption = .Upload setContainerView(option: .Upload) } } func widgetsSelected() { if(selectedOption != .Widgets) { selectedOption = .Widgets setContainerView(option: .Widgets) } } func videoSelected() { if(selectedOption != .Video) { selectedOption = .Video setContainerView(option: .Video) } } } ================================================ FILE: Example/Cloudinary/Controllers/SplashViewController.swift ================================================ // // SplashController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 27/12/2023. // import Foundation import UIKit class SplashViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) let delayInSeconds: TimeInterval = 2 DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { self.transitionToMainController() } EventsHandler.shared.logEvent(event: EventObject(name: "Splash")) } func transitionToMainController() { // Instantiate your main view controller let storyboard = UIStoryboard(name: "Main", bundle: nil) // Replace "Main" with your actual storyboard name let mainViewController = storyboard.instantiateViewController(withIdentifier: "MainViewController") // Replace "MainViewController" with your actual view controller identifier // Transition to the main view controller if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let delegate = windowScene.delegate as? SceneDelegate { delegate.window?.rootViewController = mainViewController } } } ================================================ FILE: Example/Cloudinary/Controllers/Video Feed/Controllers/MainPageController.swift ================================================ // // MainPageController.swift // ios-video-testing // // Created by Adi Mizrahi on 16/11/2023. // import Foundation import UIKit class MainPageController: UIPageViewController { var videoControllersList = [UIViewController]() override func viewDidLoad() { super.viewDidLoad() let videoLinksData = VideoHelper.parsePlist() ?? [String]() for link in videoLinksData { if let controller = UIStoryboard(name: "VideoFeed", bundle: nil).instantiateViewController(identifier: "VideoFeedContainerController") as? VideoFeedContainerController { _ = controller.view controller.videoURL = link controller.setupPlayer() controller.modalPresentationStyle = .fullScreen videoControllersList.append(controller) } } self.dataSource = self (videoControllersList[0] as! VideoFeedContainerController).playVideo() setViewControllers([videoControllersList[0]], direction: .forward, animated: true, completion: nil) } } extension MainPageController: UIPageViewControllerDataSource { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { let indexOfCurrentPageViewController = videoControllersList.index(of: viewController)! if indexOfCurrentPageViewController == 0 { return nil // To show there is no previous page } else { // Previous UIViewController instance guard let controller = videoControllersList[indexOfCurrentPageViewController - 1] as? VideoFeedContainerController else { return videoControllersList[indexOfCurrentPageViewController - 1] } controller.playVideo() return videoControllersList[indexOfCurrentPageViewController - 1] } } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { let indexOfCurrentPageViewController = videoControllersList.index(of: viewController)! if indexOfCurrentPageViewController == videoControllersList.count - 1 { return nil // To show there is no next page } else { // Next UIViewController instance guard let controller = videoControllersList[indexOfCurrentPageViewController + 1] as? VideoFeedContainerController else { return videoControllersList[indexOfCurrentPageViewController + 1] } controller.playVideo() return videoControllersList[indexOfCurrentPageViewController + 1] } } } ================================================ FILE: Example/Cloudinary/Controllers/Video Feed/Controllers/VideoFeedContainerController.swift ================================================ // // VideoFeedContainerController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 04/02/2024. // import Foundation import UIKit class VideoFeedContainerController: UIViewController { @IBOutlet weak var vwFeedContainer: UIView! var videoURL: String! var controller: VideoFeedViewController! func setupPlayer() { if isViewLoaded == false { _ = view } controller = UIStoryboard(name: "VideoFeed", bundle: nil).instantiateViewController(identifier: "VideoFeedViewController") if let controller = controller { controller.videoURL = videoURL controller.setupPlayer() controller.view.frame = vwFeedContainer.bounds vwFeedContainer.addSubview(controller.view) controller.didMove(toParent: self) } } func playVideo() { controller.playVideo() } } ================================================ FILE: Example/Cloudinary/Controllers/Video Feed/Controllers/VideoFeedController.swift ================================================ // // VideoFeedController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 04/02/2024. // import Foundation import UIKit class VideoFeedController: UIViewController { @IBOutlet weak var vwBack: UIView! override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setBackButton() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.view.bringSubviewToFront(vwBack) } private func setBackButton() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(backClicked)) vwBack.addGestureRecognizer(tapGesture) } @objc private func backClicked() { self.dismiss(animated: true) } } ================================================ FILE: Example/Cloudinary/Controllers/Video Feed/Controllers/VideoFeedViewController.swift ================================================ import Foundation import Cloudinary import UIKit import AVKit class VideoFeedViewController: UIViewController { var videoURL: String? @IBOutlet weak var vwVideoView: UIView! @IBOutlet weak var vwVideoOverlay: UIView! private var player: CLDVideoPlayer? static func getInstance(url: String) -> VideoFeedViewController { let storyboard = UIStoryboard(name: "VideoFeed", bundle: nil) guard let viewController = storyboard.instantiateViewController(withIdentifier: "VideoFeedViewController") as? VideoFeedViewController else { fatalError("Unable to instantiate VideoFeedViewController from the storyboard.") } viewController.videoURL = url return viewController } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) EventsHandler.shared.logEvent(event: EventObject(name: "Video Feed")) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } private func addVideoOverlay(_ playerController: AVPlayerViewController) { // Load the view controller from the "VideoSocialOverlay" storyboard let overlayViewController = UIStoryboard(name: "VideoSocialOverlays", bundle: nil).instantiateViewController(withIdentifier: "VideoSocialOverlaysController") // Add the overlay view controller as a child of AVPlayerViewController playerController.addChild(overlayViewController) overlayViewController.didMove(toParent: playerController) // Add the overlay view controller's view as a subview to playerController's contentOverlayView playerController.contentOverlayView?.addSubview(overlayViewController.view) overlayViewController.view.frame = playerController.contentOverlayView?.bounds ?? .zero overlayViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] } func setupPlayer() { guard let videoURLString = videoURL else { return } if isViewLoaded == false { _ = view } player = CLDVideoPlayer(url: videoURLString) player?.isMuted = true let playerController = AVPlayerViewController() playerController.player = player addChild(playerController) playerController.videoGravity = .resizeAspectFill playerController.showsPlaybackControls = false vwVideoView.addSubview(playerController.view) playerController.view.frame = vwVideoView.bounds playerController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] playerController.didMove(toParent: self) NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player?.currentItem) addVideoOverlay(playerController) } func playVideo() { player?.play() } @objc func playerItemDidReachEnd() { // Seek to the start of the video to loop it player?.seek(to: CMTime.zero) player?.play() } deinit { NotificationCenter.default.removeObserver(self) } } ================================================ FILE: Example/Cloudinary/Controllers/Video Feed/Controllers/VideoSocialOverlaysController.swift ================================================ // // VideoSocialOverlaysController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 04/02/2024. // import Foundation import UIKit class VideoSocialOverlaysController: UIViewController { } ================================================ FILE: Example/Cloudinary/Controllers/Video Feed/Controllers/VideoViewController.swift ================================================ // // VideoViewController.swift // ios-video-testing // // Created by Adi Mizrahi on 16/11/2023. // import Foundation import UIKit import Cloudinary import AVKit class VideoViewController: UIViewController { @IBOutlet weak var vwVideoView: UIView! @IBOutlet weak var cvVideoFeed: UICollectionView! var collectionController: VideoFeedCollectionController! var playerController: AVPlayerViewController! var player: CLDVideoPlayer! override func viewDidLoad() { super.viewDidLoad() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setVideoView() setVideoFeedCollectionView() EventsHandler.shared.logEvent(event: EventObject(name: "Video")) } private func setVideoView() { player = CLDVideoPlayer(url: "https://res.cloudinary.com/mobiledemoapp/video/upload/v1706627936/Demo%20app%20content/sport-1_tjwumh.mp4") playerController = AVPlayerViewController() playerController.player = player addChild(playerController) playerController.videoGravity = .resizeAspectFill vwVideoView.addSubview(playerController.view) playerController.view.frame = vwVideoView.bounds playerController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] playerController.didMove(toParent: self) } private func setVideoFeedCollectionView() { collectionController = VideoFeedCollectionController(self) cvVideoFeed.delegate = collectionController cvVideoFeed.dataSource = collectionController } @objc func playerDidFinishPlaying(note: NSNotification) { player.seek(to: .zero) // Seek to the beginning of the video player.play() } } extension VideoViewController: VideoFeedCollectionDelegate { func cellClicked(_ index: Int) { if let controller = UIStoryboard(name: "VideoFeed", bundle: nil).instantiateViewController(identifier: "VideoFeedController") as? VideoFeedController { controller.modalPresentationStyle = .fullScreen self.present(controller, animated: true, completion: nil) } } } ================================================ FILE: Example/Cloudinary/Controllers/Video Feed/Helpers/VideoHelper.swift ================================================ // // VideoHelper.swift // ios-video-testing // // Created by Adi Mizrahi on 16/11/2023. // import Foundation class VideoHelper { static func parsePlist() -> [String]? { if let path = Bundle.main.path(forResource: "video_links", ofType: "plist") { let videoDictonary = NSDictionary(contentsOfFile: path) if let videoLinks = videoDictonary?.allValues as? [String] { return videoLinks } } return nil } } ================================================ FILE: Example/Cloudinary/Controllers/Video Feed/Resources/video_links.plist ================================================ video1 https://res.cloudinary.com/mobiledemoapp/video/upload/ar_9:16,c_fill,g_auto,w_600/f_auto:video,q_auto:eco/sprort-2_zgsr5k.mp4 video2 https://res.cloudinary.com/mobiledemoapp/video/upload/ar_9:16,c_fill,g_auto,w_600/f_auto:video,q_auto:eco/fashion-2_ewukga.mp4 video3 https://res.cloudinary.com/demo/video/upload/ar_9:16,c_fill,g_auto,w_600/f_auto:video,q_auto:eco/v1/docs/surfers video4 https://res.cloudinary.com/mobiledemoapp/video/upload/ar_9:16,c_fill,g_auto,w_600/f_auto:video,q_auto:eco/fashion-1_1_kuwihy.mp4 video5 https://res.cloudinary.com/demo/video/upload/ar_9:16,c_fill,g_auto,w_600/f_auto:video,q_auto:eco/v1/docs/car-film video6 https://res.cloudinary.com/demo/video/upload/ar_9:16,c_fill,g_auto,w_600/f_auto:video,q_auto:eco/v1/docs/rocky-mountains video7 https://res.cloudinary.com/demo/video/upload/ar_9:16,c_fill,g_auto,w_600/f_auto:video,q_auto:eco/v1/docs/sailing_boat ================================================ FILE: Example/Cloudinary/Controllers/Widgets/ImageWidgetViewController.swift ================================================ // // ImageWidgetViewController.swift // iOS_Geekle_Conference // // Created by Adi Mizrahi on 19/09/2023. // import Foundation import UIKit import Cloudinary class ImageWidgetViewController: UIViewController { @IBOutlet weak var ivLocal: CLDUIImageView! @IBOutlet weak var ivRemote: CLDUIImageView! @IBOutlet weak var ivCloudinary: CLDUIImageView! let cloudinary = CloudinaryHelper.shared.cloudinary override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setLocalImage() setRemoteImage() setCloudinaryImage() EventsHandler.shared.logEvent(event: EventObject(name: "Image Widget")) } private func setLocalImage() { ivLocal.image = UIImage(named: "house") } private func setRemoteImage() { ivRemote.cldSetImage("https://res.cloudinary.com/mobiledemoapp/image/upload/v1706628181/Demo%20app%20content/Frame_871_ao5o4r.jpg", cloudinary: cloudinary) } private func setCloudinaryImage() { ivCloudinary.cldSetImage(publicId: "Demo%20app%20content/Frame_871_ao5o4r", cloudinary: cloudinary) } } ================================================ FILE: Example/Cloudinary/Controllers/Widgets/UploadWidgetViewController.swift ================================================ // // UploadWidgetViewController.swift // iOS_Geekle_Conference // // Created by Adi Mizrahi on 19/09/2023. // import Foundation import UIKit import Cloudinary class UploadWidgetViewController: UIViewController { @IBOutlet weak var vwUploadContainer: UIView! @IBOutlet weak var aiLoading: UIActivityIndicatorView! @IBOutlet weak var ivMain: CLDUIImageView! @IBOutlet weak var vwOpenGallery: UIView! var cloudinary: CLDCloudinary! var uploadWidget: CLDUploaderWidget! var noCloudController: UIViewController! override func viewDidLoad() { super.viewDidLoad() setUploadButton() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if CloudinaryHelper.shared.getUploadCloud() == nil { openNoCloudController() } else { cloudinary = CLDCloudinary(configuration: CLDConfiguration(cloudName: CloudinaryHelper.shared.getUploadCloud()!)) } EventsHandler.shared.logEvent(event: EventObject(name: "Upload Widget")) } func setUploadButton() { vwOpenGallery.layer.cornerRadius = vwOpenGallery.frame.height / 2 let tapGesture = UITapGestureRecognizer(target: self, action: #selector(openUploadWidget)) vwOpenGallery.addGestureRecognizer(tapGesture) } @objc func openUploadWidget() { let configuration = CLDWidgetConfiguration( uploadType: CLDUploadType(signed: false, preset: "ios_sample")) uploadWidget = CLDUploaderWidget( cloudinary: cloudinary, configuration: configuration, images: nil, delegate: self) uploadWidget.presentWidget(from: self) } @objc func stepBack() { self.dismiss(animated: true) } private func showUploadingView() { vwUploadContainer.isHidden = true aiLoading.isHidden = false } private func hideUploadingView() { self.aiLoading.isHidden = true } private func openNoCloudController() { noCloudController = UIStoryboard(name: "UploadNoCloud", bundle: nil).instantiateViewController(identifier: "UploadNoCloudController") (noCloudController as! UploadNoCloudController).delegate = self // currentController.modalPresentationStyle = .fullScreen self.present(noCloudController, animated: true) } } extension UploadWidgetViewController: CLDUploaderWidgetDelegate { func uploadWidget(_ widget: CLDUploaderWidget, willCall uploadRequests: [CLDUploadRequest]) { uploadRequests[0].response( { response, error in self.hideUploadingView() self.ivMain.cldSetImage(response!.secureUrl!, cloudinary: self.cloudinary) } ) } func widgetDidCancel(_ widget: CLDUploaderWidget) { } func uploadWidgetDidDismiss() { showUploadingView() } } extension UploadWidgetViewController: UploadChoiceControllerDelegate { func switchToController(_ uploadState: UploadChoiceState, url: String?) { showUploadingView() cloudinary = CLDCloudinary(configuration: CLDConfiguration(cloudName: CloudinaryHelper.shared.getUploadCloud()!)) if noCloudController != nil { noCloudController.dismiss(animated: true) } } func dismissController() { } } ================================================ FILE: Example/Cloudinary/Controllers/Widgets/WidgetsViewController.swift ================================================ // // WidgetsViewController.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 25/01/2024. // import Foundation import UIKit import Cloudinary import AVKit class WidgetsViewController: UIViewController { @IBOutlet weak var vwImageWidgetVideo: UIView! @IBOutlet weak var vwUploadWidget: UIView! @IBOutlet weak var vwImageWidget: UIView! @IBOutlet weak var vwUploadWidgetVideo: UIView! var imageWidgetplayer: CLDVideoPlayer! var uploadWidgetplayer: CLDVideoPlayer! override func viewDidLoad() { super.viewDidLoad() setUploadWidgetView() setImageWidgetView() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) setUploadWidgetVideo() setImageWidgetVideo() EventsHandler.shared.logEvent(event: EventObject(name: "Widgets")) } private func setImageWidgetVideo() { imageWidgetplayer = CLDVideoPlayer(publicId: "DevApp_ImageUpload_02_vpsz7p", cloudinary: CloudinaryHelper.shared.cloudinary) configurePlayer(imageWidgetplayer, in: vwImageWidgetVideo) } private func setUploadWidgetVideo() { uploadWidgetplayer = CLDVideoPlayer(publicId: "DevApp_UploadWidget_02_r61cfi", cloudinary: CloudinaryHelper.shared.cloudinary) configurePlayer(uploadWidgetplayer, in: vwUploadWidgetVideo) } private func configurePlayer(_ player: CLDVideoPlayer, in view: UIView) { let playerLayer = AVPlayerLayer(player: player) view.backgroundColor = .black playerLayer.frame = view.bounds playerLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(playerLayer) // Add observer for AVPlayerItemDidPlayToEndTime NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem) player.play() } @objc private func playerItemDidReachEnd(notification: Notification) { if let playerItem = notification.object as? AVPlayerItem { // Seek to the start of the video to loop it playerItem.seek(to: .zero) uploadWidgetplayer.play() imageWidgetplayer.play() } } deinit { NotificationCenter.default.removeObserver(self) } private func setUploadWidgetView() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(uploadWidgetClicked)) vwUploadWidget.addGestureRecognizer(tapGesture) } private func setImageWidgetView() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(imageWidgetClicked)) vwImageWidget.addGestureRecognizer(tapGesture) } @objc private func uploadWidgetClicked() { if let controller = UIStoryboard(name: "Base", bundle: nil).instantiateViewController(identifier: "BaseViewController") as? BaseViewController { controller.type = .UploadWidget controller.modalPresentationStyle = .fullScreen self.present(controller, animated: true, completion: nil) } } @objc private func imageWidgetClicked() { if let controller = UIStoryboard(name: "Base", bundle: nil).instantiateViewController(identifier: "BaseViewController") as? BaseViewController { controller.type = .ImageWidget controller.modalPresentationStyle = .fullScreen self.present(controller, animated: true, completion: nil) } } } ================================================ FILE: Example/Cloudinary/Core Data/AssetItems+CoreDataClass.swift ================================================ // // AssetItems_CoreDataClass.swift // Cloudinary_Example // // Created by Adi Mizrahi on 18/07/2024. // Copyright © 2024 CocoaPods. All rights reserved. // import Foundation import CoreData public class AssetItems: NSManagedObject { } ================================================ FILE: Example/Cloudinary/Core Data/AssetItems+CoreDataProperties.swift ================================================ // // AssetItems+CoreDataProperties.swift // Cloudinary_Example // // Created by Adi Mizrahi on 18/07/2024. // Copyright © 2024 CocoaPods. All rights reserved. // import Foundation import CoreData extension AssetItems { @nonobjc public class func fetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: "AssetItems") } @NSManaged public var deliveryType: String @NSManaged public var assetType: String @NSManaged public var transformation: String? @NSManaged public var publicId: String @NSManaged public var url: String } extension AssetItems : Identifiable { } ================================================ FILE: Example/Cloudinary/Core Data/AssetModel.swift ================================================ // // AssetModel.swift // Cloudinary_Example // // Created by Adi Mizrahi on 18/07/2024. // Copyright © 2024 CocoaPods. All rights reserved. // import Foundation class AssetModel { private var deliveryType: String private var assetType: String private var transformation: String? private var publicId: String private var url: String init(deliveryType: String, assetType: String, transformation: String? = nil, publicId: String, url: String) { self.deliveryType = deliveryType self.assetType = assetType self.transformation = transformation self.publicId = publicId self.url = url } func setDeliveryType(_ type: String) { deliveryType = type } func setAssetType(_ type: String) { assetType = type } func setTransformation(_ trans: String) { transformation = trans } func setPublicId(_ id: String) { publicId = id } func setUrl(_ url: String) { self.url = url } func getDeliveryType() -> String { return deliveryType } func getAssetType() -> String { return assetType } func getTransformation() -> String? { return transformation } func getPublicId() -> String { return publicId } func getUrl() -> String { return url } } ================================================ FILE: Example/Cloudinary/Core Data/AssetModel.xcdatamodeld/Model.xcdatamodel/contents ================================================ ================================================ FILE: Example/Cloudinary/Core Data/CoreDataHelper.swift ================================================ // // CoreDataHelper.swift // Cloudinary_Example // // Created by Adi Mizrahi on 18/07/2024. // Copyright © 2024 CocoaPods. All rights reserved. // import Foundation import CoreData class CoreDataHelper { static let shared = CoreDataHelper() lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "AssetModel") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container }() func saveContext() { let context = persistentContainer.viewContext if context.hasChanges { do { try context.save() } catch { let nserror = error as NSError fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } } func insertData(_ item: AssetModel) { let context = CoreDataHelper.shared.persistentContainer.viewContext let newDelivery = AssetItems(context: context) newDelivery.deliveryType = item.getDeliveryType() newDelivery.assetType = item.getAssetType() newDelivery.transformation = item.getTransformation() ?? "" newDelivery.publicId = item.getPublicId() newDelivery.url = item.getUrl() do { try context.save() } catch { print("Error saving context: \(error)") } } func fetchSingleData(publicId: String) -> AssetModel? { let context = CoreDataHelper.shared.persistentContainer.viewContext let fetchRequest: NSFetchRequest = AssetItems.fetchRequest() do { let items = try context.fetch(fetchRequest) for item in items { if item.publicId == publicId { return AssetModel(deliveryType: item.deliveryType, assetType: item.assetType, transformation: item.transformation, publicId: item.publicId, url: item.url) } } } catch { print("Error fetching data: \(error)") } return nil } func fetchData() -> [AssetItems]? { let context = CoreDataHelper.shared.persistentContainer.viewContext let fetchRequest: NSFetchRequest = AssetItems.fetchRequest() do { let items = try context.fetch(fetchRequest) return items } catch { print("Error fetching data: \(error)") } return nil } } ================================================ FILE: Example/Cloudinary/Custom Views/GradientView.swift ================================================ // // GradientView.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 27/12/2023. // import Foundation import UIKit @IBDesignable class GradientView: UIView { @IBInspectable var firstColor: UIColor = UIColor.clear { didSet { updateView() } } @IBInspectable var secondColor: UIColor = UIColor.clear { didSet { updateView() } } override class var layerClass: AnyClass { get { return CAGradientLayer.self } } override func layoutSubviews() { super.layoutSubviews() updateView() } func updateView() { guard let gradientLayer = layer as? CAGradientLayer else { return } gradientLayer.colors = [UIColor(red: 0.204, green: 0.282, blue: 0.773, alpha: 1).cgColor, UIColor(red: 0.157, green: 0.733, blue: 0.98, alpha: 1).cgColor ] gradientLayer.locations = [0, 1] gradientLayer.startPoint = CGPoint(x: 0, y: 0) // Top-left gradientLayer.endPoint = CGPoint(x: 1, y: 1) // Bottom-right // gradientLayer.transform = CATransform3DMakeAffineTransform(CGAffineTransform(a: 0.98, b: 0.98, c: -0.98, d: 0.45, tx: 0.5, ty: -0.21)) gradientLayer.position = self.center } } ================================================ FILE: Example/Cloudinary/Custom Views/RevealImageView.swift ================================================ // // RevealImageView.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 08/01/2024. // import Foundation import UIKit class RevealImageView: UIImageView { public var leftImage: UIImage? { didSet { if let img = leftImage { leftImageLayer.contents = img.cgImage } } } public var rightImage: UIImage? { didSet { if let img = rightImage { self.image = img } } } // private properties private let leftImageLayer = CALayer() private let maskLayer = CALayer() private let lineView = UIView() private var pct: CGFloat = 0.5 { didSet { updateView() } } convenience init() { self.init(frame: .zero) } override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { // any opaque color maskLayer.backgroundColor = UIColor.black.cgColor leftImageLayer.mask = maskLayer // the "reveal" image layer layer.addSublayer(leftImageLayer) // the vertical line lineView.backgroundColor = .white addSubview(lineView) isUserInteractionEnabled = true } override func touchesBegan(_ touches: Set, with event: UIEvent?) { guard let t = touches.first else { return } let loc = t.location(in: self) pct = loc.x / bounds.width } override func touchesMoved(_ touches: Set, with event: UIEvent?) { guard let t = touches.first else { return } let loc = t.location(in: self) pct = loc.x / bounds.width } private func updateView() { // move the vertical line to the touch point lineView.frame = CGRect(x: bounds.width * pct, y: bounds.minY, width: 4, height: bounds.height) // update the "left image" mask to the touch point var r = bounds r.size.width = bounds.width * pct // disable layer animation CATransaction.begin() CATransaction.setDisableActions(true) maskLayer.frame = r CATransaction.commit() } override func layoutSubviews() { super.layoutSubviews() leftImageLayer.frame = bounds updateView() } } ================================================ FILE: Example/Cloudinary/Custom Views/ToolBar/Item/ToolbarItem.swift ================================================ // // ToolbarItem.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 27/12/2023. // import Foundation import UIKit class ToolbarItem: UIView { @IBOutlet weak var ivMain: UIImageView! @IBOutlet weak var lbMain: UILabel! override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) commonInit() } private func commonInit() { guard let view = loadViewFromNib() else { return } view.frame = bounds view.autoresizingMask = [.flexibleWidth, .flexibleHeight] addSubview(view) } private func loadViewFromNib() -> UIView? { let nib = UINib(nibName: "ToolbarItem", bundle: nil) return nib.instantiate(withOwner: self, options: nil).first as? UIView } func selectItem() { ivMain.tintColor = UIColor(named: "primary") lbMain.textColor = UIColor(named: "primary") } func unselectItem() { ivMain.tintColor = UIColor(named: "text_not_selected") lbMain.textColor = UIColor(named: "text_not_selected") } } ================================================ FILE: Example/Cloudinary/Custom Views/ToolBar/Item/ToolbarItem.xib ================================================ ================================================ FILE: Example/Cloudinary/Custom Views/ToolBar/Toolbar.swift ================================================ // // Toolbar.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 27/12/2023. // import Foundation import UIKit protocol ToolbarDelegate { func deliverySelected() func uploadSelected() func widgetsSelected() func videoSelected() } class Toolbar: UIView { @IBOutlet weak var vwDelivery: ToolbarItem! @IBOutlet weak var vwUpload: ToolbarItem! @IBOutlet weak var vwWidgets: ToolbarItem! @IBOutlet weak var vwVideo: ToolbarItem! var delegate: ToolbarDelegate init(frame: CGRect, delegate: ToolbarDelegate) { self.delegate = delegate super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func commonInit() { let nibName = String(describing: type(of: self)) if let view = Bundle.main.loadNibNamed(nibName, owner: self, options: nil)?.first as? UIView { view.frame = bounds addSubview(view) } setItems() setupGestures() selectItem(.Delivery) } private func setItems() { vwDelivery.ivMain.image = UIImage(named: "car-speed-limiter") vwDelivery.lbMain.text = "Delivery" vwUpload.ivMain.image = UIImage(named: "upload") vwUpload.lbMain.text = "Upload" vwWidgets.ivMain.image = UIImage(named: "widgets") vwWidgets.lbMain.text = "Widgets" vwVideo.ivMain.image = UIImage(named: "video") vwVideo.lbMain.text = "Video" } private func setupGestures() { let deliveryTapGesture = UITapGestureRecognizer(target: self, action: #selector(deliveryViewTapped)) vwDelivery.addGestureRecognizer(deliveryTapGesture) let uploadTapGesture = UITapGestureRecognizer(target: self, action: #selector(uploadViewTapped)) vwUpload.addGestureRecognizer(uploadTapGesture) let widgetTapGesture = UITapGestureRecognizer(target: self, action: #selector(widgetsViewTapped)) vwWidgets.addGestureRecognizer(widgetTapGesture) let videoTapGesture = UITapGestureRecognizer(target: self, action: #selector(videoViewTapped)) vwVideo.addGestureRecognizer(videoTapGesture) } @objc private func deliveryViewTapped() { selectItem(.Delivery) } @objc private func uploadViewTapped() { selectItem(.Upload) } @objc private func widgetsViewTapped() { selectItem(.Widgets) } @objc private func videoViewTapped() { selectItem(.Video) } private func selectItem(_ item: ToolbarOptions) { switch item { case .Delivery: vwDelivery.selectItem() vwUpload.unselectItem() vwWidgets.unselectItem() vwVideo.unselectItem() delegate.deliverySelected() break case .Upload: vwDelivery.unselectItem() vwUpload.selectItem() vwWidgets.unselectItem() vwVideo.unselectItem() delegate.uploadSelected() break case .Widgets: vwDelivery.unselectItem() vwUpload.unselectItem() vwWidgets.selectItem() vwVideo.unselectItem() delegate.widgetsSelected() break case .Video: vwDelivery.unselectItem() vwUpload.unselectItem() vwWidgets.unselectItem() vwVideo.selectItem() delegate.videoSelected() break } } private func unselectItem(_ item: ToolbarOptions) { switch item { case .Delivery: break case .Upload: break case .Widgets: break case .Video: break } } } enum ToolbarOptions { case Delivery case Upload case Widgets case Video } ================================================ FILE: Example/Cloudinary/Custom Views/ToolBar/Toolbar.xib ================================================ ================================================ FILE: Example/Cloudinary/Custom Views/Upload Loading View/UploadLoadingView.swift ================================================ // // UploadLoadingView.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 11/01/2024. // import Foundation import UIKit class UploadLoadingView: UIView { @IBOutlet weak var aiLoading: UIActivityIndicatorView! override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) commonInit() } private func commonInit() { guard let view = loadViewFromNib() else { return } view.frame = bounds view.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.layer.cornerRadius = 4 addSubview(view) } private func loadViewFromNib() -> UIView? { let nib = UINib(nibName: "UploadLoadingView", bundle: nil) return nib.instantiate(withOwner: self, options: nil).first as? UIView } func startAnimation() { aiLoading.startAnimating() } func stopAnimation() { aiLoading.stopAnimating() } } ================================================ FILE: Example/Cloudinary/Custom Views/Upload Loading View/UploadLoadingView.xib ================================================ ================================================ FILE: Example/Cloudinary/Extensions/Double+Extension.swift ================================================ // // Double+Extension.swift // iOS_Geekle_Conference // // Created by Adi Mizrahi on 18/09/2023. // import Foundation extension Double { /// Rounds the double to decimal places value func rounded(toPlaces places:Int) -> Double { let divisor = pow(10.0, Double(places)) return (self * divisor).rounded() / divisor } } ================================================ FILE: Example/Cloudinary/GoogleService-Info.plist ================================================ API_KEY AIzaSyCFgj2Omu6hBF7frbKT-LHQHJmXiiK2OOo GCM_SENDER_ID 554013255617 PLIST_VERSION 1 BUNDLE_ID com.cloudinary.Cloudinary-Sample-App PROJECT_ID ios-sample-app-d3d7d STORAGE_BUCKET ios-sample-app-d3d7d.appspot.com IS_ADS_ENABLED IS_ANALYTICS_ENABLED IS_APPINVITE_ENABLED IS_GCM_ENABLED IS_SIGNIN_ENABLED GOOGLE_APP_ID 1:554013255617:ios:8a787dcf8bc7e668ca3140 ================================================ FILE: Example/Cloudinary/Helpers/AnimationHelper.swift ================================================ // // AnimationHelper.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 03/01/2024. // import Foundation import UIKit class AnimationHelper { public static func animateOut(view: UIView) { UIView.animate(withDuration: 0.5) { view.alpha = 0 } completion: { _ in view.removeFromSuperview() } } public static func animateTabController(_ vwContainer: UIView, _ newController: UIViewController, currentViewController: UIViewController?, completion: @escaping () -> Void) { let animationDirection = getAnimationDirection(newController, currentViewController) // Add new controller as child // Add the new controller's view vwContainer.addSubview(newController.view) // Perform the animation if animationDirection == .leftToRight { newController.view.frame = CGRect(x: vwContainer.bounds.width, y: 0, width: vwContainer.bounds.width, height: vwContainer.bounds.height) UIView.animate(withDuration: 0.5) { // Slide the current view out to the left if let viewController = currentViewController { viewController.view.frame.origin.x = -vwContainer.bounds.width } // Slide the new view in from the right newController.view.frame.origin.x = 0 } completion: { (_) in completion() } } else { newController.view.frame = CGRect(x: -vwContainer.bounds.width, y: 0, width: vwContainer.bounds.width, height: vwContainer.bounds.height) UIView.animate(withDuration: 0.25) { if let viewController = currentViewController { viewController.view.frame.origin.x = vwContainer.bounds.width } // Slide the new view in from the left newController.view.frame.origin.x = 0 } completion: { (_) in completion() } } } private static func getAnimationDirection(_ newController: UIViewController, _ currentController: UIViewController?) -> AnimationDirection { guard let currentController = currentController else { return .leftToRight } if let _ = newController as? DeliveryViewController { return .rightToLeft; } if let _ = newController as? UploadViewController { if let _ = currentController as? DeliveryViewController { return .leftToRight } else { return .rightToLeft } } return .leftToRight } public static func animateTitleOut(_ view: UIView) { UIView.animate(withDuration: 0.25, animations: { view.alpha = 0.0 }) } public static func animateTitleIn(_ view: UIView) { UIView.animate(withDuration: 0.25, animations: { view.alpha = 1.0 }) } enum AnimationDirection { case rightToLeft case leftToRight } } ================================================ FILE: Example/Cloudinary/Helpers/Events/EventObject.swift ================================================ // // EventObject.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 05/02/2024. // import Foundation class EventObject{ var eventName:String?; var eventAttributes:[String:String]?; init(name: String, attributes: [String: String]? = nil) { self.eventName = name self.eventAttributes = attributes } func getAttributes() -> [String:String]? { return eventAttributes; } func setEventAttribute(_ key:String,_ value:String) { eventAttributes![key] = value; } func setEventAttributes(attrs:[String:String]) { self.eventAttributes = attrs; } func getEventName()->String { return eventName!; } func setEventName(name:String) { eventName = name; } } ================================================ FILE: Example/Cloudinary/Helpers/Events/EventsHandler.swift ================================================ // // EventsHandler.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 05/02/2024. // import Foundation import Foundation class EventsHandler{ static let shared = EventsHandler(); private var providersList = [EventsProvider](); private init() { initProvidersList(); } private func initProvidersList(){ providersList.append(FirebaseEventsHandler()) } func logEvent(event:EventObject){ for provider in providersList{ provider.logEvent(event: event); } } } ================================================ FILE: Example/Cloudinary/Helpers/Events/EventsProvider.swift ================================================ // // EventsProvider.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 05/02/2024. // import Foundation protocol EventsProvider { func logEvent(event: EventObject) } ================================================ FILE: Example/Cloudinary/Helpers/Events/FirebaseEventsHandler.swift ================================================ // // FirebaseEventsHandler.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 05/02/2024. // import Foundation //import Firebase class FirebaseEventsHandler: EventsProvider { func logEvent(event: EventObject) { // Analytics.logEvent(event.getEventName(), parameters: event.getAttributes()) } } ================================================ FILE: Example/Cloudinary/Helpers/ImageHelper.swift ================================================ // // ImageHelper.swift // Cloudinary_Sample_App // // Created by Adi Mizrahi on 09/01/2024. // import Foundation import UIKit class ImageHelper { static func getImageFromURL(_ url: URL, completion: @escaping (UIImage?) -> Void) { URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data, error == nil else { completion(nil) return } if let image = UIImage(data: data) { completion(image) } else { completion(nil) } }.resume() } } ================================================ FILE: Example/Cloudinary/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName Cloudinary CFBundleShortVersionString 1.0 CFBundleVersion 1 NSPhotoLibraryUsageDescription The app needs access to the photo library in order for you to choose photos and videos for uploading UIApplicationSceneManifest UIApplicationSupportsMultipleScenes UISceneConfigurations UIWindowSceneSessionRoleApplication UISceneConfigurationName Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate UISceneStoryboardFile Splash UILaunchStoryboardName LaunchScreen.storyboard UIStatusBarStyle UISupportedInterfaceOrientations UIInterfaceOrientationPortrait ================================================ FILE: Example/Cloudinary/SceneDelegate.swift ================================================ // // SceneDelegate.swift // iOS_Geekle_Conference // // Created by Adi Mizrahi on 11/09/2023. // import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let _ = (scene as? UIWindowScene) else { return } } func sceneDidDisconnect(_ scene: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). } func sceneDidBecomeActive(_ scene: UIScene) { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } func sceneWillResignActive(_ scene: UIScene) { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). } func sceneWillEnterForeground(_ scene: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. } func sceneDidEnterBackground(_ scene: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. } } ================================================ FILE: Example/Cloudinary/Utils/CloudinaryHelper.swift ================================================ // // CloudinaryHelper.swift // iOS_Geekle_Conference // // Created by Adi Mizrahi on 12/09/2023. // import Foundation import Cloudinary class CloudinaryHelper { static let shared = CloudinaryHelper() var cloudinary: CLDCloudinary init() { cloudinary = CLDCloudinary(configuration: CLDConfiguration(cloudName: "mobiledemoapp", secure: true)) } func setUploadCloud(_ cloudName: String?) { guard let cloudName = cloudName else { return } UserDefaults.standard.set(cloudName, forKey: "uploadCloudName") } func getUploadCloud() -> String? { guard let cloudName = UserDefaults.standard.value(forKey: "uploadCloudName") as? String else { return nil } return cloudName } } ================================================ FILE: Example/Cloudinary/Utils/FileUtils.swift ================================================ // // FileUtils.swift // iOS_Geekle_Conference // // Created by Adi Mizrahi on 18/09/2023. // import Foundation import UIKit class FileUtils { static func getFileSizeForImage(_ image: UIImage) -> String { guard let imgData = image.jpegData(compressionQuality: 1.0) else { return "" } let imageSize: Int = imgData.count let size = Double(imageSize) / 1024.0 / 1024.0 return "\(size.rounded(toPlaces: 2))" } static func getImageInfo(_ url: URL, completion: @escaping (_ format: String, _ size: String, _ dimensions: (width: CGFloat, height: CGFloat)) -> Void) { getData(from: url) { data, response, error in guard let data = data, error == nil else { // Handle error case return } DispatchQueue.main.async { guard data.count > 0 else { return } let format = ImageFormat.get(from: data).rawValue let imageSize: Int = data.count let size = Double(imageSize) / 1024.0 / 1024.0 let image = UIImage(data: data) let dimensions = image.map { ($0.size.width, $0.size.height) } completion(format, "\(size.rounded(toPlaces: 2))", dimensions!) } } } private static func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> ()) { let request = URLRequest(url: url) URLSession.shared.dataTask(with: request, completionHandler: completion).resume() } } enum ImageFormat: String { case png, jpg, gif, tiff, webp, heic, unknown } extension ImageFormat { static func get(from data: Data) -> ImageFormat { switch data[0] { case 0x89: return .png case 0xFF: return .jpg case 0x47: return .gif case 0x49, 0x4D: return .tiff case 0x52 where data.count >= 12: let subdata = data[0...11] if let dataString = String(data: subdata, encoding: .ascii), dataString.hasPrefix("RIFF"), dataString.hasSuffix("WEBP") { return .webp } case 0x00 where data.count >= 12 : let subdata = data[8...11] if let dataString = String(data: subdata, encoding: .ascii), Set(["heic", "heix", "hevc", "hevx"]).contains(dataString) ///OLD: "ftypheic", "ftypheix", "ftyphevc", "ftyphevx" { return .heic } default: break } return .unknown } var contentType: String { return "image/\(rawValue)" } } ================================================ FILE: Example/Cloudinary/Views/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Base/Base.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Transform/Delivery.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Transform/Optimization.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Transform/Use Cases/ConditionalProduct.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Transform/Use Cases/IntegrateAI.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Transform/Use Cases/NormalizingProductAssets.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Transform/UseCases.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Upload/InnerUploadFrame.xib ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Upload/Upload.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Upload/UploadNoCloud.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Video/Social/VideoSocialOverlays.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Video/Video.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Video/VideoFeed.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Widgets/ImageWidget.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Widgets/UploadWidget.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Inner Views/Widgets/Widgets.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Splash.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Transform/RevealImage.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Transform/SmartCropping.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Transform/Transform.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Upload/SingleUpload.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Upload/SingleUploadPreview.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Upload/UploadChoice.storyboard ================================================ ================================================ FILE: Example/Cloudinary/Views/Upload/UploadDoesNotExist.storyboard ================================================ ================================================ FILE: Example/Cloudinary.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 1E635A3E25A756870003E9D3 /* ObjcBaseTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E635A3D25A756860003E9D3 /* ObjcBaseTestCase.m */; }; 1E635A7025A757DA0003E9D3 /* BaseMockProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E635A6F25A757DA0003E9D3 /* BaseMockProvider.swift */; }; 1E635A7F25A7585E0003E9D3 /* MockProviderQualityAnalysis.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E635A7825A7585E0003E9D3 /* MockProviderQualityAnalysis.swift */; }; 1E635A8025A7585E0003E9D3 /* ObjcUploaderQualityAnalysisTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E635A7925A7585E0003E9D3 /* ObjcUploaderQualityAnalysisTests.m */; }; 1E635A8125A7585E0003E9D3 /* QualityAnalysisUploadResultParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E635A7A25A7585E0003E9D3 /* QualityAnalysisUploadResultParserTests.swift */; }; 1E635A8225A7585E0003E9D3 /* ObjcQualityAnalysisExplicitResultParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E635A7B25A7585E0003E9D3 /* ObjcQualityAnalysisExplicitResultParserTests.m */; }; 1E635A8325A7585E0003E9D3 /* UploaderQualityAnalysisTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E635A7C25A7585E0003E9D3 /* UploaderQualityAnalysisTests.swift */; }; 1E635A8425A7585E0003E9D3 /* QualityAnalysisExplicitResultParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E635A7D25A7585E0003E9D3 /* QualityAnalysisExplicitResultParserTests.swift */; }; 1E635A8525A7585E0003E9D3 /* ObjcQualityAnalysisUploadResultParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E635A7E25A7585E0003E9D3 /* ObjcQualityAnalysisUploadResultParserTests.m */; }; 270C1BF723FBBA3C00A503C3 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 270C1BF623FBBA3C00A503C3 /* LICENSE */; }; 272C517F242B6C360093AB1B /* SessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C513E242B6C340093AB1B /* SessionManagerTests.swift */; }; 272C5180242B6C360093AB1B /* FileManager+CloudinaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C513F242B6C340093AB1B /* FileManager+CloudinaryTests.swift */; }; 272C5181242B6C360093AB1B /* AuthenticationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C5140242B6C340093AB1B /* AuthenticationTests.swift */; }; 272C5182242B6C360093AB1B /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C5141242B6C350093AB1B /* RequestTests.swift */; }; 272C5183242B6C360093AB1B /* SessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C5142242B6C350093AB1B /* SessionDelegateTests.swift */; }; 272C5185242B6C360093AB1B /* ResponseSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C5144242B6C350093AB1B /* ResponseSerializationTests.swift */; }; 272C5186242B6C360093AB1B /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C5145242B6C350093AB1B /* MultipartFormDataTests.swift */; }; 272C5187242B6C360093AB1B /* URLProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C5146242B6C350093AB1B /* URLProtocolTests.swift */; }; 272C5189242B6C360093AB1B /* ResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C5148242B6C350093AB1B /* ResponseTests.swift */; }; 272C518C242B6C360093AB1B /* BaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C514B242B6C350093AB1B /* BaseTestCase.swift */; }; 272C518D242B6C360093AB1B /* unicorn.png in Resources */ = {isa = PBXBuildFile; fileRef = 272C514E242B6C350093AB1B /* unicorn.png */; }; 272C518E242B6C360093AB1B /* rainbow.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 272C514F242B6C350093AB1B /* rainbow.jpg */; }; 272C518F242B6C360093AB1B /* invalid.data in Resources */ = {isa = PBXBuildFile; fileRef = 272C5152242B6C350093AB1B /* invalid.data */; }; 272C5190242B6C360093AB1B /* valid.data in Resources */ = {isa = PBXBuildFile; fileRef = 272C5153242B6C350093AB1B /* valid.data */; }; 272C5191242B6C360093AB1B /* empty.data in Resources */ = {isa = PBXBuildFile; fileRef = 272C5154242B6C350093AB1B /* empty.data */; }; 272C5192242B6C360093AB1B /* empty_data.json in Resources */ = {isa = PBXBuildFile; fileRef = 272C5156242B6C350093AB1B /* empty_data.json */; }; 272C5193242B6C360093AB1B /* valid_data.json in Resources */ = {isa = PBXBuildFile; fileRef = 272C5157242B6C350093AB1B /* valid_data.json */; }; 272C5194242B6C360093AB1B /* invalid_data.json in Resources */ = {isa = PBXBuildFile; fileRef = 272C5158242B6C350093AB1B /* invalid_data.json */; }; 272C5195242B6C360093AB1B /* utf8_string.txt in Resources */ = {isa = PBXBuildFile; fileRef = 272C515A242B6C350093AB1B /* utf8_string.txt */; }; 272C5196242B6C360093AB1B /* utf32_string.txt in Resources */ = {isa = PBXBuildFile; fileRef = 272C515B242B6C350093AB1B /* utf32_string.txt */; }; 272C5197242B6C360093AB1B /* empty_string.txt in Resources */ = {isa = PBXBuildFile; fileRef = 272C515C242B6C350093AB1B /* empty_string.txt */; }; 272C51B0242B6C360093AB1B /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C5179242B6C360093AB1B /* UploadTests.swift */; }; 272C51B1242B6C360093AB1B /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C517A242B6C360093AB1B /* CacheTests.swift */; }; 272C51B3242B6C360093AB1B /* CLDNError+CloudinaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C517C242B6C360093AB1B /* CLDNError+CloudinaryTests.swift */; }; 272C51B4242B6C360093AB1B /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C517D242B6C360093AB1B /* ResultTests.swift */; }; 272C51B5242B6C360093AB1B /* ParameterEncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C517E242B6C360093AB1B /* ParameterEncodingTests.swift */; }; 274C6D2D23FBAF5E0090BC40 /* CryptoUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274C6D1323FBAF5E0090BC40 /* CryptoUtilsTests.swift */; }; 274C6D2E23FBAF5E0090BC40 /* PreprocessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274C6D1423FBAF5E0090BC40 /* PreprocessTests.swift */; }; 274C6D2F23FBAF5E0090BC40 /* FileUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274C6D1523FBAF5E0090BC40 /* FileUtilsTests.swift */; }; 274C6D3023FBAF5E0090BC40 /* UrlTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 274C6D1723FBAF5E0090BC40 /* UrlTests.m */; }; 274C6D3123FBAF5E0090BC40 /* UrlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274C6D1823FBAF5E0090BC40 /* UrlTests.swift */; }; 274C6D3223FBAF5E0090BC40 /* UIBaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274C6D1A23FBAF5E0090BC40 /* UIBaseTest.swift */; }; 274C6D3323FBAF5E0090BC40 /* UIButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274C6D1B23FBAF5E0090BC40 /* UIButtonTests.swift */; }; 274C6D3423FBAF5E0090BC40 /* UIImageViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274C6D1C23FBAF5E0090BC40 /* UIImageViewTests.swift */; }; 274C6D3623FBAF5E0090BC40 /* UploaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274C6D1F23FBAF5E0090BC40 /* UploaderTests.swift */; }; 274C6D3723FBAF5E0090BC40 /* ManagementApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274C6D2023FBAF5E0090BC40 /* ManagementApiTests.swift */; }; 274C6D3823FBAF5E0090BC40 /* DownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274C6D2123FBAF5E0090BC40 /* DownloaderTests.swift */; }; 274C6D3923FBAF5E0090BC40 /* StringUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274C6D2223FBAF5E0090BC40 /* StringUtilsTest.swift */; }; 274C6D3A23FBAF5E0090BC40 /* borderCollie.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 274C6D2523FBAF5E0090BC40 /* borderCollie.jpg */; }; 274C6D3B23FBAF5E0090BC40 /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 274C6D2623FBAF5E0090BC40 /* logo.png */; }; 274C6D3C23FBAF5E0090BC40 /* docx.docx in Resources */ = {isa = PBXBuildFile; fileRef = 274C6D2823FBAF5E0090BC40 /* docx.docx */; }; 274C6D3D23FBAF5E0090BC40 /* pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 274C6D2923FBAF5E0090BC40 /* pdf.pdf */; }; 274C6D3E23FBAF5E0090BC40 /* dog.mov in Resources */ = {isa = PBXBuildFile; fileRef = 274C6D2B23FBAF5E0090BC40 /* dog.mov */; }; 274C6D3F23FBAF5E0090BC40 /* dog.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 274C6D2C23FBAF5E0090BC40 /* dog.mp4 */; }; 27BC1FAE2431C3F6000AFC2C /* CLDNDataResponse+CloudinaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BC1FAD2431C3F6000AFC2C /* CLDNDataResponse+CloudinaryTests.swift */; }; 27BC1FB02431C48D000AFC2C /* CLDNResult+CloudinaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BC1FAF2431C48D000AFC2C /* CLDNResult+CloudinaryTests.swift */; }; 5B1EF16C5EA94AD09F6C22C2 /* Pods_Cloudinary_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D07C87FEDB1F561D123D6873 /* Pods_Cloudinary_Tests.framework */; }; 5D4D8C2D246098F900AE9C96 /* CLDConditionExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D4D8C2B246098BA00AE9C96 /* CLDConditionExpressionTests.swift */; }; 5D4D8C2E24609C1400AE9C96 /* ValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272C514A242B6C350093AB1B /* ValidationTests.swift */; }; 5D53A9512488CE23005C14AB /* CLDConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D53A94F2488CE23005C14AB /* CLDConfigurationTests.swift */; }; 5D53A9522488CE23005C14AB /* CLDConfigurationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D53A9502488CE23005C14AB /* CLDConfigurationTests.m */; }; 5D7C2DCF245E93F7007E95F7 /* CLDExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D7C2DCE245E93F7007E95F7 /* CLDExpressionTests.swift */; }; 5DB2D29824FFE32500001845 /* UploaderWidgetConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DB2D29624FFE32500001845 /* UploaderWidgetConfigurationTests.swift */; }; 5DB2D29924FFE32500001845 /* UploaderWidgetConfigurationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DB2D29724FFE32500001845 /* UploaderWidgetConfigurationTests.m */; }; 5DE4EC202469919A00F6C8D6 /* CLDTransformationBaselineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE4EC1E2469919700F6C8D6 /* CLDTransformationBaselineTests.swift */; }; 9753E077581D46FC1463D265 /* Pods_Cloudinary_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EAA36BF6CA4E5748DFCD320 /* Pods_Cloudinary_Example.framework */; }; B60F3C222D4B86AF00160FFC /* TestableCloudinary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60F3C212D4B86AF00160FFC /* TestableCloudinary.swift */; }; B64120BD2C48ED3F005A5495 /* SingleUploadCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64120BC2C48ED3F005A5495 /* SingleUploadCell.swift */; }; B64120BF2C48ED69005A5495 /* SingleUploadCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64120BE2C48ED69005A5495 /* SingleUploadCollectionController.swift */; }; B64120C12C48ED83005A5495 /* SingleUploadCollectionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64120C02C48ED83005A5495 /* SingleUploadCollectionLayout.swift */; }; B64120C42C48EE76005A5495 /* AssetItems+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64120C32C48EE76005A5495 /* AssetItems+CoreDataClass.swift */; }; B64120C62C48EEC7005A5495 /* AssetItems+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64120C52C48EEC7005A5495 /* AssetItems+CoreDataProperties.swift */; }; B64120C82C48EED7005A5495 /* AssetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64120C72C48EED7005A5495 /* AssetModel.swift */; }; B64120CB2C48EF10005A5495 /* AssetModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B64120C92C48EF10005A5495 /* AssetModel.xcdatamodeld */; }; B64120CD2C48EF22005A5495 /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64120CC2C48EF22005A5495 /* CoreDataHelper.swift */; }; B64120CF2C48F049005A5495 /* SingleUploadPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64120CE2C48F049005A5495 /* SingleUploadPreview.swift */; }; B64120D12C48F0EC005A5495 /* SingleUploadPreview.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64120D02C48F0EC005A5495 /* SingleUploadPreview.storyboard */; }; B64ED1E22B8C933B00BE653E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1632B8C933900BE653E /* AppDelegate.swift */; }; B64ED1E32B8C933B00BE653E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1642B8C933900BE653E /* GoogleService-Info.plist */; }; B64ED1E52B8C933B00BE653E /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1672B8C933A00BE653E /* Colors.xcassets */; }; B64ED1E62B8C933B00BE653E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1682B8C933A00BE653E /* Assets.xcassets */; }; B64ED1E72B8C933B00BE653E /* VideoFeedCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED16C2B8C933A00BE653E /* VideoFeedCollectionController.swift */; }; B64ED1E82B8C933B00BE653E /* VideoFeedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED16E2B8C933A00BE653E /* VideoFeedCell.swift */; }; B64ED1E92B8C933B00BE653E /* OptimizationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1712B8C933A00BE653E /* OptimizationViewController.swift */; }; B64ED1EA2B8C933B00BE653E /* TransformCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1732B8C933A00BE653E /* TransformCollectionCell.swift */; }; B64ED1EB2B8C933B00BE653E /* TransformViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1742B8C933A00BE653E /* TransformViewController.swift */; }; B64ED1EC2B8C933B00BE653E /* TransformationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1752B8C933A00BE653E /* TransformationCell.swift */; }; B64ED1ED2B8C933B00BE653E /* RevealImageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1772B8C933A00BE653E /* RevealImageController.swift */; }; B64ED1EE2B8C933B00BE653E /* DeliveryTransformCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1782B8C933A00BE653E /* DeliveryTransformCollectionController.swift */; }; B64ED1EF2B8C933B00BE653E /* TransformCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1792B8C933A00BE653E /* TransformCollectionController.swift */; }; B64ED1F02B8C933B00BE653E /* SmartCroppingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED17B2B8C933A00BE653E /* SmartCroppingController.swift */; }; B64ED1F12B8C933B00BE653E /* UseCaseCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED17D2B8C933A00BE653E /* UseCaseCell.swift */; }; B64ED1F22B8C933B00BE653E /* UseCaseCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED17E2B8C933A00BE653E /* UseCaseCollectionCell.swift */; }; B64ED1F32B8C933B00BE653E /* ConditionalProductViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1802B8C933A00BE653E /* ConditionalProductViewController.swift */; }; B64ED1F42B8C933B00BE653E /* NormalizingProductAssetsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1812B8C933A00BE653E /* NormalizingProductAssetsViewController.swift */; }; B64ED1F52B8C933B00BE653E /* UseCasesCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1822B8C933A00BE653E /* UseCasesCollectionController.swift */; }; B64ED1F62B8C933B00BE653E /* DeliveryUseCasesCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1832B8C933A00BE653E /* DeliveryUseCasesCollectionController.swift */; }; B64ED1F72B8C933B00BE653E /* UseCasesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1842B8C933A00BE653E /* UseCasesViewController.swift */; }; B64ED1F82B8C933B00BE653E /* DeliveryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1852B8C933A00BE653E /* DeliveryViewController.swift */; }; B64ED1F92B8C933B00BE653E /* UploadChoiceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1872B8C933A00BE653E /* UploadChoiceController.swift */; }; B64ED1FA2B8C933B00BE653E /* InnerUploadFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1882B8C933A00BE653E /* InnerUploadFrame.swift */; }; B64ED1FB2B8C933B00BE653E /* SingleUploadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1892B8C933A00BE653E /* SingleUploadViewController.swift */; }; B64ED1FC2B8C933B00BE653E /* UploadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED18A2B8C933A00BE653E /* UploadViewController.swift */; }; B64ED1FD2B8C933B00BE653E /* UploadNoCloudController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED18B2B8C933A00BE653E /* UploadNoCloudController.swift */; }; B64ED1FE2B8C933B00BE653E /* UploadDoesNotExistController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED18D2B8C933A00BE653E /* UploadDoesNotExistController.swift */; }; B64ED1FF2B8C933B00BE653E /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED18E2B8C933A00BE653E /* MainViewController.swift */; }; B64ED2002B8C933B00BE653E /* video_links.plist in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1912B8C933A00BE653E /* video_links.plist */; }; B64ED2012B8C933B00BE653E /* VideoSocialOverlaysController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1932B8C933A00BE653E /* VideoSocialOverlaysController.swift */; }; B64ED2022B8C933B00BE653E /* VideoFeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1942B8C933A00BE653E /* VideoFeedViewController.swift */; }; B64ED2032B8C933B00BE653E /* MainPageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1952B8C933A00BE653E /* MainPageController.swift */; }; B64ED2042B8C933B00BE653E /* VideoFeedContainerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1962B8C933A00BE653E /* VideoFeedContainerController.swift */; }; B64ED2052B8C933B00BE653E /* VideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1972B8C933A00BE653E /* VideoViewController.swift */; }; B64ED2062B8C933B00BE653E /* VideoFeedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1982B8C933A00BE653E /* VideoFeedController.swift */; }; B64ED2072B8C933B00BE653E /* VideoHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED19A2B8C933A00BE653E /* VideoHelper.swift */; }; B64ED2082B8C933B00BE653E /* SplashViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED19B2B8C933A00BE653E /* SplashViewController.swift */; }; B64ED2092B8C933B00BE653E /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED19D2B8C933A00BE653E /* BaseViewController.swift */; }; B64ED20A2B8C933B00BE653E /* WidgetsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED19F2B8C933A00BE653E /* WidgetsViewController.swift */; }; B64ED20B2B8C933B00BE653E /* ImageWidgetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1A02B8C933A00BE653E /* ImageWidgetViewController.swift */; }; B64ED20C2B8C933B00BE653E /* UploadWidgetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1A12B8C933A00BE653E /* UploadWidgetViewController.swift */; }; B64ED20E2B8C933B00BE653E /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1A52B8C933A00BE653E /* Toolbar.swift */; }; B64ED20F2B8C933B00BE653E /* Toolbar.xib in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1A62B8C933A00BE653E /* Toolbar.xib */; }; B64ED2102B8C933B00BE653E /* ToolbarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1A82B8C933A00BE653E /* ToolbarItem.swift */; }; B64ED2112B8C933B00BE653E /* ToolbarItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1A92B8C933A00BE653E /* ToolbarItem.xib */; }; B64ED2122B8C933B00BE653E /* RevealImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1AA2B8C933A00BE653E /* RevealImageView.swift */; }; B64ED2132B8C933B00BE653E /* UploadLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1AC2B8C933A00BE653E /* UploadLoadingView.swift */; }; B64ED2142B8C933B00BE653E /* UploadLoadingView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1AD2B8C933A00BE653E /* UploadLoadingView.xib */; }; B64ED2152B8C933B00BE653E /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1AE2B8C933A00BE653E /* GradientView.swift */; }; B64ED2162B8C933B00BE653E /* VideoFeed.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1B22B8C933A00BE653E /* VideoFeed.storyboard */; }; B64ED2172B8C933B00BE653E /* VideoSocialOverlays.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1B42B8C933A00BE653E /* VideoSocialOverlays.storyboard */; }; B64ED2182B8C933B00BE653E /* Video.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1B52B8C933A00BE653E /* Video.storyboard */; }; B64ED2192B8C933B00BE653E /* Delivery.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1B72B8C933A00BE653E /* Delivery.storyboard */; }; B64ED21A2B8C933B00BE653E /* Optimization.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1B82B8C933A00BE653E /* Optimization.storyboard */; }; B64ED21B2B8C933B00BE653E /* UseCases.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1B92B8C933A00BE653E /* UseCases.storyboard */; }; B64ED21C2B8C933B00BE653E /* NormalizingProductAssets.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1BB2B8C933A00BE653E /* NormalizingProductAssets.storyboard */; }; B64ED21D2B8C933B00BE653E /* ConditionalProduct.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1BC2B8C933A00BE653E /* ConditionalProduct.storyboard */; }; B64ED21E2B8C933B00BE653E /* Base.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1BE2B8C933A00BE653E /* Base.storyboard */; }; B64ED21F2B8C933B00BE653E /* ImageWidget.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1C02B8C933A00BE653E /* ImageWidget.storyboard */; }; B64ED2212B8C933B00BE653E /* Widgets.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1C22B8C933A00BE653E /* Widgets.storyboard */; }; B64ED2222B8C933B00BE653E /* InnerUploadFrame.xib in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1C42B8C933A00BE653E /* InnerUploadFrame.xib */; }; B64ED2232B8C933B00BE653E /* Upload.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1C52B8C933A00BE653E /* Upload.storyboard */; }; B64ED2242B8C933B00BE653E /* UploadNoCloud.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1C62B8C933A00BE653E /* UploadNoCloud.storyboard */; }; B64ED2252B8C933B00BE653E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1C72B8C933A00BE653E /* LaunchScreen.storyboard */; }; B64ED2262B8C933B00BE653E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1C92B8C933A00BE653E /* Main.storyboard */; }; B64ED2272B8C933B00BE653E /* Splash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1CB2B8C933A00BE653E /* Splash.storyboard */; }; B64ED2282B8C933B00BE653E /* SmartCropping.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1CD2B8C933A00BE653E /* SmartCropping.storyboard */; }; B64ED2292B8C933B00BE653E /* RevealImage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1CE2B8C933A00BE653E /* RevealImage.storyboard */; }; B64ED22A2B8C933B00BE653E /* Transform.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1CF2B8C933A00BE653E /* Transform.storyboard */; }; B64ED22B2B8C933B00BE653E /* UploadDoesNotExist.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1D12B8C933A00BE653E /* UploadDoesNotExist.storyboard */; }; B64ED22C2B8C933B00BE653E /* SingleUpload.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1D22B8C933A00BE653E /* SingleUpload.storyboard */; }; B64ED22D2B8C933B00BE653E /* UploadChoice.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64ED1D32B8C933A00BE653E /* UploadChoice.storyboard */; }; B64ED22E2B8C933B00BE653E /* CloudinaryHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1D52B8C933A00BE653E /* CloudinaryHelper.swift */; }; B64ED22F2B8C933B00BE653E /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1D62B8C933A00BE653E /* FileUtils.swift */; }; B64ED2302B8C933B00BE653E /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1D82B8C933B00BE653E /* Double+Extension.swift */; }; B64ED2312B8C933B00BE653E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1D92B8C933B00BE653E /* SceneDelegate.swift */; }; B64ED2322B8C933B00BE653E /* ImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1DB2B8C933B00BE653E /* ImageHelper.swift */; }; B64ED2332B8C933B00BE653E /* AnimationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1DC2B8C933B00BE653E /* AnimationHelper.swift */; }; B64ED2342B8C933B00BE653E /* EventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1DE2B8C933B00BE653E /* EventsHandler.swift */; }; B64ED2352B8C933B00BE653E /* FirebaseEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1DF2B8C933B00BE653E /* FirebaseEventsHandler.swift */; }; B64ED2362B8C933B00BE653E /* EventObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1E02B8C933B00BE653E /* EventObject.swift */; }; B64ED2372B8C933B00BE653E /* EventsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64ED1E12B8C933B00BE653E /* EventsProvider.swift */; }; B64F0DDD2BC6541000B94590 /* UploadWidget.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64F0DDC2BC6541000B94590 /* UploadWidget.storyboard */; }; B67476152B8CA0D2006ED6C2 /* IntegrateAIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67476142B8CA0D2006ED6C2 /* IntegrateAIViewController.swift */; }; B67476172B8CA0EF006ED6C2 /* IntegrateAI.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B67476162B8CA0EF006ED6C2 /* IntegrateAI.storyboard */; }; B694AAF32B308C3C00075041 /* VideoEventsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B694AAF22B308C3C00075041 /* VideoEventsTests.swift */; }; B694AAF52B308C5A00075041 /* VideoEventsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B694AAF42B308C5A00075041 /* VideoEventsManagerTests.swift */; }; B6AF51632D4A2A430037965A /* NetworkTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AF51622D4A2A430037965A /* NetworkTestUtils.swift */; }; B6B4C49F2C56152700C9B604 /* VideoPreprocessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4C49E2C56152700C9B604 /* VideoPreprocessTests.swift */; }; B6C2D4882A72741B00AA0039 /* CLDVideoPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C2D4872A72741B00AA0039 /* CLDVideoPlayerTests.swift */; }; B6F11F3D288FC20900A895CD /* CLDAnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F11F3C288FC20900A895CD /* CLDAnalyticsTests.swift */; }; D7119CE3246C7C8100F6B3ED /* CLDConditionExpressionHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7119CE2246C7C8100F6B3ED /* CLDConditionExpressionHelpersTests.swift */; }; D7173498258296CE006F34CD /* WidgetUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7173497258296CE006F34CD /* WidgetUITests.swift */; }; D71ADD9C247D976500235AD4 /* CryptoUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D71ADD9B247D976500235AD4 /* CryptoUtilsTests.m */; }; D71B78FE24680743004AA28E /* CLDTransformationVariablesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71B78FD24680743004AA28E /* CLDTransformationVariablesTests.swift */; }; D71B790024680773004AA28E /* CLDTransformationExpressionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71B78FF24680773004AA28E /* CLDTransformationExpressionsTests.swift */; }; D71B7902246808EE004AA28E /* CLDTransformationConditionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71B7901246808EE004AA28E /* CLDTransformationConditionsTests.swift */; }; D7289C7F258A9CB5004DBD29 /* dog2.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = D7289C7E258A9CB5004DBD29 /* dog2.mp4 */; }; D7289C81258A9D0B004DBD29 /* UploaderWidgetAssetContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7289C80258A9D0B004DBD29 /* UploaderWidgetAssetContainerTests.swift */; }; D7289C87258A9E18004DBD29 /* UploaderWidgetVideoControlsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7289C83258A9E18004DBD29 /* UploaderWidgetVideoControlsTests.swift */; }; D7289C88258A9E18004DBD29 /* UploaderWidgetVideoPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7289C84258A9E18004DBD29 /* UploaderWidgetVideoPlayerTests.swift */; }; D7289C89258A9E18004DBD29 /* UploaderWidgetVideoDisplayLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7289C85258A9E18004DBD29 /* UploaderWidgetVideoDisplayLinkTests.swift */; }; D7289C8A258A9E18004DBD29 /* UploaderWidgetVideoViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7289C86258A9E18004DBD29 /* UploaderWidgetVideoViewTests.swift */; }; D73117582473D7A30051AAFC /* CLDExpressionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D73117572473D7A30051AAFC /* CLDExpressionTests.m */; }; D731175A247415340051AAFC /* CLDConditionExpressionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D7311759247415340051AAFC /* CLDConditionExpressionTests.m */; }; D731175C24741FBE0051AAFC /* CLDTransformationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D731175B24741FBE0051AAFC /* CLDTransformationTests.m */; }; D73DBB0A258BA0550002E7E5 /* dog2.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = D7289C7E258A9CB5004DBD29 /* dog2.mp4 */; }; D73DBB0B258BA0570002E7E5 /* dog.mov in Resources */ = {isa = PBXBuildFile; fileRef = 274C6D2B23FBAF5E0090BC40 /* dog.mov */; }; D73DBB0C258BA05A0002E7E5 /* dog.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 274C6D2C23FBAF5E0090BC40 /* dog.mp4 */; }; D7519FAC25E2693A006839B1 /* CLDCloudinaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7519FAA25E2693A006839B1 /* CLDCloudinaryTests.swift */; }; D7519FAD25E2693A006839B1 /* CLDCloudinaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D7519FAB25E2693A006839B1 /* CLDCloudinaryTests.m */; }; D7519FEC25E26BE9006839B1 /* DownloaderAssetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7519FEB25E26BE9006839B1 /* DownloaderAssetTests.swift */; }; D7567064255D661F005B65D3 /* UploaderWidgetViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7567063255D661F005B65D3 /* UploaderWidgetViewControllerTests.swift */; }; D7788A3824DC482C00B63EB7 /* borderCollieCropped.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D7788A3724DC482C00B63EB7 /* borderCollieCropped.jpg */; }; D7788A9424E29BEC00B63EB7 /* borderCollieRotatedJpg.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D7788A9224E29BEC00B63EB7 /* borderCollieRotatedJpg.jpg */; }; D7788A9524E29BEC00B63EB7 /* borderCollieRotatedPng.png in Resources */ = {isa = PBXBuildFile; fileRef = D7788A9324E29BEC00B63EB7 /* borderCollieRotatedPng.png */; }; D779B69724B7117300E496EB /* UploaderAccessibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D779B69424B7117200E496EB /* UploaderAccessibilityTests.swift */; }; D779B69824B7117300E496EB /* UploaderAccessibilityTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D779B69524B7117200E496EB /* UploaderAccessibilityTests.m */; }; D7866141248FE34A0099DE8C /* textImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D7866140248FE34A0099DE8C /* textImage.jpg */; }; D79AE385244CC1D7004F2439 /* CLDTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79AE384244CC1D7004F2439 /* CLDTransformationTests.swift */; }; D79AE387244DF1E7004F2439 /* CLDVariableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79AE386244DF1E7004F2439 /* CLDVariableTests.swift */; }; D79AE39C24585533004F2439 /* CLDVariableTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D79AE39B24585533004F2439 /* CLDVariableTests.m */; }; D7E79DC924B515260082288A /* UploaderOcrTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E79DC324B515260082288A /* UploaderOcrTests.swift */; }; D7E79DCA24B515260082288A /* OcrMockProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E79DC424B515260082288A /* OcrMockProvider.swift */; }; D7E79DCB24B515260082288A /* ExplicitMockOcrTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D7E79DC524B515260082288A /* ExplicitMockOcrTests.m */; }; D7E79DCC24B515260082288A /* UploaderMockOcrTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D7E79DC624B515260082288A /* UploaderMockOcrTests.m */; }; D7E79DCD24B515260082288A /* UploaderMockOcrTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E79DC724B515260082288A /* UploaderMockOcrTests.swift */; }; D7E79DCE24B515260082288A /* ExplicitMockOcrTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E79DC824B515260082288A /* ExplicitMockOcrTests.swift */; }; D7E79DD124B515650082288A /* UploadRequestParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E79DD024B515650082288A /* UploadRequestParamsTests.swift */; }; D7EC161F2592261200DD7611 /* borderCollieRotatedJpgUnderIOS12.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D7EC161E2592261200DD7611 /* borderCollieRotatedJpgUnderIOS12.jpg */; }; D7EC162125922BA200DD7611 /* borderCollieRotatedPngUnderIOS12.png in Resources */ = {isa = PBXBuildFile; fileRef = D7EC162025922BA200DD7611 /* borderCollieRotatedPngUnderIOS12.png */; }; D7EF05AB255D8B8E00F93827 /* NetworkBaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EF05A8255D8B8E00F93827 /* NetworkBaseTest.swift */; }; D7EF05AC255D8B8E00F93827 /* NetworkBaseTestObjc.m in Sources */ = {isa = PBXBuildFile; fileRef = D7EF05AA255D8B8E00F93827 /* NetworkBaseTestObjc.m */; }; D7EF05B0255D8C1900F93827 /* UploaderWidgetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EF05AE255D8C1900F93827 /* UploaderWidgetTests.swift */; }; D7EF05B1255D8C1900F93827 /* UploaderWidgetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D7EF05AF255D8C1900F93827 /* UploaderWidgetTests.m */; }; D7F34F2F2504F48C00C282B1 /* UploaderWidgetCollectionCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F34F2E2504F48C00C282B1 /* UploaderWidgetCollectionCellTests.swift */; }; D7F34F3125052FBD00C282B1 /* UploaderWidgetPreviewViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F34F3025052FBD00C282B1 /* UploaderWidgetPreviewViewControllerTests.swift */; }; D7F34F3325052FE700C282B1 /* WidgetBaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F34F3225052FE700C282B1 /* WidgetBaseTest.swift */; }; D7F34F9125120E0D00C282B1 /* UploaderWidgetEditViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F34F9025120E0C00C282B1 /* UploaderWidgetEditViewControllerTests.swift */; }; D7F3D46024DC3BAB002C1D27 /* PreprocessUploaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F3D45F24DC3BAB002C1D27 /* PreprocessUploaderTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 607FACC81AFB9204008FA782 /* Project object */; proxyType = 1; remoteGlobalIDString = 607FACCF1AFB9204008FA782; remoteInfo = Cloudinary; }; D717349A258296CE006F34CD /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 607FACC81AFB9204008FA782 /* Project object */; proxyType = 1; remoteGlobalIDString = 607FACCF1AFB9204008FA782; remoteInfo = Cloudinary_Example; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 1E635A3C25A756860003E9D3 /* ObjcBaseTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjcBaseTestCase.h; sourceTree = ""; }; 1E635A3D25A756860003E9D3 /* ObjcBaseTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjcBaseTestCase.m; sourceTree = ""; }; 1E635A6F25A757DA0003E9D3 /* BaseMockProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseMockProvider.swift; sourceTree = ""; }; 1E635A7825A7585E0003E9D3 /* MockProviderQualityAnalysis.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockProviderQualityAnalysis.swift; sourceTree = ""; }; 1E635A7925A7585E0003E9D3 /* ObjcUploaderQualityAnalysisTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjcUploaderQualityAnalysisTests.m; sourceTree = ""; }; 1E635A7A25A7585E0003E9D3 /* QualityAnalysisUploadResultParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QualityAnalysisUploadResultParserTests.swift; sourceTree = ""; }; 1E635A7B25A7585E0003E9D3 /* ObjcQualityAnalysisExplicitResultParserTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjcQualityAnalysisExplicitResultParserTests.m; sourceTree = ""; }; 1E635A7C25A7585E0003E9D3 /* UploaderQualityAnalysisTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderQualityAnalysisTests.swift; sourceTree = ""; }; 1E635A7D25A7585E0003E9D3 /* QualityAnalysisExplicitResultParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QualityAnalysisExplicitResultParserTests.swift; sourceTree = ""; }; 1E635A7E25A7585E0003E9D3 /* ObjcQualityAnalysisUploadResultParserTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjcQualityAnalysisUploadResultParserTests.m; sourceTree = ""; }; 270C1BF623FBBA3C00A503C3 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 272C513E242B6C340093AB1B /* SessionManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionManagerTests.swift; sourceTree = ""; }; 272C513F242B6C340093AB1B /* FileManager+CloudinaryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FileManager+CloudinaryTests.swift"; sourceTree = ""; }; 272C5140242B6C340093AB1B /* AuthenticationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationTests.swift; sourceTree = ""; }; 272C5141242B6C350093AB1B /* RequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = ""; }; 272C5142242B6C350093AB1B /* SessionDelegateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionDelegateTests.swift; sourceTree = ""; }; 272C5144242B6C350093AB1B /* ResponseSerializationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseSerializationTests.swift; sourceTree = ""; }; 272C5145242B6C350093AB1B /* MultipartFormDataTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormDataTests.swift; sourceTree = ""; }; 272C5146242B6C350093AB1B /* URLProtocolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLProtocolTests.swift; sourceTree = ""; }; 272C5148242B6C350093AB1B /* ResponseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseTests.swift; sourceTree = ""; }; 272C514A242B6C350093AB1B /* ValidationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidationTests.swift; sourceTree = ""; }; 272C514B242B6C350093AB1B /* BaseTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTestCase.swift; sourceTree = ""; }; 272C514E242B6C350093AB1B /* unicorn.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = unicorn.png; sourceTree = ""; }; 272C514F242B6C350093AB1B /* rainbow.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = rainbow.jpg; sourceTree = ""; }; 272C5152242B6C350093AB1B /* invalid.data */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = invalid.data; sourceTree = ""; }; 272C5153242B6C350093AB1B /* valid.data */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = valid.data; sourceTree = ""; }; 272C5154242B6C350093AB1B /* empty.data */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = empty.data; sourceTree = ""; }; 272C5156242B6C350093AB1B /* empty_data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = empty_data.json; sourceTree = ""; }; 272C5157242B6C350093AB1B /* valid_data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = valid_data.json; sourceTree = ""; }; 272C5158242B6C350093AB1B /* invalid_data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = invalid_data.json; sourceTree = ""; }; 272C515A242B6C350093AB1B /* utf8_string.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = utf8_string.txt; sourceTree = ""; }; 272C515B242B6C350093AB1B /* utf32_string.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = utf32_string.txt; sourceTree = ""; }; 272C515C242B6C350093AB1B /* empty_string.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = empty_string.txt; sourceTree = ""; }; 272C5179242B6C360093AB1B /* UploadTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadTests.swift; sourceTree = ""; }; 272C517A242B6C360093AB1B /* CacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheTests.swift; sourceTree = ""; }; 272C517C242B6C360093AB1B /* CLDNError+CloudinaryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CLDNError+CloudinaryTests.swift"; sourceTree = ""; }; 272C517D242B6C360093AB1B /* ResultTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultTests.swift; sourceTree = ""; }; 272C517E242B6C360093AB1B /* ParameterEncodingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParameterEncodingTests.swift; sourceTree = ""; }; 274C6D1323FBAF5E0090BC40 /* CryptoUtilsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoUtilsTests.swift; sourceTree = ""; }; 274C6D1423FBAF5E0090BC40 /* PreprocessTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreprocessTests.swift; sourceTree = ""; }; 274C6D1523FBAF5E0090BC40 /* FileUtilsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtilsTests.swift; sourceTree = ""; }; 274C6D1723FBAF5E0090BC40 /* UrlTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UrlTests.m; sourceTree = ""; }; 274C6D1823FBAF5E0090BC40 /* UrlTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UrlTests.swift; sourceTree = ""; }; 274C6D1A23FBAF5E0090BC40 /* UIBaseTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIBaseTest.swift; sourceTree = ""; }; 274C6D1B23FBAF5E0090BC40 /* UIButtonTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIButtonTests.swift; sourceTree = ""; }; 274C6D1C23FBAF5E0090BC40 /* UIImageViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageViewTests.swift; sourceTree = ""; }; 274C6D1F23FBAF5E0090BC40 /* UploaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderTests.swift; sourceTree = ""; }; 274C6D2023FBAF5E0090BC40 /* ManagementApiTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagementApiTests.swift; sourceTree = ""; }; 274C6D2123FBAF5E0090BC40 /* DownloaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloaderTests.swift; sourceTree = ""; }; 274C6D2223FBAF5E0090BC40 /* StringUtilsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringUtilsTest.swift; sourceTree = ""; }; 274C6D2523FBAF5E0090BC40 /* borderCollie.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = borderCollie.jpg; sourceTree = ""; }; 274C6D2623FBAF5E0090BC40 /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo.png; sourceTree = ""; }; 274C6D2823FBAF5E0090BC40 /* docx.docx */ = {isa = PBXFileReference; lastKnownFileType = file; path = docx.docx; sourceTree = ""; }; 274C6D2923FBAF5E0090BC40 /* pdf.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = pdf.pdf; sourceTree = ""; }; 274C6D2B23FBAF5E0090BC40 /* dog.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = dog.mov; sourceTree = ""; }; 274C6D2C23FBAF5E0090BC40 /* dog.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = dog.mp4; sourceTree = ""; }; 27BC1FAD2431C3F6000AFC2C /* CLDNDataResponse+CloudinaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLDNDataResponse+CloudinaryTests.swift"; sourceTree = ""; }; 27BC1FAF2431C48D000AFC2C /* CLDNResult+CloudinaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLDNResult+CloudinaryTests.swift"; sourceTree = ""; }; 2F5D96AD5982EFEBDEE91350 /* Pods-Cloudinary_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Cloudinary_Tests.debug.xcconfig"; path = "Target Support Files/Pods-Cloudinary_Tests/Pods-Cloudinary_Tests.debug.xcconfig"; sourceTree = ""; }; 5D4D8C2B246098BA00AE9C96 /* CLDConditionExpressionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLDConditionExpressionTests.swift; sourceTree = ""; }; 5D53A94F2488CE23005C14AB /* CLDConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLDConfigurationTests.swift; sourceTree = ""; }; 5D53A9502488CE23005C14AB /* CLDConfigurationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CLDConfigurationTests.m; sourceTree = ""; }; 5D7C2DCE245E93F7007E95F7 /* CLDExpressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDExpressionTests.swift; sourceTree = ""; }; 5DB2D29624FFE32500001845 /* UploaderWidgetConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderWidgetConfigurationTests.swift; sourceTree = ""; }; 5DB2D29724FFE32500001845 /* UploaderWidgetConfigurationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UploaderWidgetConfigurationTests.m; sourceTree = ""; }; 5DE4EC1E2469919700F6C8D6 /* CLDTransformationBaselineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLDTransformationBaselineTests.swift; sourceTree = ""; }; 607FACD01AFB9204008FA782 /* Cloudinary_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Cloudinary_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 607FACE51AFB9204008FA782 /* Cloudinary_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Cloudinary_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7EAA36BF6CA4E5748DFCD320 /* Pods_Cloudinary_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Cloudinary_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AA2F53C389142220209152BA /* Cloudinary.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; name = Cloudinary.podspec; path = ../Cloudinary.podspec; sourceTree = ""; }; B60F3C212D4B86AF00160FFC /* TestableCloudinary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableCloudinary.swift; sourceTree = ""; }; B60F3C232D4B86E200160FFC /* Cloudinary.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Cloudinary.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B64120BC2C48ED3F005A5495 /* SingleUploadCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleUploadCell.swift; sourceTree = ""; }; B64120BE2C48ED69005A5495 /* SingleUploadCollectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleUploadCollectionController.swift; sourceTree = ""; }; B64120C02C48ED83005A5495 /* SingleUploadCollectionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleUploadCollectionLayout.swift; sourceTree = ""; }; B64120C32C48EE76005A5495 /* AssetItems+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetItems+CoreDataClass.swift"; sourceTree = ""; }; B64120C52C48EEC7005A5495 /* AssetItems+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetItems+CoreDataProperties.swift"; sourceTree = ""; }; B64120C72C48EED7005A5495 /* AssetModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetModel.swift; sourceTree = ""; }; B64120CA2C48EF10005A5495 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; B64120CC2C48EF22005A5495 /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = ""; }; B64120CE2C48F049005A5495 /* SingleUploadPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleUploadPreview.swift; sourceTree = ""; }; B64120D02C48F0EC005A5495 /* SingleUploadPreview.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = SingleUploadPreview.storyboard; sourceTree = ""; }; B64ED1622B8C933900BE653E /* Cloudinary_Example-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Cloudinary_Example-Bridging-Header.h"; sourceTree = ""; }; B64ED1632B8C933900BE653E /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; B64ED1642B8C933900BE653E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; B64ED1672B8C933A00BE653E /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; B64ED1682B8C933A00BE653E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B64ED16C2B8C933A00BE653E /* VideoFeedCollectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoFeedCollectionController.swift; sourceTree = ""; }; B64ED16E2B8C933A00BE653E /* VideoFeedCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoFeedCell.swift; sourceTree = ""; }; B64ED1712B8C933A00BE653E /* OptimizationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizationViewController.swift; sourceTree = ""; }; B64ED1732B8C933A00BE653E /* TransformCollectionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformCollectionCell.swift; sourceTree = ""; }; B64ED1742B8C933A00BE653E /* TransformViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformViewController.swift; sourceTree = ""; }; B64ED1752B8C933A00BE653E /* TransformationCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformationCell.swift; sourceTree = ""; }; B64ED1772B8C933A00BE653E /* RevealImageController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevealImageController.swift; sourceTree = ""; }; B64ED1782B8C933A00BE653E /* DeliveryTransformCollectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeliveryTransformCollectionController.swift; sourceTree = ""; }; B64ED1792B8C933A00BE653E /* TransformCollectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformCollectionController.swift; sourceTree = ""; }; B64ED17B2B8C933A00BE653E /* SmartCroppingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartCroppingController.swift; sourceTree = ""; }; B64ED17D2B8C933A00BE653E /* UseCaseCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UseCaseCell.swift; sourceTree = ""; }; B64ED17E2B8C933A00BE653E /* UseCaseCollectionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UseCaseCollectionCell.swift; sourceTree = ""; }; B64ED1802B8C933A00BE653E /* ConditionalProductViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionalProductViewController.swift; sourceTree = ""; }; B64ED1812B8C933A00BE653E /* NormalizingProductAssetsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalizingProductAssetsViewController.swift; sourceTree = ""; }; B64ED1822B8C933A00BE653E /* UseCasesCollectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UseCasesCollectionController.swift; sourceTree = ""; }; B64ED1832B8C933A00BE653E /* DeliveryUseCasesCollectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeliveryUseCasesCollectionController.swift; sourceTree = ""; }; B64ED1842B8C933A00BE653E /* UseCasesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UseCasesViewController.swift; sourceTree = ""; }; B64ED1852B8C933A00BE653E /* DeliveryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeliveryViewController.swift; sourceTree = ""; }; B64ED1872B8C933A00BE653E /* UploadChoiceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadChoiceController.swift; sourceTree = ""; }; B64ED1882B8C933A00BE653E /* InnerUploadFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InnerUploadFrame.swift; sourceTree = ""; }; B64ED1892B8C933A00BE653E /* SingleUploadViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleUploadViewController.swift; sourceTree = ""; }; B64ED18A2B8C933A00BE653E /* UploadViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadViewController.swift; sourceTree = ""; }; B64ED18B2B8C933A00BE653E /* UploadNoCloudController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadNoCloudController.swift; sourceTree = ""; }; B64ED18D2B8C933A00BE653E /* UploadDoesNotExistController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadDoesNotExistController.swift; sourceTree = ""; }; B64ED18E2B8C933A00BE653E /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; B64ED1912B8C933A00BE653E /* video_links.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = video_links.plist; sourceTree = ""; }; B64ED1932B8C933A00BE653E /* VideoSocialOverlaysController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoSocialOverlaysController.swift; sourceTree = ""; }; B64ED1942B8C933A00BE653E /* VideoFeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoFeedViewController.swift; sourceTree = ""; }; B64ED1952B8C933A00BE653E /* MainPageController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainPageController.swift; sourceTree = ""; }; B64ED1962B8C933A00BE653E /* VideoFeedContainerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoFeedContainerController.swift; sourceTree = ""; }; B64ED1972B8C933A00BE653E /* VideoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoViewController.swift; sourceTree = ""; }; B64ED1982B8C933A00BE653E /* VideoFeedController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoFeedController.swift; sourceTree = ""; }; B64ED19A2B8C933A00BE653E /* VideoHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoHelper.swift; sourceTree = ""; }; B64ED19B2B8C933A00BE653E /* SplashViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashViewController.swift; sourceTree = ""; }; B64ED19D2B8C933A00BE653E /* BaseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; B64ED19F2B8C933A00BE653E /* WidgetsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetsViewController.swift; sourceTree = ""; }; B64ED1A02B8C933A00BE653E /* ImageWidgetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageWidgetViewController.swift; sourceTree = ""; }; B64ED1A12B8C933A00BE653E /* UploadWidgetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadWidgetViewController.swift; sourceTree = ""; }; B64ED1A52B8C933A00BE653E /* Toolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = ""; }; B64ED1A62B8C933A00BE653E /* Toolbar.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = Toolbar.xib; sourceTree = ""; }; B64ED1A82B8C933A00BE653E /* ToolbarItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolbarItem.swift; sourceTree = ""; }; B64ED1A92B8C933A00BE653E /* ToolbarItem.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ToolbarItem.xib; sourceTree = ""; }; B64ED1AA2B8C933A00BE653E /* RevealImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevealImageView.swift; sourceTree = ""; }; B64ED1AC2B8C933A00BE653E /* UploadLoadingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadLoadingView.swift; sourceTree = ""; }; B64ED1AD2B8C933A00BE653E /* UploadLoadingView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = UploadLoadingView.xib; sourceTree = ""; }; B64ED1AE2B8C933A00BE653E /* GradientView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; B64ED1B22B8C933A00BE653E /* VideoFeed.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = VideoFeed.storyboard; sourceTree = ""; }; B64ED1B42B8C933A00BE653E /* VideoSocialOverlays.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = VideoSocialOverlays.storyboard; sourceTree = ""; }; B64ED1B52B8C933A00BE653E /* Video.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Video.storyboard; sourceTree = ""; }; B64ED1B72B8C933A00BE653E /* Delivery.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Delivery.storyboard; sourceTree = ""; }; B64ED1B82B8C933A00BE653E /* Optimization.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Optimization.storyboard; sourceTree = ""; }; B64ED1B92B8C933A00BE653E /* UseCases.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = UseCases.storyboard; sourceTree = ""; }; B64ED1BB2B8C933A00BE653E /* NormalizingProductAssets.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = NormalizingProductAssets.storyboard; sourceTree = ""; }; B64ED1BC2B8C933A00BE653E /* ConditionalProduct.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ConditionalProduct.storyboard; sourceTree = ""; }; B64ED1BE2B8C933A00BE653E /* Base.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Base.storyboard; sourceTree = ""; }; B64ED1C02B8C933A00BE653E /* ImageWidget.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ImageWidget.storyboard; sourceTree = ""; }; B64ED1C22B8C933A00BE653E /* Widgets.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Widgets.storyboard; sourceTree = ""; }; B64ED1C42B8C933A00BE653E /* InnerUploadFrame.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = InnerUploadFrame.xib; sourceTree = ""; }; B64ED1C52B8C933A00BE653E /* Upload.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Upload.storyboard; sourceTree = ""; }; B64ED1C62B8C933A00BE653E /* UploadNoCloud.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = UploadNoCloud.storyboard; sourceTree = ""; }; B64ED1C82B8C933A00BE653E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; B64ED1CA2B8C933A00BE653E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; B64ED1CB2B8C933A00BE653E /* Splash.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Splash.storyboard; sourceTree = ""; }; B64ED1CD2B8C933A00BE653E /* SmartCropping.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SmartCropping.storyboard; sourceTree = ""; }; B64ED1CE2B8C933A00BE653E /* RevealImage.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = RevealImage.storyboard; sourceTree = ""; }; B64ED1CF2B8C933A00BE653E /* Transform.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Transform.storyboard; sourceTree = ""; }; B64ED1D12B8C933A00BE653E /* UploadDoesNotExist.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = UploadDoesNotExist.storyboard; sourceTree = ""; }; B64ED1D22B8C933A00BE653E /* SingleUpload.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SingleUpload.storyboard; sourceTree = ""; }; B64ED1D32B8C933A00BE653E /* UploadChoice.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = UploadChoice.storyboard; sourceTree = ""; }; B64ED1D52B8C933A00BE653E /* CloudinaryHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudinaryHelper.swift; sourceTree = ""; }; B64ED1D62B8C933A00BE653E /* FileUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; B64ED1D82B8C933B00BE653E /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; B64ED1D92B8C933B00BE653E /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; B64ED1DB2B8C933B00BE653E /* ImageHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageHelper.swift; sourceTree = ""; }; B64ED1DC2B8C933B00BE653E /* AnimationHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationHelper.swift; sourceTree = ""; }; B64ED1DE2B8C933B00BE653E /* EventsHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventsHandler.swift; sourceTree = ""; }; B64ED1DF2B8C933B00BE653E /* FirebaseEventsHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirebaseEventsHandler.swift; sourceTree = ""; }; B64ED1E02B8C933B00BE653E /* EventObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventObject.swift; sourceTree = ""; }; B64ED1E12B8C933B00BE653E /* EventsProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventsProvider.swift; sourceTree = ""; }; B64F0DDA2BC6506700B94590 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B64F0DDC2BC6541000B94590 /* UploadWidget.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = UploadWidget.storyboard; sourceTree = ""; }; B67476142B8CA0D2006ED6C2 /* IntegrateAIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrateAIViewController.swift; sourceTree = ""; }; B67476162B8CA0EF006ED6C2 /* IntegrateAI.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = IntegrateAI.storyboard; sourceTree = ""; }; B694AAF22B308C3C00075041 /* VideoEventsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoEventsTests.swift; sourceTree = ""; }; B694AAF42B308C5A00075041 /* VideoEventsManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoEventsManagerTests.swift; sourceTree = ""; }; B6AF51622D4A2A430037965A /* NetworkTestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkTestUtils.swift; sourceTree = ""; }; B6B4C49E2C56152700C9B604 /* VideoPreprocessTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPreprocessTests.swift; sourceTree = ""; }; B6C2D4872A72741B00AA0039 /* CLDVideoPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVideoPlayerTests.swift; sourceTree = ""; }; B6F11F3C288FC20900A895CD /* CLDAnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDAnalyticsTests.swift; sourceTree = ""; }; B6FFA5C42D4B86020002EFA5 /* Cloudinary.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Cloudinary.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D07C87FEDB1F561D123D6873 /* Pods_Cloudinary_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Cloudinary_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D7119CE2246C7C8100F6B3ED /* CLDConditionExpressionHelpersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDConditionExpressionHelpersTests.swift; sourceTree = ""; }; D7173495258296CE006F34CD /* Cloudinary_UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Cloudinary_UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D7173497258296CE006F34CD /* WidgetUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetUITests.swift; sourceTree = ""; }; D7173499258296CE006F34CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D71ADD9B247D976500235AD4 /* CryptoUtilsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CryptoUtilsTests.m; sourceTree = ""; }; D71B78FD24680743004AA28E /* CLDTransformationVariablesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDTransformationVariablesTests.swift; sourceTree = ""; }; D71B78FF24680773004AA28E /* CLDTransformationExpressionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDTransformationExpressionsTests.swift; sourceTree = ""; }; D71B7901246808EE004AA28E /* CLDTransformationConditionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDTransformationConditionsTests.swift; sourceTree = ""; }; D7289C7E258A9CB5004DBD29 /* dog2.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = dog2.mp4; sourceTree = ""; }; D7289C80258A9D0B004DBD29 /* UploaderWidgetAssetContainerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderWidgetAssetContainerTests.swift; sourceTree = ""; }; D7289C83258A9E18004DBD29 /* UploaderWidgetVideoControlsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderWidgetVideoControlsTests.swift; sourceTree = ""; }; D7289C84258A9E18004DBD29 /* UploaderWidgetVideoPlayerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderWidgetVideoPlayerTests.swift; sourceTree = ""; }; D7289C85258A9E18004DBD29 /* UploaderWidgetVideoDisplayLinkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderWidgetVideoDisplayLinkTests.swift; sourceTree = ""; }; D7289C86258A9E18004DBD29 /* UploaderWidgetVideoViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderWidgetVideoViewTests.swift; sourceTree = ""; }; D73117572473D7A30051AAFC /* CLDExpressionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CLDExpressionTests.m; sourceTree = ""; }; D7311759247415340051AAFC /* CLDConditionExpressionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CLDConditionExpressionTests.m; sourceTree = ""; }; D731175B24741FBE0051AAFC /* CLDTransformationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CLDTransformationTests.m; sourceTree = ""; }; D7519FAA25E2693A006839B1 /* CLDCloudinaryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLDCloudinaryTests.swift; sourceTree = ""; }; D7519FAB25E2693A006839B1 /* CLDCloudinaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CLDCloudinaryTests.m; sourceTree = ""; }; D7519FEB25E26BE9006839B1 /* DownloaderAssetTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloaderAssetTests.swift; sourceTree = ""; }; D7567063255D661F005B65D3 /* UploaderWidgetViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderWidgetViewControllerTests.swift; sourceTree = ""; }; D7788A3724DC482C00B63EB7 /* borderCollieCropped.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = borderCollieCropped.jpg; sourceTree = ""; }; D7788A9224E29BEC00B63EB7 /* borderCollieRotatedJpg.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = borderCollieRotatedJpg.jpg; sourceTree = ""; }; D7788A9324E29BEC00B63EB7 /* borderCollieRotatedPng.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = borderCollieRotatedPng.png; sourceTree = ""; }; D779B69424B7117200E496EB /* UploaderAccessibilityTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderAccessibilityTests.swift; sourceTree = ""; }; D779B69524B7117200E496EB /* UploaderAccessibilityTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UploaderAccessibilityTests.m; sourceTree = ""; }; D7866140248FE34A0099DE8C /* textImage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = textImage.jpg; sourceTree = ""; }; D79AE384244CC1D7004F2439 /* CLDTransformationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDTransformationTests.swift; sourceTree = ""; }; D79AE386244DF1E7004F2439 /* CLDVariableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLDVariableTests.swift; sourceTree = ""; }; D79AE39B24585533004F2439 /* CLDVariableTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CLDVariableTests.m; sourceTree = ""; }; D7E79DC324B515260082288A /* UploaderOcrTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderOcrTests.swift; sourceTree = ""; }; D7E79DC424B515260082288A /* OcrMockProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OcrMockProvider.swift; sourceTree = ""; }; D7E79DC524B515260082288A /* ExplicitMockOcrTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExplicitMockOcrTests.m; sourceTree = ""; }; D7E79DC624B515260082288A /* UploaderMockOcrTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UploaderMockOcrTests.m; sourceTree = ""; }; D7E79DC724B515260082288A /* UploaderMockOcrTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderMockOcrTests.swift; sourceTree = ""; }; D7E79DC824B515260082288A /* ExplicitMockOcrTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExplicitMockOcrTests.swift; sourceTree = ""; }; D7E79DD024B515650082288A /* UploadRequestParamsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadRequestParamsTests.swift; sourceTree = ""; }; D7EC161E2592261200DD7611 /* borderCollieRotatedJpgUnderIOS12.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = borderCollieRotatedJpgUnderIOS12.jpg; sourceTree = ""; }; D7EC162025922BA200DD7611 /* borderCollieRotatedPngUnderIOS12.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = borderCollieRotatedPngUnderIOS12.png; sourceTree = ""; }; D7EF05A8255D8B8E00F93827 /* NetworkBaseTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkBaseTest.swift; sourceTree = ""; }; D7EF05A9255D8B8E00F93827 /* NetworkBaseTestObjc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NetworkBaseTestObjc.h; sourceTree = ""; }; D7EF05AA255D8B8E00F93827 /* NetworkBaseTestObjc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NetworkBaseTestObjc.m; sourceTree = ""; }; D7EF05AE255D8C1900F93827 /* UploaderWidgetTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderWidgetTests.swift; sourceTree = ""; }; D7EF05AF255D8C1900F93827 /* UploaderWidgetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UploaderWidgetTests.m; sourceTree = ""; }; D7F34F2E2504F48C00C282B1 /* UploaderWidgetCollectionCellTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderWidgetCollectionCellTests.swift; sourceTree = ""; }; D7F34F3025052FBD00C282B1 /* UploaderWidgetPreviewViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderWidgetPreviewViewControllerTests.swift; sourceTree = ""; }; D7F34F3225052FE700C282B1 /* WidgetBaseTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetBaseTest.swift; sourceTree = ""; }; D7F34F9025120E0C00C282B1 /* UploaderWidgetEditViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploaderWidgetEditViewControllerTests.swift; sourceTree = ""; }; D7F3D45F24DC3BAB002C1D27 /* PreprocessUploaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreprocessUploaderTests.swift; sourceTree = ""; }; DE971235B5CF94F0DA4D461B /* Pods-Cloudinary_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Cloudinary_Example.debug.xcconfig"; path = "Target Support Files/Pods-Cloudinary_Example/Pods-Cloudinary_Example.debug.xcconfig"; sourceTree = ""; }; E5BA1162DEC8D6E579692502 /* Pods-Cloudinary_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Cloudinary_Example.release.xcconfig"; path = "Target Support Files/Pods-Cloudinary_Example/Pods-Cloudinary_Example.release.xcconfig"; sourceTree = ""; }; F2530AD793711AEF6C37A33F /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; FB8B4986C00FEC3B8BD40223 /* Pods-Cloudinary_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Cloudinary_Tests.release.xcconfig"; path = "Target Support Files/Pods-Cloudinary_Tests/Pods-Cloudinary_Tests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 607FACCD1AFB9204008FA782 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 9753E077581D46FC1463D265 /* Pods_Cloudinary_Example.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 607FACE21AFB9204008FA782 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 5B1EF16C5EA94AD09F6C22C2 /* Pods_Cloudinary_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; D7173492258296CE006F34CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 0293C6AFE5E20FD5F3D8E4D4 /* Pods */ = { isa = PBXGroup; children = ( DE971235B5CF94F0DA4D461B /* Pods-Cloudinary_Example.debug.xcconfig */, E5BA1162DEC8D6E579692502 /* Pods-Cloudinary_Example.release.xcconfig */, 2F5D96AD5982EFEBDEE91350 /* Pods-Cloudinary_Tests.debug.xcconfig */, FB8B4986C00FEC3B8BD40223 /* Pods-Cloudinary_Tests.release.xcconfig */, ); path = Pods; sourceTree = ""; }; 1E635A6E25A757DA0003E9D3 /* MockProvider */ = { isa = PBXGroup; children = ( 1E635A6F25A757DA0003E9D3 /* BaseMockProvider.swift */, ); path = MockProvider; sourceTree = ""; }; 1E635A7725A7585E0003E9D3 /* QualityAnalysisUploaderTests */ = { isa = PBXGroup; children = ( 1E635A7825A7585E0003E9D3 /* MockProviderQualityAnalysis.swift */, 1E635A7925A7585E0003E9D3 /* ObjcUploaderQualityAnalysisTests.m */, 1E635A7A25A7585E0003E9D3 /* QualityAnalysisUploadResultParserTests.swift */, 1E635A7B25A7585E0003E9D3 /* ObjcQualityAnalysisExplicitResultParserTests.m */, 1E635A7C25A7585E0003E9D3 /* UploaderQualityAnalysisTests.swift */, 1E635A7D25A7585E0003E9D3 /* QualityAnalysisExplicitResultParserTests.swift */, 1E635A7E25A7585E0003E9D3 /* ObjcQualityAnalysisUploadResultParserTests.m */, ); path = QualityAnalysisUploaderTests; sourceTree = ""; }; 272C513B242B6B6E0093AB1B /* BaseNetwork */ = { isa = PBXGroup; children = ( 1E635A3C25A756860003E9D3 /* ObjcBaseTestCase.h */, 1E635A3D25A756860003E9D3 /* ObjcBaseTestCase.m */, 272C514B242B6C350093AB1B /* BaseTestCase.swift */, 272C51B6242C7F3D0093AB1B /* Core */, 272C51B7242C80720093AB1B /* Extensions */, 272C51B8242C80E70093AB1B /* Features */, 272C514C242B6C350093AB1B /* Resources */, ); path = BaseNetwork; sourceTree = ""; }; 272C514C242B6C350093AB1B /* Resources */ = { isa = PBXGroup; children = ( 272C514D242B6C350093AB1B /* Images */, 272C5150242B6C350093AB1B /* Responses */, ); path = Resources; sourceTree = ""; }; 272C514D242B6C350093AB1B /* Images */ = { isa = PBXGroup; children = ( 272C514E242B6C350093AB1B /* unicorn.png */, 272C514F242B6C350093AB1B /* rainbow.jpg */, ); path = Images; sourceTree = ""; }; 272C5150242B6C350093AB1B /* Responses */ = { isa = PBXGroup; children = ( 272C5151242B6C350093AB1B /* Property List */, 272C5155242B6C350093AB1B /* JSON */, 272C5159242B6C350093AB1B /* String */, ); path = Responses; sourceTree = ""; }; 272C5151242B6C350093AB1B /* Property List */ = { isa = PBXGroup; children = ( 272C5152242B6C350093AB1B /* invalid.data */, 272C5153242B6C350093AB1B /* valid.data */, 272C5154242B6C350093AB1B /* empty.data */, ); path = "Property List"; sourceTree = ""; }; 272C5155242B6C350093AB1B /* JSON */ = { isa = PBXGroup; children = ( 272C5156242B6C350093AB1B /* empty_data.json */, 272C5157242B6C350093AB1B /* valid_data.json */, 272C5158242B6C350093AB1B /* invalid_data.json */, ); path = JSON; sourceTree = ""; }; 272C5159242B6C350093AB1B /* String */ = { isa = PBXGroup; children = ( 272C515A242B6C350093AB1B /* utf8_string.txt */, 272C515B242B6C350093AB1B /* utf32_string.txt */, 272C515C242B6C350093AB1B /* empty_string.txt */, ); path = String; sourceTree = ""; }; 272C51B6242C7F3D0093AB1B /* Core */ = { isa = PBXGroup; children = ( D7519FAB25E2693A006839B1 /* CLDCloudinaryTests.m */, D7519FAA25E2693A006839B1 /* CLDCloudinaryTests.swift */, 272C5140242B6C340093AB1B /* AuthenticationTests.swift */, 272C517E242B6C360093AB1B /* ParameterEncodingTests.swift */, 272C5141242B6C350093AB1B /* RequestTests.swift */, 272C5148242B6C350093AB1B /* ResponseTests.swift */, 272C517D242B6C360093AB1B /* ResultTests.swift */, 272C5142242B6C350093AB1B /* SessionDelegateTests.swift */, 272C513E242B6C340093AB1B /* SessionManagerTests.swift */, 272C5179242B6C360093AB1B /* UploadTests.swift */, ); path = Core; sourceTree = ""; }; 272C51B7242C80720093AB1B /* Extensions */ = { isa = PBXGroup; children = ( 272C517C242B6C360093AB1B /* CLDNError+CloudinaryTests.swift */, 272C513F242B6C340093AB1B /* FileManager+CloudinaryTests.swift */, 27BC1FAD2431C3F6000AFC2C /* CLDNDataResponse+CloudinaryTests.swift */, 27BC1FAF2431C48D000AFC2C /* CLDNResult+CloudinaryTests.swift */, ); path = Extensions; sourceTree = ""; }; 272C51B8242C80E70093AB1B /* Features */ = { isa = PBXGroup; children = ( D751A01225E26D5D006839B1 /* CLDURLCacheTests */, 272C517A242B6C360093AB1B /* CacheTests.swift */, 272C5145242B6C350093AB1B /* MultipartFormDataTests.swift */, 272C5144242B6C350093AB1B /* ResponseSerializationTests.swift */, 272C5146242B6C350093AB1B /* URLProtocolTests.swift */, 272C514A242B6C350093AB1B /* ValidationTests.swift */, ); path = Features; sourceTree = ""; }; 274C6D1623FBAF5E0090BC40 /* GenerateUrlTests */ = { isa = PBXGroup; children = ( 274C6D1723FBAF5E0090BC40 /* UrlTests.m */, 274C6D1823FBAF5E0090BC40 /* UrlTests.swift */, ); path = GenerateUrlTests; sourceTree = ""; }; 274C6D1923FBAF5E0090BC40 /* UIExtensions */ = { isa = PBXGroup; children = ( B694AAF62B308C6B00075041 /* Video Analytics */, 274C6D1A23FBAF5E0090BC40 /* UIBaseTest.swift */, 274C6D1B23FBAF5E0090BC40 /* UIButtonTests.swift */, 274C6D1C23FBAF5E0090BC40 /* UIImageViewTests.swift */, B6C2D4872A72741B00AA0039 /* CLDVideoPlayerTests.swift */, ); path = UIExtensions; sourceTree = ""; }; 274C6D1D23FBAF5E0090BC40 /* NetworkTests */ = { isa = PBXGroup; children = ( D73622B724910FDD0021E1D4 /* UploaderTests */, D7EF05A7255D8B6D00F93827 /* NetworkBaseTests */, D779B69324B7117200E496EB /* AccessibilityUploderTests */, 274C6D2023FBAF5E0090BC40 /* ManagementApiTests.swift */, 274C6D2123FBAF5E0090BC40 /* DownloaderTests.swift */, D7519FEB25E26BE9006839B1 /* DownloaderAssetTests.swift */, B6AF51622D4A2A430037965A /* NetworkTestUtils.swift */, ); path = NetworkTests; sourceTree = ""; }; 274C6D2323FBAF5E0090BC40 /* Resources */ = { isa = PBXGroup; children = ( 274C6D2423FBAF5E0090BC40 /* Images */, 274C6D2723FBAF5E0090BC40 /* Docs */, 274C6D2A23FBAF5E0090BC40 /* Videos */, ); path = Resources; sourceTree = ""; }; 274C6D2423FBAF5E0090BC40 /* Images */ = { isa = PBXGroup; children = ( D7EC161E2592261200DD7611 /* borderCollieRotatedJpgUnderIOS12.jpg */, D7788A9224E29BEC00B63EB7 /* borderCollieRotatedJpg.jpg */, D7788A9324E29BEC00B63EB7 /* borderCollieRotatedPng.png */, D7EC162025922BA200DD7611 /* borderCollieRotatedPngUnderIOS12.png */, 274C6D2523FBAF5E0090BC40 /* borderCollie.jpg */, D7788A3724DC482C00B63EB7 /* borderCollieCropped.jpg */, D7866140248FE34A0099DE8C /* textImage.jpg */, 274C6D2623FBAF5E0090BC40 /* logo.png */, ); path = Images; sourceTree = ""; }; 274C6D2723FBAF5E0090BC40 /* Docs */ = { isa = PBXGroup; children = ( 274C6D2823FBAF5E0090BC40 /* docx.docx */, 274C6D2923FBAF5E0090BC40 /* pdf.pdf */, ); path = Docs; sourceTree = ""; }; 274C6D2A23FBAF5E0090BC40 /* Videos */ = { isa = PBXGroup; children = ( D7289C7E258A9CB5004DBD29 /* dog2.mp4 */, 274C6D2B23FBAF5E0090BC40 /* dog.mov */, 274C6D2C23FBAF5E0090BC40 /* dog.mp4 */, ); path = Videos; sourceTree = ""; }; 2752795D245ED68F00908341 /* CLDExpressionTests */ = { isa = PBXGroup; children = ( D73117572473D7A30051AAFC /* CLDExpressionTests.m */, 5D7C2DCE245E93F7007E95F7 /* CLDExpressionTests.swift */, ); path = CLDExpressionTests; sourceTree = ""; }; 29B7FB6ECAB68FF54197832A /* Frameworks */ = { isa = PBXGroup; children = ( B60F3C232D4B86E200160FFC /* Cloudinary.framework */, B6FFA5C42D4B86020002EFA5 /* Cloudinary.framework */, 7EAA36BF6CA4E5748DFCD320 /* Pods_Cloudinary_Example.framework */, D07C87FEDB1F561D123D6873 /* Pods_Cloudinary_Tests.framework */, ); name = Frameworks; sourceTree = ""; }; 5D4D8C2A246098A200AE9C96 /* CLDConditionExpressionTests */ = { isa = PBXGroup; children = ( 5D4D8C2B246098BA00AE9C96 /* CLDConditionExpressionTests.swift */, D7119CE2246C7C8100F6B3ED /* CLDConditionExpressionHelpersTests.swift */, D7311759247415340051AAFC /* CLDConditionExpressionTests.m */, ); path = CLDConditionExpressionTests; sourceTree = ""; }; 5D53A94E2488CDB7005C14AB /* ConfigurationTests */ = { isa = PBXGroup; children = ( 5D53A9502488CE23005C14AB /* CLDConfigurationTests.m */, 5D53A94F2488CE23005C14AB /* CLDConfigurationTests.swift */, B6F11F3C288FC20900A895CD /* CLDAnalyticsTests.swift */, ); path = ConfigurationTests; sourceTree = ""; }; 5DB2D29424FFE2E600001845 /* UploadWidgetTests */ = { isa = PBXGroup; children = ( D7EF05AD255D8C0000F93827 /* UploaderWidgetTests */, D7178DA8255D6578007C1995 /* UploaderWidgetViewControllerTests */, D7F34F2D2504F46600C282B1 /* UploaderWidgetPreviewTests */, D7F34F8F25120DE400C282B1 /* UploaderWidgetEditTests */, D7289C82258A9DD4004DBD29 /* UploadWidgetVideoTests */, 5DB2D29524FFE31000001845 /* UploadWidgetHelpersTests */, D7F34F3225052FE700C282B1 /* WidgetBaseTest.swift */, ); path = UploadWidgetTests; sourceTree = ""; }; 5DB2D29524FFE31000001845 /* UploadWidgetHelpersTests */ = { isa = PBXGroup; children = ( 5DB2D29724FFE32500001845 /* UploaderWidgetConfigurationTests.m */, 5DB2D29624FFE32500001845 /* UploaderWidgetConfigurationTests.swift */, D7289C80258A9D0B004DBD29 /* UploaderWidgetAssetContainerTests.swift */, ); path = UploadWidgetHelpersTests; sourceTree = ""; }; 5DBC4D49247BFE570014B37E /* TransformationTests */ = { isa = PBXGroup; children = ( 5D4D8C2A246098A200AE9C96 /* CLDConditionExpressionTests */, 2752795D245ED68F00908341 /* CLDExpressionTests */, D79AE39A245854A6004F2439 /* CLDTransformationTests */, D79AE39924585499004F2439 /* CLDVariableTests */, ); path = TransformationTests; sourceTree = ""; }; 607FACC71AFB9204008FA782 = { isa = PBXGroup; children = ( 607FACF51AFB993E008FA782 /* Podspec Metadata */, 607FACD21AFB9204008FA782 /* Example for Cloudinary */, 607FACE81AFB9204008FA782 /* Tests */, D7173496258296CE006F34CD /* WidgetUITests */, 607FACD11AFB9204008FA782 /* Products */, 0293C6AFE5E20FD5F3D8E4D4 /* Pods */, 29B7FB6ECAB68FF54197832A /* Frameworks */, ); sourceTree = ""; }; 607FACD11AFB9204008FA782 /* Products */ = { isa = PBXGroup; children = ( 607FACD01AFB9204008FA782 /* Cloudinary_Example.app */, 607FACE51AFB9204008FA782 /* Cloudinary_Tests.xctest */, D7173495258296CE006F34CD /* Cloudinary_UITests.xctest */, ); name = Products; sourceTree = ""; }; 607FACD21AFB9204008FA782 /* Example for Cloudinary */ = { isa = PBXGroup; children = ( B64120C22C48EE5F005A5495 /* Core Data */, B64ED1632B8C933900BE653E /* AppDelegate.swift */, B64ED1682B8C933A00BE653E /* Assets.xcassets */, B64ED1672B8C933A00BE653E /* Colors.xcassets */, B64ED1692B8C933A00BE653E /* Controllers */, B64ED1A32B8C933A00BE653E /* Custom Views */, B64ED1D72B8C933B00BE653E /* Extensions */, B64ED1642B8C933900BE653E /* GoogleService-Info.plist */, B64ED1DA2B8C933B00BE653E /* Helpers */, B64F0DDA2BC6506700B94590 /* Info.plist */, B64ED1D92B8C933B00BE653E /* SceneDelegate.swift */, B64ED1D42B8C933A00BE653E /* Utils */, B64ED1AF2B8C933A00BE653E /* Views */, B64ED1622B8C933900BE653E /* Cloudinary_Example-Bridging-Header.h */, ); name = "Example for Cloudinary"; path = Cloudinary; sourceTree = ""; }; 607FACE81AFB9204008FA782 /* Tests */ = { isa = PBXGroup; children = ( B6B4C49D2C56151100C9B604 /* Preprocess */, 272C513B242B6B6E0093AB1B /* BaseNetwork */, D7E79DCF24B515650082288A /* ParamTests */, D71ADD9A247D973E00235AD4 /* CryptoUtilsTests */, 274C6D1523FBAF5E0090BC40 /* FileUtilsTests.swift */, 274C6D1623FBAF5E0090BC40 /* GenerateUrlTests */, 274C6D1D23FBAF5E0090BC40 /* NetworkTests */, 5DBC4D49247BFE570014B37E /* TransformationTests */, 5D53A94E2488CDB7005C14AB /* ConfigurationTests */, 274C6D2323FBAF5E0090BC40 /* Resources */, 274C6D2223FBAF5E0090BC40 /* StringUtilsTest.swift */, 274C6D1923FBAF5E0090BC40 /* UIExtensions */, 5DB2D29424FFE2E600001845 /* UploadWidgetTests */, 607FACE91AFB9204008FA782 /* Supporting Files */, B60F3C212D4B86AF00160FFC /* TestableCloudinary.swift */, ); path = Tests; sourceTree = ""; }; 607FACE91AFB9204008FA782 /* Supporting Files */ = { isa = PBXGroup; children = ( 607FACEA1AFB9204008FA782 /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { isa = PBXGroup; children = ( AA2F53C389142220209152BA /* Cloudinary.podspec */, F2530AD793711AEF6C37A33F /* README.md */, 270C1BF623FBBA3C00A503C3 /* LICENSE */, ); name = "Podspec Metadata"; sourceTree = ""; }; B64120BB2C48ED2A005A5495 /* Single Upload */ = { isa = PBXGroup; children = ( B64ED1892B8C933A00BE653E /* SingleUploadViewController.swift */, B64120BC2C48ED3F005A5495 /* SingleUploadCell.swift */, B64120BE2C48ED69005A5495 /* SingleUploadCollectionController.swift */, B64120C02C48ED83005A5495 /* SingleUploadCollectionLayout.swift */, B64120CE2C48F049005A5495 /* SingleUploadPreview.swift */, ); path = "Single Upload"; sourceTree = ""; }; B64120C22C48EE5F005A5495 /* Core Data */ = { isa = PBXGroup; children = ( B64120C92C48EF10005A5495 /* AssetModel.xcdatamodeld */, B64120C32C48EE76005A5495 /* AssetItems+CoreDataClass.swift */, B64120C52C48EEC7005A5495 /* AssetItems+CoreDataProperties.swift */, B64120C72C48EED7005A5495 /* AssetModel.swift */, B64120CC2C48EF22005A5495 /* CoreDataHelper.swift */, ); path = "Core Data"; sourceTree = ""; }; B64ED1692B8C933A00BE653E /* Controllers */ = { isa = PBXGroup; children = ( B64ED16A2B8C933A00BE653E /* Inner Views */, B64ED18E2B8C933A00BE653E /* MainViewController.swift */, B64ED18F2B8C933A00BE653E /* Video Feed */, B64ED19B2B8C933A00BE653E /* SplashViewController.swift */, B64ED19C2B8C933A00BE653E /* Base */, B64ED19E2B8C933A00BE653E /* Widgets */, ); path = Controllers; sourceTree = ""; }; B64ED16A2B8C933A00BE653E /* Inner Views */ = { isa = PBXGroup; children = ( B64ED16B2B8C933A00BE653E /* Video */, B64ED16F2B8C933A00BE653E /* Transform */, B64ED1862B8C933A00BE653E /* Upload */, ); path = "Inner Views"; sourceTree = ""; }; B64ED16B2B8C933A00BE653E /* Video */ = { isa = PBXGroup; children = ( B64ED16C2B8C933A00BE653E /* VideoFeedCollectionController.swift */, B64ED16D2B8C933A00BE653E /* Video Feed */, ); path = Video; sourceTree = ""; }; B64ED16D2B8C933A00BE653E /* Video Feed */ = { isa = PBXGroup; children = ( B64ED16E2B8C933A00BE653E /* VideoFeedCell.swift */, ); path = "Video Feed"; sourceTree = ""; }; B64ED16F2B8C933A00BE653E /* Transform */ = { isa = PBXGroup; children = ( B64ED1702B8C933A00BE653E /* Optimization */, B64ED1722B8C933A00BE653E /* Transformation */, B64ED17C2B8C933A00BE653E /* Use Cases */, B64ED1852B8C933A00BE653E /* DeliveryViewController.swift */, ); path = Transform; sourceTree = ""; }; B64ED1702B8C933A00BE653E /* Optimization */ = { isa = PBXGroup; children = ( B64ED1712B8C933A00BE653E /* OptimizationViewController.swift */, ); path = Optimization; sourceTree = ""; }; B64ED1722B8C933A00BE653E /* Transformation */ = { isa = PBXGroup; children = ( B64ED1732B8C933A00BE653E /* TransformCollectionCell.swift */, B64ED1742B8C933A00BE653E /* TransformViewController.swift */, B64ED1752B8C933A00BE653E /* TransformationCell.swift */, B64ED1762B8C933A00BE653E /* Reveal Image */, B64ED1782B8C933A00BE653E /* DeliveryTransformCollectionController.swift */, B64ED1792B8C933A00BE653E /* TransformCollectionController.swift */, B64ED17A2B8C933A00BE653E /* Smart Cropping */, ); path = Transformation; sourceTree = ""; }; B64ED1762B8C933A00BE653E /* Reveal Image */ = { isa = PBXGroup; children = ( B64ED1772B8C933A00BE653E /* RevealImageController.swift */, ); path = "Reveal Image"; sourceTree = ""; }; B64ED17A2B8C933A00BE653E /* Smart Cropping */ = { isa = PBXGroup; children = ( B64ED17B2B8C933A00BE653E /* SmartCroppingController.swift */, ); path = "Smart Cropping"; sourceTree = ""; }; B64ED17C2B8C933A00BE653E /* Use Cases */ = { isa = PBXGroup; children = ( B64ED17D2B8C933A00BE653E /* UseCaseCell.swift */, B64ED17E2B8C933A00BE653E /* UseCaseCollectionCell.swift */, B64ED17F2B8C933A00BE653E /* Screens */, B64ED1822B8C933A00BE653E /* UseCasesCollectionController.swift */, B64ED1832B8C933A00BE653E /* DeliveryUseCasesCollectionController.swift */, B64ED1842B8C933A00BE653E /* UseCasesViewController.swift */, ); path = "Use Cases"; sourceTree = ""; }; B64ED17F2B8C933A00BE653E /* Screens */ = { isa = PBXGroup; children = ( B64ED1802B8C933A00BE653E /* ConditionalProductViewController.swift */, B64ED1812B8C933A00BE653E /* NormalizingProductAssetsViewController.swift */, B67476142B8CA0D2006ED6C2 /* IntegrateAIViewController.swift */, ); path = Screens; sourceTree = ""; }; B64ED1862B8C933A00BE653E /* Upload */ = { isa = PBXGroup; children = ( B64120BB2C48ED2A005A5495 /* Single Upload */, B64ED1872B8C933A00BE653E /* UploadChoiceController.swift */, B64ED1882B8C933A00BE653E /* InnerUploadFrame.swift */, B64ED18A2B8C933A00BE653E /* UploadViewController.swift */, B64ED18B2B8C933A00BE653E /* UploadNoCloudController.swift */, B64ED18C2B8C933A00BE653E /* Upload Does Not Exist */, ); path = Upload; sourceTree = ""; }; B64ED18C2B8C933A00BE653E /* Upload Does Not Exist */ = { isa = PBXGroup; children = ( B64ED18D2B8C933A00BE653E /* UploadDoesNotExistController.swift */, ); path = "Upload Does Not Exist"; sourceTree = ""; }; B64ED18F2B8C933A00BE653E /* Video Feed */ = { isa = PBXGroup; children = ( B64ED1902B8C933A00BE653E /* Resources */, B64ED1922B8C933A00BE653E /* Controllers */, B64ED1992B8C933A00BE653E /* Helpers */, ); path = "Video Feed"; sourceTree = ""; }; B64ED1902B8C933A00BE653E /* Resources */ = { isa = PBXGroup; children = ( B64ED1912B8C933A00BE653E /* video_links.plist */, ); path = Resources; sourceTree = ""; }; B64ED1922B8C933A00BE653E /* Controllers */ = { isa = PBXGroup; children = ( B64ED1932B8C933A00BE653E /* VideoSocialOverlaysController.swift */, B64ED1942B8C933A00BE653E /* VideoFeedViewController.swift */, B64ED1952B8C933A00BE653E /* MainPageController.swift */, B64ED1962B8C933A00BE653E /* VideoFeedContainerController.swift */, B64ED1972B8C933A00BE653E /* VideoViewController.swift */, B64ED1982B8C933A00BE653E /* VideoFeedController.swift */, ); path = Controllers; sourceTree = ""; }; B64ED1992B8C933A00BE653E /* Helpers */ = { isa = PBXGroup; children = ( B64ED19A2B8C933A00BE653E /* VideoHelper.swift */, ); path = Helpers; sourceTree = ""; }; B64ED19C2B8C933A00BE653E /* Base */ = { isa = PBXGroup; children = ( B64ED19D2B8C933A00BE653E /* BaseViewController.swift */, ); path = Base; sourceTree = ""; }; B64ED19E2B8C933A00BE653E /* Widgets */ = { isa = PBXGroup; children = ( B64ED19F2B8C933A00BE653E /* WidgetsViewController.swift */, B64ED1A02B8C933A00BE653E /* ImageWidgetViewController.swift */, B64ED1A12B8C933A00BE653E /* UploadWidgetViewController.swift */, ); path = Widgets; sourceTree = ""; }; B64ED1A32B8C933A00BE653E /* Custom Views */ = { isa = PBXGroup; children = ( B64ED1A42B8C933A00BE653E /* ToolBar */, B64ED1AA2B8C933A00BE653E /* RevealImageView.swift */, B64ED1AB2B8C933A00BE653E /* Upload Loading View */, B64ED1AE2B8C933A00BE653E /* GradientView.swift */, ); path = "Custom Views"; sourceTree = ""; }; B64ED1A42B8C933A00BE653E /* ToolBar */ = { isa = PBXGroup; children = ( B64ED1A52B8C933A00BE653E /* Toolbar.swift */, B64ED1A62B8C933A00BE653E /* Toolbar.xib */, B64ED1A72B8C933A00BE653E /* Item */, ); path = ToolBar; sourceTree = ""; }; B64ED1A72B8C933A00BE653E /* Item */ = { isa = PBXGroup; children = ( B64ED1A82B8C933A00BE653E /* ToolbarItem.swift */, B64ED1A92B8C933A00BE653E /* ToolbarItem.xib */, ); path = Item; sourceTree = ""; }; B64ED1AB2B8C933A00BE653E /* Upload Loading View */ = { isa = PBXGroup; children = ( B64ED1AC2B8C933A00BE653E /* UploadLoadingView.swift */, B64ED1AD2B8C933A00BE653E /* UploadLoadingView.xib */, ); path = "Upload Loading View"; sourceTree = ""; }; B64ED1AF2B8C933A00BE653E /* Views */ = { isa = PBXGroup; children = ( B64ED1B02B8C933A00BE653E /* Inner Views */, B64ED1C72B8C933A00BE653E /* LaunchScreen.storyboard */, B64ED1C92B8C933A00BE653E /* Main.storyboard */, B64ED1CB2B8C933A00BE653E /* Splash.storyboard */, B64ED1CC2B8C933A00BE653E /* Transform */, B64ED1D02B8C933A00BE653E /* Upload */, ); path = Views; sourceTree = ""; }; B64ED1B02B8C933A00BE653E /* Inner Views */ = { isa = PBXGroup; children = ( B64ED1B12B8C933A00BE653E /* Video */, B64ED1B62B8C933A00BE653E /* Transform */, B64ED1BD2B8C933A00BE653E /* Base */, B64ED1BF2B8C933A00BE653E /* Widgets */, B64ED1C32B8C933A00BE653E /* Upload */, ); path = "Inner Views"; sourceTree = ""; }; B64ED1B12B8C933A00BE653E /* Video */ = { isa = PBXGroup; children = ( B64ED1B22B8C933A00BE653E /* VideoFeed.storyboard */, B64ED1B32B8C933A00BE653E /* Social */, B64ED1B52B8C933A00BE653E /* Video.storyboard */, ); path = Video; sourceTree = ""; }; B64ED1B32B8C933A00BE653E /* Social */ = { isa = PBXGroup; children = ( B64ED1B42B8C933A00BE653E /* VideoSocialOverlays.storyboard */, ); path = Social; sourceTree = ""; }; B64ED1B62B8C933A00BE653E /* Transform */ = { isa = PBXGroup; children = ( B64ED1B72B8C933A00BE653E /* Delivery.storyboard */, B64ED1B82B8C933A00BE653E /* Optimization.storyboard */, B64ED1B92B8C933A00BE653E /* UseCases.storyboard */, B64ED1BA2B8C933A00BE653E /* Use Cases */, ); path = Transform; sourceTree = ""; }; B64ED1BA2B8C933A00BE653E /* Use Cases */ = { isa = PBXGroup; children = ( B64ED1BB2B8C933A00BE653E /* NormalizingProductAssets.storyboard */, B64ED1BC2B8C933A00BE653E /* ConditionalProduct.storyboard */, B67476162B8CA0EF006ED6C2 /* IntegrateAI.storyboard */, ); path = "Use Cases"; sourceTree = ""; }; B64ED1BD2B8C933A00BE653E /* Base */ = { isa = PBXGroup; children = ( B64ED1BE2B8C933A00BE653E /* Base.storyboard */, ); path = Base; sourceTree = ""; }; B64ED1BF2B8C933A00BE653E /* Widgets */ = { isa = PBXGroup; children = ( B64ED1C02B8C933A00BE653E /* ImageWidget.storyboard */, B64ED1C22B8C933A00BE653E /* Widgets.storyboard */, B64F0DDC2BC6541000B94590 /* UploadWidget.storyboard */, ); path = Widgets; sourceTree = ""; }; B64ED1C32B8C933A00BE653E /* Upload */ = { isa = PBXGroup; children = ( B64ED1C42B8C933A00BE653E /* InnerUploadFrame.xib */, B64ED1C52B8C933A00BE653E /* Upload.storyboard */, B64ED1C62B8C933A00BE653E /* UploadNoCloud.storyboard */, ); path = Upload; sourceTree = ""; }; B64ED1CC2B8C933A00BE653E /* Transform */ = { isa = PBXGroup; children = ( B64ED1CD2B8C933A00BE653E /* SmartCropping.storyboard */, B64ED1CE2B8C933A00BE653E /* RevealImage.storyboard */, B64ED1CF2B8C933A00BE653E /* Transform.storyboard */, ); path = Transform; sourceTree = ""; }; B64ED1D02B8C933A00BE653E /* Upload */ = { isa = PBXGroup; children = ( B64ED1D12B8C933A00BE653E /* UploadDoesNotExist.storyboard */, B64ED1D22B8C933A00BE653E /* SingleUpload.storyboard */, B64ED1D32B8C933A00BE653E /* UploadChoice.storyboard */, B64120D02C48F0EC005A5495 /* SingleUploadPreview.storyboard */, ); path = Upload; sourceTree = ""; }; B64ED1D42B8C933A00BE653E /* Utils */ = { isa = PBXGroup; children = ( B64ED1D52B8C933A00BE653E /* CloudinaryHelper.swift */, B64ED1D62B8C933A00BE653E /* FileUtils.swift */, ); path = Utils; sourceTree = ""; }; B64ED1D72B8C933B00BE653E /* Extensions */ = { isa = PBXGroup; children = ( B64ED1D82B8C933B00BE653E /* Double+Extension.swift */, ); path = Extensions; sourceTree = ""; }; B64ED1DA2B8C933B00BE653E /* Helpers */ = { isa = PBXGroup; children = ( B64ED1DB2B8C933B00BE653E /* ImageHelper.swift */, B64ED1DC2B8C933B00BE653E /* AnimationHelper.swift */, B64ED1DD2B8C933B00BE653E /* Events */, ); path = Helpers; sourceTree = ""; }; B64ED1DD2B8C933B00BE653E /* Events */ = { isa = PBXGroup; children = ( B64ED1DE2B8C933B00BE653E /* EventsHandler.swift */, B64ED1DF2B8C933B00BE653E /* FirebaseEventsHandler.swift */, B64ED1E02B8C933B00BE653E /* EventObject.swift */, B64ED1E12B8C933B00BE653E /* EventsProvider.swift */, ); path = Events; sourceTree = ""; }; B694AAF62B308C6B00075041 /* Video Analytics */ = { isa = PBXGroup; children = ( B694AAF22B308C3C00075041 /* VideoEventsTests.swift */, B694AAF42B308C5A00075041 /* VideoEventsManagerTests.swift */, ); path = "Video Analytics"; sourceTree = ""; }; B6B4C49D2C56151100C9B604 /* Preprocess */ = { isa = PBXGroup; children = ( 274C6D1423FBAF5E0090BC40 /* PreprocessTests.swift */, B6B4C49E2C56152700C9B604 /* VideoPreprocessTests.swift */, ); path = Preprocess; sourceTree = ""; }; D7173496258296CE006F34CD /* WidgetUITests */ = { isa = PBXGroup; children = ( D7173497258296CE006F34CD /* WidgetUITests.swift */, D7173499258296CE006F34CD /* Info.plist */, ); path = WidgetUITests; sourceTree = ""; }; D7178DA8255D6578007C1995 /* UploaderWidgetViewControllerTests */ = { isa = PBXGroup; children = ( D7567063255D661F005B65D3 /* UploaderWidgetViewControllerTests.swift */, ); path = UploaderWidgetViewControllerTests; sourceTree = ""; }; D71ADD9A247D973E00235AD4 /* CryptoUtilsTests */ = { isa = PBXGroup; children = ( 274C6D1323FBAF5E0090BC40 /* CryptoUtilsTests.swift */, D71ADD9B247D976500235AD4 /* CryptoUtilsTests.m */, ); path = CryptoUtilsTests; sourceTree = ""; }; D7289C82258A9DD4004DBD29 /* UploadWidgetVideoTests */ = { isa = PBXGroup; children = ( D7289C83258A9E18004DBD29 /* UploaderWidgetVideoControlsTests.swift */, D7289C85258A9E18004DBD29 /* UploaderWidgetVideoDisplayLinkTests.swift */, D7289C84258A9E18004DBD29 /* UploaderWidgetVideoPlayerTests.swift */, D7289C86258A9E18004DBD29 /* UploaderWidgetVideoViewTests.swift */, ); path = UploadWidgetVideoTests; sourceTree = ""; }; D73622B724910FDD0021E1D4 /* UploaderTests */ = { isa = PBXGroup; children = ( 1E635A7725A7585E0003E9D3 /* QualityAnalysisUploaderTests */, 1E635A6E25A757DA0003E9D3 /* MockProvider */, 274C6D1F23FBAF5E0090BC40 /* UploaderTests.swift */, D7F3D45E24DC3B8A002C1D27 /* PreprocessUploaderTests */, D7E79DC224B515260082288A /* OcrUploaderTests */, ); path = UploaderTests; sourceTree = ""; }; D751A01225E26D5D006839B1 /* CLDURLCacheTests */ = { isa = PBXGroup; children = ( ); path = CLDURLCacheTests; sourceTree = ""; }; D779B69324B7117200E496EB /* AccessibilityUploderTests */ = { isa = PBXGroup; children = ( D779B69424B7117200E496EB /* UploaderAccessibilityTests.swift */, D779B69524B7117200E496EB /* UploaderAccessibilityTests.m */, ); path = AccessibilityUploderTests; sourceTree = ""; }; D79AE39924585499004F2439 /* CLDVariableTests */ = { isa = PBXGroup; children = ( D79AE39B24585533004F2439 /* CLDVariableTests.m */, D79AE386244DF1E7004F2439 /* CLDVariableTests.swift */, ); path = CLDVariableTests; sourceTree = ""; }; D79AE39A245854A6004F2439 /* CLDTransformationTests */ = { isa = PBXGroup; children = ( 5DE4EC1E2469919700F6C8D6 /* CLDTransformationBaselineTests.swift */, D79AE384244CC1D7004F2439 /* CLDTransformationTests.swift */, D71B78FD24680743004AA28E /* CLDTransformationVariablesTests.swift */, D71B78FF24680773004AA28E /* CLDTransformationExpressionsTests.swift */, D71B7901246808EE004AA28E /* CLDTransformationConditionsTests.swift */, D731175B24741FBE0051AAFC /* CLDTransformationTests.m */, ); path = CLDTransformationTests; sourceTree = ""; }; D7E79DC224B515260082288A /* OcrUploaderTests */ = { isa = PBXGroup; children = ( D7E79DC324B515260082288A /* UploaderOcrTests.swift */, D7E79DC424B515260082288A /* OcrMockProvider.swift */, D7E79DC524B515260082288A /* ExplicitMockOcrTests.m */, D7E79DC624B515260082288A /* UploaderMockOcrTests.m */, D7E79DC724B515260082288A /* UploaderMockOcrTests.swift */, D7E79DC824B515260082288A /* ExplicitMockOcrTests.swift */, ); path = OcrUploaderTests; sourceTree = ""; }; D7E79DCF24B515650082288A /* ParamTests */ = { isa = PBXGroup; children = ( D7E79DD024B515650082288A /* UploadRequestParamsTests.swift */, ); path = ParamTests; sourceTree = ""; }; D7EF05A7255D8B6D00F93827 /* NetworkBaseTests */ = { isa = PBXGroup; children = ( D7EF05A8255D8B8E00F93827 /* NetworkBaseTest.swift */, D7EF05A9255D8B8E00F93827 /* NetworkBaseTestObjc.h */, D7EF05AA255D8B8E00F93827 /* NetworkBaseTestObjc.m */, ); path = NetworkBaseTests; sourceTree = ""; }; D7EF05AD255D8C0000F93827 /* UploaderWidgetTests */ = { isa = PBXGroup; children = ( D7EF05AF255D8C1900F93827 /* UploaderWidgetTests.m */, D7EF05AE255D8C1900F93827 /* UploaderWidgetTests.swift */, ); path = UploaderWidgetTests; sourceTree = ""; }; D7F34F2D2504F46600C282B1 /* UploaderWidgetPreviewTests */ = { isa = PBXGroup; children = ( D7F34F2E2504F48C00C282B1 /* UploaderWidgetCollectionCellTests.swift */, D7F34F3025052FBD00C282B1 /* UploaderWidgetPreviewViewControllerTests.swift */, ); path = UploaderWidgetPreviewTests; sourceTree = ""; }; D7F34F8F25120DE400C282B1 /* UploaderWidgetEditTests */ = { isa = PBXGroup; children = ( D7F34F9025120E0C00C282B1 /* UploaderWidgetEditViewControllerTests.swift */, ); path = UploaderWidgetEditTests; sourceTree = ""; }; D7F3D45E24DC3B8A002C1D27 /* PreprocessUploaderTests */ = { isa = PBXGroup; children = ( D7F3D45F24DC3BAB002C1D27 /* PreprocessUploaderTests.swift */, ); path = PreprocessUploaderTests; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 607FACCF1AFB9204008FA782 /* Cloudinary_Example */ = { isa = PBXNativeTarget; buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "Cloudinary_Example" */; buildPhases = ( 4DDBDAA6D5EA05A66CEFFB59 /* [CP] Check Pods Manifest.lock */, 607FACCC1AFB9204008FA782 /* Sources */, 607FACCD1AFB9204008FA782 /* Frameworks */, 607FACCE1AFB9204008FA782 /* Resources */, 0B0BD5E65F34EDF0AD421887 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Cloudinary_Example; productName = Cloudinary; productReference = 607FACD01AFB9204008FA782 /* Cloudinary_Example.app */; productType = "com.apple.product-type.application"; }; 607FACE41AFB9204008FA782 /* Cloudinary_Tests */ = { isa = PBXNativeTarget; buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "Cloudinary_Tests" */; buildPhases = ( 66267CF881CE925C297775A4 /* [CP] Check Pods Manifest.lock */, 607FACE11AFB9204008FA782 /* Sources */, 607FACE21AFB9204008FA782 /* Frameworks */, 607FACE31AFB9204008FA782 /* Resources */, ); buildRules = ( ); dependencies = ( 607FACE71AFB9204008FA782 /* PBXTargetDependency */, ); name = Cloudinary_Tests; productName = Tests; productReference = 607FACE51AFB9204008FA782 /* Cloudinary_Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; D7173494258296CE006F34CD /* Cloudinary_UITests */ = { isa = PBXNativeTarget; buildConfigurationList = D717349C258296CE006F34CD /* Build configuration list for PBXNativeTarget "Cloudinary_UITests" */; buildPhases = ( D7173491258296CE006F34CD /* Sources */, D7173492258296CE006F34CD /* Frameworks */, D7173493258296CE006F34CD /* Resources */, ); buildRules = ( ); dependencies = ( D717349B258296CE006F34CD /* PBXTargetDependency */, ); name = Cloudinary_UITests; productName = WidgetUITests; productReference = D7173495258296CE006F34CD /* Cloudinary_UITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 607FACC81AFB9204008FA782 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1170; LastUpgradeCheck = 1250; ORGANIZATIONNAME = CocoaPods; TargetAttributes = { 607FACCF1AFB9204008FA782 = { CreatedOnToolsVersion = 6.3.1; DevelopmentTeam = 3TR999VVAS; LastSwiftMigration = 1510; }; 607FACE41AFB9204008FA782 = { CreatedOnToolsVersion = 6.3.1; DevelopmentTeam = 3TR999VVAS; LastSwiftMigration = 1130; TestTargetID = 607FACCF1AFB9204008FA782; }; D7173494258296CE006F34CD = { CreatedOnToolsVersion = 11.7; DevelopmentTeam = 69UZJ87W84; ProvisioningStyle = Automatic; TestTargetID = 607FACCF1AFB9204008FA782; }; }; }; buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "Cloudinary" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 607FACC71AFB9204008FA782; productRefGroup = 607FACD11AFB9204008FA782 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 607FACCF1AFB9204008FA782 /* Cloudinary_Example */, 607FACE41AFB9204008FA782 /* Cloudinary_Tests */, D7173494258296CE006F34CD /* Cloudinary_UITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 607FACCE1AFB9204008FA782 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( B64ED21C2B8C933B00BE653E /* NormalizingProductAssets.storyboard in Resources */, B64ED21E2B8C933B00BE653E /* Base.storyboard in Resources */, D73DBB0C258BA05A0002E7E5 /* dog.mp4 in Resources */, 270C1BF723FBBA3C00A503C3 /* LICENSE in Resources */, B64ED21B2B8C933B00BE653E /* UseCases.storyboard in Resources */, B64ED2292B8C933B00BE653E /* RevealImage.storyboard in Resources */, B64ED1E32B8C933B00BE653E /* GoogleService-Info.plist in Resources */, B67476172B8CA0EF006ED6C2 /* IntegrateAI.storyboard in Resources */, D73DBB0B258BA0570002E7E5 /* dog.mov in Resources */, B64ED2222B8C933B00BE653E /* InnerUploadFrame.xib in Resources */, D73DBB0A258BA0550002E7E5 /* dog2.mp4 in Resources */, B64ED21D2B8C933B00BE653E /* ConditionalProduct.storyboard in Resources */, B64ED2002B8C933B00BE653E /* video_links.plist in Resources */, B64ED2192B8C933B00BE653E /* Delivery.storyboard in Resources */, B64ED22A2B8C933B00BE653E /* Transform.storyboard in Resources */, B64ED2272B8C933B00BE653E /* Splash.storyboard in Resources */, B64ED22C2B8C933B00BE653E /* SingleUpload.storyboard in Resources */, B64ED1E62B8C933B00BE653E /* Assets.xcassets in Resources */, B64ED2142B8C933B00BE653E /* UploadLoadingView.xib in Resources */, B64ED20F2B8C933B00BE653E /* Toolbar.xib in Resources */, B64ED2252B8C933B00BE653E /* LaunchScreen.storyboard in Resources */, B64ED22B2B8C933B00BE653E /* UploadDoesNotExist.storyboard in Resources */, B64ED21F2B8C933B00BE653E /* ImageWidget.storyboard in Resources */, B64120D12C48F0EC005A5495 /* SingleUploadPreview.storyboard in Resources */, B64ED2112B8C933B00BE653E /* ToolbarItem.xib in Resources */, B64ED22D2B8C933B00BE653E /* UploadChoice.storyboard in Resources */, B64ED2262B8C933B00BE653E /* Main.storyboard in Resources */, B64ED21A2B8C933B00BE653E /* Optimization.storyboard in Resources */, B64ED2282B8C933B00BE653E /* SmartCropping.storyboard in Resources */, B64ED2172B8C933B00BE653E /* VideoSocialOverlays.storyboard in Resources */, B64ED2212B8C933B00BE653E /* Widgets.storyboard in Resources */, B64ED2162B8C933B00BE653E /* VideoFeed.storyboard in Resources */, B64F0DDD2BC6541000B94590 /* UploadWidget.storyboard in Resources */, B64ED2242B8C933B00BE653E /* UploadNoCloud.storyboard in Resources */, B64ED2182B8C933B00BE653E /* Video.storyboard in Resources */, B64ED2232B8C933B00BE653E /* Upload.storyboard in Resources */, B64ED1E52B8C933B00BE653E /* Colors.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 607FACE31AFB9204008FA782 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( D7866141248FE34A0099DE8C /* textImage.jpg in Resources */, 272C5190242B6C360093AB1B /* valid.data in Resources */, 272C5191242B6C360093AB1B /* empty.data in Resources */, 274C6D3F23FBAF5E0090BC40 /* dog.mp4 in Resources */, 274C6D3B23FBAF5E0090BC40 /* logo.png in Resources */, D7289C7F258A9CB5004DBD29 /* dog2.mp4 in Resources */, 272C5194242B6C360093AB1B /* invalid_data.json in Resources */, D7788A9424E29BEC00B63EB7 /* borderCollieRotatedJpg.jpg in Resources */, 274C6D3E23FBAF5E0090BC40 /* dog.mov in Resources */, 274C6D3D23FBAF5E0090BC40 /* pdf.pdf in Resources */, 272C518E242B6C360093AB1B /* rainbow.jpg in Resources */, 272C518D242B6C360093AB1B /* unicorn.png in Resources */, 272C5193242B6C360093AB1B /* valid_data.json in Resources */, 272C5195242B6C360093AB1B /* utf8_string.txt in Resources */, 272C5196242B6C360093AB1B /* utf32_string.txt in Resources */, D7EC162125922BA200DD7611 /* borderCollieRotatedPngUnderIOS12.png in Resources */, 274C6D3C23FBAF5E0090BC40 /* docx.docx in Resources */, 272C5197242B6C360093AB1B /* empty_string.txt in Resources */, 272C518F242B6C360093AB1B /* invalid.data in Resources */, D7EC161F2592261200DD7611 /* borderCollieRotatedJpgUnderIOS12.jpg in Resources */, D7788A3824DC482C00B63EB7 /* borderCollieCropped.jpg in Resources */, 274C6D3A23FBAF5E0090BC40 /* borderCollie.jpg in Resources */, 272C5192242B6C360093AB1B /* empty_data.json in Resources */, D7788A9524E29BEC00B63EB7 /* borderCollieRotatedPng.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; D7173493258296CE006F34CD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 0B0BD5E65F34EDF0AD421887 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Cloudinary_Example/Pods-Cloudinary_Example-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Cloudinary/Cloudinary.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Cloudinary.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Cloudinary_Example/Pods-Cloudinary_Example-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 4DDBDAA6D5EA05A66CEFFB59 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Cloudinary_Example-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 66267CF881CE925C297775A4 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Cloudinary_Tests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 607FACCC1AFB9204008FA782 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( B64ED1FC2B8C933B00BE653E /* UploadViewController.swift in Sources */, B64ED1FF2B8C933B00BE653E /* MainViewController.swift in Sources */, B64ED2032B8C933B00BE653E /* MainPageController.swift in Sources */, B64ED2082B8C933B00BE653E /* SplashViewController.swift in Sources */, B64ED2062B8C933B00BE653E /* VideoFeedController.swift in Sources */, B64ED2342B8C933B00BE653E /* EventsHandler.swift in Sources */, B64ED2092B8C933B00BE653E /* BaseViewController.swift in Sources */, B64ED1E22B8C933B00BE653E /* AppDelegate.swift in Sources */, B64ED1F32B8C933B00BE653E /* ConditionalProductViewController.swift in Sources */, B64ED2122B8C933B00BE653E /* RevealImageView.swift in Sources */, B64ED22F2B8C933B00BE653E /* FileUtils.swift in Sources */, B64ED1F12B8C933B00BE653E /* UseCaseCell.swift in Sources */, B64ED1F92B8C933B00BE653E /* UploadChoiceController.swift in Sources */, B64ED2052B8C933B00BE653E /* VideoViewController.swift in Sources */, B64ED1EA2B8C933B00BE653E /* TransformCollectionCell.swift in Sources */, B64ED20A2B8C933B00BE653E /* WidgetsViewController.swift in Sources */, B64ED1EF2B8C933B00BE653E /* TransformCollectionController.swift in Sources */, B64ED1F02B8C933B00BE653E /* SmartCroppingController.swift in Sources */, B64ED1F42B8C933B00BE653E /* NormalizingProductAssetsViewController.swift in Sources */, B64ED2042B8C933B00BE653E /* VideoFeedContainerController.swift in Sources */, B64ED2012B8C933B00BE653E /* VideoSocialOverlaysController.swift in Sources */, B64ED1E82B8C933B00BE653E /* VideoFeedCell.swift in Sources */, B64ED1F82B8C933B00BE653E /* DeliveryViewController.swift in Sources */, B64ED1FE2B8C933B00BE653E /* UploadDoesNotExistController.swift in Sources */, B64ED2072B8C933B00BE653E /* VideoHelper.swift in Sources */, B64ED2362B8C933B00BE653E /* EventObject.swift in Sources */, B64ED20C2B8C933B00BE653E /* UploadWidgetViewController.swift in Sources */, B64ED20B2B8C933B00BE653E /* ImageWidgetViewController.swift in Sources */, B64120CD2C48EF22005A5495 /* CoreDataHelper.swift in Sources */, B64120C82C48EED7005A5495 /* AssetModel.swift in Sources */, B64ED1FB2B8C933B00BE653E /* SingleUploadViewController.swift in Sources */, B64ED2132B8C933B00BE653E /* UploadLoadingView.swift in Sources */, B64120CB2C48EF10005A5495 /* AssetModel.xcdatamodeld in Sources */, B64120CF2C48F049005A5495 /* SingleUploadPreview.swift in Sources */, B64ED1EB2B8C933B00BE653E /* TransformViewController.swift in Sources */, B64ED2332B8C933B00BE653E /* AnimationHelper.swift in Sources */, B67476152B8CA0D2006ED6C2 /* IntegrateAIViewController.swift in Sources */, B64120C42C48EE76005A5495 /* AssetItems+CoreDataClass.swift in Sources */, B64ED2372B8C933B00BE653E /* EventsProvider.swift in Sources */, B64ED2352B8C933B00BE653E /* FirebaseEventsHandler.swift in Sources */, B64ED1FD2B8C933B00BE653E /* UploadNoCloudController.swift in Sources */, B64ED20E2B8C933B00BE653E /* Toolbar.swift in Sources */, B64ED1F22B8C933B00BE653E /* UseCaseCollectionCell.swift in Sources */, B64ED1ED2B8C933B00BE653E /* RevealImageController.swift in Sources */, B64ED1E92B8C933B00BE653E /* OptimizationViewController.swift in Sources */, B64120C62C48EEC7005A5495 /* AssetItems+CoreDataProperties.swift in Sources */, B64120BF2C48ED69005A5495 /* SingleUploadCollectionController.swift in Sources */, B64120BD2C48ED3F005A5495 /* SingleUploadCell.swift in Sources */, B64ED1F72B8C933B00BE653E /* UseCasesViewController.swift in Sources */, B64ED1EC2B8C933B00BE653E /* TransformationCell.swift in Sources */, B64ED1F52B8C933B00BE653E /* UseCasesCollectionController.swift in Sources */, B64ED1E72B8C933B00BE653E /* VideoFeedCollectionController.swift in Sources */, B64ED2302B8C933B00BE653E /* Double+Extension.swift in Sources */, B64ED2312B8C933B00BE653E /* SceneDelegate.swift in Sources */, B64ED1EE2B8C933B00BE653E /* DeliveryTransformCollectionController.swift in Sources */, B64ED1FA2B8C933B00BE653E /* InnerUploadFrame.swift in Sources */, B64ED22E2B8C933B00BE653E /* CloudinaryHelper.swift in Sources */, B64ED2022B8C933B00BE653E /* VideoFeedViewController.swift in Sources */, B64ED2102B8C933B00BE653E /* ToolbarItem.swift in Sources */, B64ED2152B8C933B00BE653E /* GradientView.swift in Sources */, B64ED1F62B8C933B00BE653E /* DeliveryUseCasesCollectionController.swift in Sources */, B64ED2322B8C933B00BE653E /* ImageHelper.swift in Sources */, B64120C12C48ED83005A5495 /* SingleUploadCollectionLayout.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 607FACE11AFB9204008FA782 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 5DB2D29824FFE32500001845 /* UploaderWidgetConfigurationTests.swift in Sources */, D7519FAC25E2693A006839B1 /* CLDCloudinaryTests.swift in Sources */, 272C5189242B6C360093AB1B /* ResponseTests.swift in Sources */, D7519FAD25E2693A006839B1 /* CLDCloudinaryTests.m in Sources */, D7F3D46024DC3BAB002C1D27 /* PreprocessUploaderTests.swift in Sources */, 1E635A8425A7585E0003E9D3 /* QualityAnalysisExplicitResultParserTests.swift in Sources */, 274C6D3923FBAF5E0090BC40 /* StringUtilsTest.swift in Sources */, 274C6D2D23FBAF5E0090BC40 /* CryptoUtilsTests.swift in Sources */, D7E79DCC24B515260082288A /* UploaderMockOcrTests.m in Sources */, D79AE39C24585533004F2439 /* CLDVariableTests.m in Sources */, D7F34F9125120E0D00C282B1 /* UploaderWidgetEditViewControllerTests.swift in Sources */, D7519FEC25E26BE9006839B1 /* DownloaderAssetTests.swift in Sources */, D71B790024680773004AA28E /* CLDTransformationExpressionsTests.swift in Sources */, D7E79DCE24B515260082288A /* ExplicitMockOcrTests.swift in Sources */, 272C5186242B6C360093AB1B /* MultipartFormDataTests.swift in Sources */, D7EF05B1255D8C1900F93827 /* UploaderWidgetTests.m in Sources */, 1E635A8525A7585E0003E9D3 /* ObjcQualityAnalysisUploadResultParserTests.m in Sources */, D7289C8A258A9E18004DBD29 /* UploaderWidgetVideoViewTests.swift in Sources */, 272C5180242B6C360093AB1B /* FileManager+CloudinaryTests.swift in Sources */, D71ADD9C247D976500235AD4 /* CryptoUtilsTests.m in Sources */, 272C5187242B6C360093AB1B /* URLProtocolTests.swift in Sources */, 272C517F242B6C360093AB1B /* SessionManagerTests.swift in Sources */, B6AF51632D4A2A430037965A /* NetworkTestUtils.swift in Sources */, 274C6D3823FBAF5E0090BC40 /* DownloaderTests.swift in Sources */, 5D7C2DCF245E93F7007E95F7 /* CLDExpressionTests.swift in Sources */, 272C51B0242B6C360093AB1B /* UploadTests.swift in Sources */, 274C6D3723FBAF5E0090BC40 /* ManagementApiTests.swift in Sources */, D7EF05B0255D8C1900F93827 /* UploaderWidgetTests.swift in Sources */, D7119CE3246C7C8100F6B3ED /* CLDConditionExpressionHelpersTests.swift in Sources */, D7289C88258A9E18004DBD29 /* UploaderWidgetVideoPlayerTests.swift in Sources */, 274C6D3123FBAF5E0090BC40 /* UrlTests.swift in Sources */, B694AAF52B308C5A00075041 /* VideoEventsManagerTests.swift in Sources */, D7567064255D661F005B65D3 /* UploaderWidgetViewControllerTests.swift in Sources */, D7F34F2F2504F48C00C282B1 /* UploaderWidgetCollectionCellTests.swift in Sources */, D79AE387244DF1E7004F2439 /* CLDVariableTests.swift in Sources */, 1E635A8025A7585E0003E9D3 /* ObjcUploaderQualityAnalysisTests.m in Sources */, 272C5181242B6C360093AB1B /* AuthenticationTests.swift in Sources */, D7EF05AB255D8B8E00F93827 /* NetworkBaseTest.swift in Sources */, D7E79DD124B515650082288A /* UploadRequestParamsTests.swift in Sources */, D71B78FE24680743004AA28E /* CLDTransformationVariablesTests.swift in Sources */, 1E635A8125A7585E0003E9D3 /* QualityAnalysisUploadResultParserTests.swift in Sources */, 5D53A9512488CE23005C14AB /* CLDConfigurationTests.swift in Sources */, D779B69724B7117300E496EB /* UploaderAccessibilityTests.swift in Sources */, 27BC1FB02431C48D000AFC2C /* CLDNResult+CloudinaryTests.swift in Sources */, B6F11F3D288FC20900A895CD /* CLDAnalyticsTests.swift in Sources */, 1E635A7F25A7585E0003E9D3 /* MockProviderQualityAnalysis.swift in Sources */, D73117582473D7A30051AAFC /* CLDExpressionTests.m in Sources */, 1E635A8325A7585E0003E9D3 /* UploaderQualityAnalysisTests.swift in Sources */, B60F3C222D4B86AF00160FFC /* TestableCloudinary.swift in Sources */, 1E635A3E25A756870003E9D3 /* ObjcBaseTestCase.m in Sources */, B6B4C49F2C56152700C9B604 /* VideoPreprocessTests.swift in Sources */, 274C6D3323FBAF5E0090BC40 /* UIButtonTests.swift in Sources */, 274C6D3223FBAF5E0090BC40 /* UIBaseTest.swift in Sources */, 5DB2D29924FFE32500001845 /* UploaderWidgetConfigurationTests.m in Sources */, 1E635A7025A757DA0003E9D3 /* BaseMockProvider.swift in Sources */, 274C6D2E23FBAF5E0090BC40 /* PreprocessTests.swift in Sources */, B6C2D4882A72741B00AA0039 /* CLDVideoPlayerTests.swift in Sources */, D731175C24741FBE0051AAFC /* CLDTransformationTests.m in Sources */, 274C6D3423FBAF5E0090BC40 /* UIImageViewTests.swift in Sources */, D7289C81258A9D0B004DBD29 /* UploaderWidgetAssetContainerTests.swift in Sources */, D731175A247415340051AAFC /* CLDConditionExpressionTests.m in Sources */, 272C51B5242B6C360093AB1B /* ParameterEncodingTests.swift in Sources */, 274C6D2F23FBAF5E0090BC40 /* FileUtilsTests.swift in Sources */, D7E79DCA24B515260082288A /* OcrMockProvider.swift in Sources */, 5D4D8C2D246098F900AE9C96 /* CLDConditionExpressionTests.swift in Sources */, D79AE385244CC1D7004F2439 /* CLDTransformationTests.swift in Sources */, 5DE4EC202469919A00F6C8D6 /* CLDTransformationBaselineTests.swift in Sources */, D7289C89258A9E18004DBD29 /* UploaderWidgetVideoDisplayLinkTests.swift in Sources */, 27BC1FAE2431C3F6000AFC2C /* CLDNDataResponse+CloudinaryTests.swift in Sources */, D7F34F3325052FE700C282B1 /* WidgetBaseTest.swift in Sources */, B694AAF32B308C3C00075041 /* VideoEventsTests.swift in Sources */, D7F34F3125052FBD00C282B1 /* UploaderWidgetPreviewViewControllerTests.swift in Sources */, 272C5182242B6C360093AB1B /* RequestTests.swift in Sources */, D7E79DC924B515260082288A /* UploaderOcrTests.swift in Sources */, 272C51B3242B6C360093AB1B /* CLDNError+CloudinaryTests.swift in Sources */, D779B69824B7117300E496EB /* UploaderAccessibilityTests.m in Sources */, D7EF05AC255D8B8E00F93827 /* NetworkBaseTestObjc.m in Sources */, D7289C87258A9E18004DBD29 /* UploaderWidgetVideoControlsTests.swift in Sources */, 272C5185242B6C360093AB1B /* ResponseSerializationTests.swift in Sources */, 272C5183242B6C360093AB1B /* SessionDelegateTests.swift in Sources */, 1E635A8225A7585E0003E9D3 /* ObjcQualityAnalysisExplicitResultParserTests.m in Sources */, 5D53A9522488CE23005C14AB /* CLDConfigurationTests.m in Sources */, D7E79DCB24B515260082288A /* ExplicitMockOcrTests.m in Sources */, D71B7902246808EE004AA28E /* CLDTransformationConditionsTests.swift in Sources */, 274C6D3623FBAF5E0090BC40 /* UploaderTests.swift in Sources */, D7E79DCD24B515260082288A /* UploaderMockOcrTests.swift in Sources */, 272C51B1242B6C360093AB1B /* CacheTests.swift in Sources */, 274C6D3023FBAF5E0090BC40 /* UrlTests.m in Sources */, 272C518C242B6C360093AB1B /* BaseTestCase.swift in Sources */, 5D4D8C2E24609C1400AE9C96 /* ValidationTests.swift in Sources */, 272C51B4242B6C360093AB1B /* ResultTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; D7173491258296CE006F34CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( D7173498258296CE006F34CD /* WidgetUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 607FACE71AFB9204008FA782 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 607FACCF1AFB9204008FA782 /* Cloudinary_Example */; targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */; }; D717349B258296CE006F34CD /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 607FACCF1AFB9204008FA782 /* Cloudinary_Example */; targetProxy = D717349A258296CE006F34CD /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ B64ED1C72B8C933A00BE653E /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( B64ED1C82B8C933A00BE653E /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; B64ED1C92B8C933A00BE653E /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( B64ED1CA2B8C933A00BE653E /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 607FACED1AFB9204008FA782 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 607FACEE1AFB9204008FA782 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 607FACF01AFB9204008FA782 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = DE971235B5CF94F0DA4D461B /* Pods-Cloudinary_Example.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1.0; DEVELOPMENT_TEAM = 3TR999VVAS; INFOPLIST_FILE = Cloudinary/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Cloudinary; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 1.0.0; MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Cloudinary/Cloudinary_Example-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; }; name = Debug; }; 607FACF11AFB9204008FA782 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = E5BA1162DEC8D6E579692502 /* Pods-Cloudinary_Example.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1.0; DEVELOPMENT_TEAM = 3TR999VVAS; INFOPLIST_FILE = Cloudinary/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Cloudinary; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 1.0.0; MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Cloudinary/Cloudinary_Example-Bridging-Header.h"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; }; name = Release; }; 607FACF31AFB9204008FA782 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 2F5D96AD5982EFEBDEE91350 /* Pods-Cloudinary_Tests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; DEVELOPMENT_TEAM = 3TR999VVAS; FRAMEWORK_SEARCH_PATHS = ( "$(PLATFORM_DIR)/Developer/Library/Frameworks", "$(inherited)", ); GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Cloudinary_Example.app/Cloudinary_Example"; }; name = Debug; }; 607FACF41AFB9204008FA782 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = FB8B4986C00FEC3B8BD40223 /* Pods-Cloudinary_Tests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; DEVELOPMENT_TEAM = 3TR999VVAS; FRAMEWORK_SEARCH_PATHS = ( "$(PLATFORM_DIR)/Developer/Library/Frameworks", "$(inherited)", ); INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Cloudinary_Example.app/Cloudinary_Example"; }; name = Release; }; D717349D258296CE006F34CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 69UZJ87W84; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = WidgetUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.demo.WidgetUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_VERSION = 5.0; TEST_TARGET_NAME = Cloudinary_Example; }; name = Debug; }; D717349E258296CE006F34CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 69UZJ87W84; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = WidgetUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.demo.WidgetUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_TARGET_NAME = Cloudinary_Example; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "Cloudinary" */ = { isa = XCConfigurationList; buildConfigurations = ( 607FACED1AFB9204008FA782 /* Debug */, 607FACEE1AFB9204008FA782 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "Cloudinary_Example" */ = { isa = XCConfigurationList; buildConfigurations = ( 607FACF01AFB9204008FA782 /* Debug */, 607FACF11AFB9204008FA782 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "Cloudinary_Tests" */ = { isa = XCConfigurationList; buildConfigurations = ( 607FACF31AFB9204008FA782 /* Debug */, 607FACF41AFB9204008FA782 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D717349C258296CE006F34CD /* Build configuration list for PBXNativeTarget "Cloudinary_UITests" */ = { isa = XCConfigurationList; buildConfigurations = ( D717349D258296CE006F34CD /* Debug */, D717349E258296CE006F34CD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCVersionGroup section */ B64120C92C48EF10005A5495 /* AssetModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( B64120CA2C48EF10005A5495 /* Model.xcdatamodel */, ); currentVersion = B64120CA2C48EF10005A5495 /* Model.xcdatamodel */; path = AssetModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; /* End XCVersionGroup section */ }; rootObject = 607FACC81AFB9204008FA782 /* Project object */; } ================================================ FILE: Example/Cloudinary.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Example/Cloudinary.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Example/Cloudinary.xcodeproj/xcshareddata/xcschemes/Cloudinary-Example.xcscheme ================================================ ================================================ FILE: Example/Cloudinary.xcodeproj/xcshareddata/xcschemes/travis_public_scheme.xcscheme ================================================ ================================================ FILE: Example/Podfile ================================================ platform :ios, '13.0' use_frameworks! target 'Cloudinary_Example' do pod 'Cloudinary', :path => '../' # pod 'Firebase' target 'Cloudinary_Tests' do inherit! :search_paths # For view base tests add FBSnapshotTestCase #pod 'FBSnapshotTestCase' , '~> 2.1.4' end end ================================================ FILE: Example/README.md ================================================ ## Introduction This is a very simple application that integrates the [Cloudinary iOS SDK](https://github.com/cloudinary/cloudinary_ios). The application has the following samples: 1. Delivery 1. Optimization 2. Transform 3. Use cases 2. Upload 1. Upload 2. Upload large 3. Fetch upload 3. UI 1. Upload Widget 2. Image Widget 4. Video 1. Video Widget 2. Video feed ## Installation To use the sample project please go to the Example directory and run the following command: ```bash pod install ``` ## Configuration Once you clone this repository there are two required steps to build the sample app: 1. Configure your Cloudinary cloud name for the app: * Once you open the upload controller you'll be asked to enter your cloud name, you can find your cloud name at the top of your [dashboard.](https://console.cloudinary.com/pm/developer-dashboard) 2. Create an upload preset named 'ios_sample' in your cloudinary account console: * Login to your [Cloudinary console](https://cloudinary.com/console), go to settings>upload, scroll down to Upload Presets and click `Add upload preset`. Alternatively, head directly to the [new preset page](https://console.cloudinary.com/console/upload_presets/new). * Type in `ios_sample` as the name and save, leaving all the fields with their default values. ================================================ FILE: Example/Tests/BaseNetwork/BaseTestCase.swift ================================================ // // BaseTestCase.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class BaseTestCase: XCTestCase { let timeout: TimeInterval = 120.0 static var testDirectoryURL: URL { return FileManager.temporaryDirectoryURL.appendingPathComponent("org.cloudinary.tests") } var testDirectoryURL: URL { return BaseTestCase.testDirectoryURL } override func setUp() { super.setUp() FileManager.removeAllItemsInsideDirectory(at: testDirectoryURL) FileManager.createDirectory(at: testDirectoryURL) } override func setUpWithError() throws { try XCTSkipIf(shouldSkipTest(), "test skipped") try super.setUpWithError() } /** override this method to skip tests when needed */ func shouldSkipTest() -> Bool { return false } func url(forResource fileName: String, withExtension ext: String) -> URL { let bundle = Bundle(for: BaseTestCase.self) return bundle.url(forResource: fileName, withExtension: ext)! } } ================================================ FILE: Example/Tests/BaseNetwork/Core/AuthenticationTests.swift ================================================ // // AuthenticationTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class AuthenticationTestCase: BaseTestCase { let user = "user" let password = "password" var urlString = "" var manager: CLDNSessionManager! override func setUp() { super.setUp() manager = CLDNSessionManager(configuration: .default) // Clear out credentials let credentialStorage = URLCredentialStorage.shared for (protectionSpace, credentials) in credentialStorage.allCredentials { for (_, credential) in credentials { credentialStorage.remove(credential, for: protectionSpace) } } // Clear out cookies let cookieStorage = HTTPCookieStorage.shared cookieStorage.cookies?.forEach { cookieStorage.deleteCookie($0) } } } // MARK: - class BasicAuthenticationTestCase: AuthenticationTestCase { override func setUp() { super.setUp() urlString = "https://httpbin.org/" } func skipped_testHTTPBasicAuthenticationWithInvalidCredentials() { // Given let expectation = self.expectation(description: "\(urlString) 401") var response: CLDNDefaultDataResponse? // When let credentials = "\(user):\(password)".data(using: .utf8)?.base64EncodedString() manager.request(urlString, headers: ["Authorization": "Basic \(credentials ?? "")"]) .authenticate(user: "invalid", password: "credentials") .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertEqual(response?.response?.statusCode, 401) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) } func testHTTPBasicAuthenticationWithValidCredentials() { // Given let expectation = self.expectation(description: "\(urlString) 200") var response: CLDNDefaultDataResponse? // When manager.request(urlString) .authenticate(user: user, password: password) .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertEqual(response?.response?.statusCode, 200) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) } func testHiddenHTTPBasicAuthentication() { // Given let urlString = "https://httpbin.org/" let expectation = self.expectation(description: "\(urlString) 200") var headers: CLDNHTTPHeaders? if let authorizationHeader = CLDNRequest.authorizationHeader(user: user, password: password) { headers = [authorizationHeader.key: authorizationHeader.value] } var response: CLDNDefaultDataResponse? // When manager.request(urlString, headers: headers) .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertEqual(response?.response?.statusCode, 200) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) } } // MARK: - class HTTPDigestAuthenticationTestCase: AuthenticationTestCase { let qop = "auth" override func setUp() { super.setUp() urlString = "https://httpbin.org/" } func skipped_testHTTPDigestAuthenticationWithInvalidCredentials() { // Given let expectation = self.expectation(description: "\(urlString) 401") var response: CLDNDefaultDataResponse? // When manager.request(urlString) .authenticate(user: "invalid", password: "credentials") .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertEqual(response?.response?.statusCode, 401) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) } func testHTTPDigestAuthenticationWithValidCredentials() { // Given let expectation = self.expectation(description: "\(urlString) 200") var response: CLDNDefaultDataResponse? // When manager.request(urlString) .authenticate(user: user, password: password) .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertEqual(response?.response?.statusCode, 200) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) } } ================================================ FILE: Example/Tests/BaseNetwork/Core/CLDCloudinaryTests.m ================================================ // // CLDCloudinaryTests.m // // Copyright (c) 2021 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "Cloudinary_Tests-Swift.h" #import "NetworkBaseTestObjc.h" @interface CLDCustomAdapter: NSObject @end @implementation CLDCustomAdapter - (id _Nonnull)cloudinaryRequest:(NSString * _Nonnull)url headers:(NSDictionary * _Nonnull)headers parameters:(NSDictionary * _Nonnull)parameters {return nil;} - (id _Nonnull)downloadFromCloudinary:(NSString * _Nonnull)url {return nil;} - (void (^ _Nullable)(void))getBackgroundCompletionHandler {return nil;} - (void)setBackgroundCompletionHandler:(void (^ _Nullable)(void))newValue {} - (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads {} - (id _Nonnull)uploadToCloudinary:(NSString * _Nonnull)url headers:(NSDictionary * _Nonnull)headers parameters:(NSDictionary * _Nonnull)parameters data:(id _Nonnull)data {return nil;} @end @interface CLDCloudinaryTests: XCTestCase @property (nonatomic, strong, nullable) CLDCloudinary* sut; @property (nonatomic, strong, nullable) CLDConfiguration* config; @end @implementation CLDCloudinaryTests // MARK: - setup and teardown - (void)setUp { [super setUp]; self.config = [[CLDConfiguration alloc] initWithCloudinaryUrl:@"cloudinary://a:b@test123"]; self.sut = [[CLDCloudinary alloc] initWithConfiguration: self.config networkAdapter:nil sessionConfiguration:nil]; } - (void)tearDown { self.sut = nil; self.config = nil; [super tearDown]; } // MARK: - init - (void)test_init_config_shouldStoreProperties { // Given NSString* cloudinaryUrl = @"cloudinary://a:b@test123"; CLDConfiguration* tempConfiguration = [[CLDConfiguration alloc] initWithCloudinaryUrl:cloudinaryUrl]; // When CLDCloudinary * tempSut = [[CLDCloudinary alloc] initWithConfiguration:tempConfiguration networkAdapter:nil sessionConfiguration:nil]; // Then XCTAssertNotNil(tempSut, "initialized object should not be nil"); XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value"); } - (void)test_init_configSession_shouldStoreProperties { // Given NSString * cloudinaryUrl = @"cloudinary://a:b@test123"; NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; CLDConfiguration * tempConfiguration = [[CLDConfiguration alloc] initWithCloudinaryUrl:cloudinaryUrl]; // When CLDCloudinary* tempSut = [[CLDCloudinary alloc] initWithConfiguration:tempConfiguration networkAdapter:nil sessionConfiguration:sessionConfig]; // Then XCTAssertNotNil(tempSut, "initialized object should not be nil"); XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value"); } - (void)test_init_configDownloadSession_shouldStoreProperties { // Given NSString * cloudinaryUrl = @"cloudinary://a:b@test123"; NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; CLDConfiguration * tempConfiguration = [[CLDConfiguration alloc] initWithCloudinaryUrl:cloudinaryUrl]; // When CLDCloudinary* tempSut = [[CLDCloudinary alloc] initWithConfiguration:tempConfiguration networkAdapter:nil downloadAdapter:nil sessionConfiguration:sessionConfig downloadSessionConfiguration:sessionConfig]; // Then XCTAssertNotNil(tempSut, "initialized object should not be nil"); XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value"); } - (void)test_init_configDownloadSessionAdapter_shouldStoreProperties { // Given NSString * cloudinaryUrl = @"cloudinary://a:b@test123"; NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; CLDConfiguration * tempConfiguration = [[CLDConfiguration alloc] initWithCloudinaryUrl:cloudinaryUrl]; CLDCustomAdapter * adapter = [[CLDCustomAdapter alloc] init]; // When CLDCloudinary* tempSut = [[CLDCloudinary alloc] initWithConfiguration:tempConfiguration networkAdapter:adapter downloadAdapter:nil sessionConfiguration:sessionConfig downloadSessionConfiguration:sessionConfig]; // Then XCTAssertNotNil(tempSut, "initialized object should not be nil"); XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value"); } - (void)test_init_configDownloadSessionDownloadAdapter_shouldStoreProperties { // Given NSString * cloudinaryUrl = @"cloudinary://a:b@test123"; NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; CLDConfiguration * tempConfiguration = [[CLDConfiguration alloc] initWithCloudinaryUrl:cloudinaryUrl]; CLDCustomAdapter * adapter = [[CLDCustomAdapter alloc] init]; // When CLDCloudinary* tempSut = [[CLDCloudinary alloc] initWithConfiguration:tempConfiguration networkAdapter:adapter downloadAdapter:adapter sessionConfiguration:sessionConfig downloadSessionConfiguration:sessionConfig]; // Then XCTAssertNotNil(tempSut, "initialized object should not be nil"); XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value"); } - (void)test_init_configSessionAdapter_shouldStoreProperties { // Given NSString * cloudinaryUrl = @"cloudinary://a:b@test123"; NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; CLDConfiguration * tempConfiguration = [[CLDConfiguration alloc] initWithCloudinaryUrl:cloudinaryUrl]; CLDCustomAdapter * adapter = [[CLDCustomAdapter alloc] init]; // When CLDCloudinary* tempSut = [[CLDCloudinary alloc] initWithConfiguration:tempConfiguration networkAdapter:adapter sessionConfiguration:sessionConfig]; // Then XCTAssertNotNil(tempSut, "initialized object should not be nil"); XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value"); } @end ================================================ FILE: Example/Tests/BaseNetwork/Core/CLDCloudinaryTests.swift ================================================ // // CLDCloudinaryTests.swift // // Copyright (c) 2021 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class CLDCloudinaryTests: XCTestCase { var sut : CLDCloudinary! var config: CLDConfiguration! override func setUp() { super.setUp() config = CLDConfiguration(cloudinaryUrl: "cloudinary://a:b@test123")! sut = CLDCloudinary(configuration: config, sessionConfiguration: .default) } override func tearDownWithError() throws { sut = nil config = nil super.tearDown() } // MARK: - init func test_init_config_shouldStoreProperties() { // Given let cloudinaryUrl = "cloudinary://a:b@test123" let tempConfiguration = CLDConfiguration(cloudinaryUrl: cloudinaryUrl)! // When let tempSut = CLDCloudinary(configuration: tempConfiguration) // Then XCTAssertNotNil(tempSut, "initialized object should not be nil") XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value") } func test_init_configSession_shouldStoreProperties() { // Given let cloudinaryUrl = "cloudinary://a:b@test123" let tempConfiguration = CLDConfiguration(cloudinaryUrl: cloudinaryUrl)! // When let tempSut = CLDCloudinary(configuration: tempConfiguration, sessionConfiguration: .default) // Then XCTAssertNotNil(tempSut, "initialized object should not be nil") XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value") } func test_init_configDownloadSession_shouldStoreProperties() { // Given let cloudinaryUrl = "cloudinary://a:b@test123" let tempConfiguration = CLDConfiguration(cloudinaryUrl: cloudinaryUrl)! // When let tempSut = CLDCloudinary(configuration: tempConfiguration, sessionConfiguration: .default, downloadSessionConfiguration: .default) // Then XCTAssertNotNil(tempSut, "initialized object should not be nil") XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value") } func test_init_configDownloadSessionAdapter_shouldStoreProperties() { // Given let adapter = CLDDefaultNetworkAdapter() let cloudinaryUrl = "cloudinary://a:b@test123" let tempConfiguration = CLDConfiguration(cloudinaryUrl: cloudinaryUrl)! // When let tempSut = CLDCloudinary(configuration: tempConfiguration, networkAdapter: adapter, downloadAdapter: nil, sessionConfiguration: .default, downloadSessionConfiguration: .default) // Then XCTAssertNotNil(tempSut, "initialized object should not be nil") XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value") } func test_init_configDownloadSessionDownloadAdapter_shouldStoreProperties() { // Given let adapter = CLDDefaultNetworkAdapter() let cloudinaryUrl = "cloudinary://a:b@test123" let tempConfiguration = CLDConfiguration(cloudinaryUrl: cloudinaryUrl)! // When let tempSut = CLDCloudinary(configuration: tempConfiguration, networkAdapter: adapter, downloadAdapter: adapter, sessionConfiguration: .default, downloadSessionConfiguration: .default) // Then XCTAssertNotNil(tempSut, "initialized object should not be nil") XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value") } func test_init_configSessionAdapter_shouldStoreProperties() { // Given let adapter = CLDDefaultNetworkAdapter() let cloudinaryUrl = "cloudinary://a:b@test123" let tempConfiguration = CLDConfiguration(cloudinaryUrl: cloudinaryUrl)! // When let tempSut = CLDCloudinary(configuration: tempConfiguration, networkAdapter: adapter, sessionConfiguration: .default) // Then XCTAssertNotNil(tempSut, "initialized object should not be nil") XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value") } func test_init_configSessionAdapter_shouldStoreExtraHeaders() { // Given let extraHeaders = ["Test": "Test"] var adapter = CLDDefaultNetworkAdapter() let cloudinaryUrl = "cloudinary://a:b@test123" let tempConfiguration = CLDConfiguration(cloudinaryUrl: cloudinaryUrl)! // When let tempSut = CLDCloudinary(configuration: tempConfiguration, networkAdapter: adapter, sessionConfiguration: .default) tempSut.setExtraHeaderes(extraHeaders) // Then XCTAssertNotNil(tempSut, "initialized object should not be nil") XCTAssertEqual (tempSut.config, tempConfiguration, "Initilized object should contain expected value") } } ================================================ FILE: Example/Tests/BaseNetwork/Core/ParameterEncodingTests.swift ================================================ // // ParameterEncodingTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class ParameterEncodingTestCase: BaseTestCase { let urlRequest = URLRequest(url: URL(string: "https://example.com/")!) internal func checkParamsEncodedCorrectly(params: [String: Any], encoding: CLDNURLEncoding = CLDNURLEncoding.default) throws { var request = urlRequest request.httpMethod = CLDNHTTPMethod.post.rawValue let encodedURLRequest = try encoding.CLDN_Encode(request, with: params) XCTAssertNotNil(encodedURLRequest.httpBody, "HTTPBody should not be nil") let queryParams = params.map({ encoding.queryComponents(fromKey: $0, value: $1) }).reduce([], +) if let httpBody = encodedURLRequest.httpBody, let decodedHTTPBody = String(data: httpBody, encoding: .utf8) { for query in queryParams { XCTAssert(decodedHTTPBody.contains("\(query.0)=\(query.1)")) } } else { XCTFail("decoded http body should not be nil") } } } // MARK: - class URLParameterEncodingTestCase: ParameterEncodingTestCase { // MARK: Properties let encoding = CLDNURLEncoding.default // MARK: Tests - Parameter Types func testURLParameterEncodeNilParameters() { do { // Given, When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: nil) // Then XCTAssertNil(urlRequest.url?.query) } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeEmptyDictionaryParameter() { do { // Given let parameters: [String: Any] = [:] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertNil(urlRequest.url?.query) } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeOneStringKeyStringValueParameter() { do { // Given let parameters = ["foo": "bar"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "foo=bar") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeOneStringKeyStringValueParameterAppendedToQuery() { do { // Given var mutableURLRequest = self.urlRequest var urlComponents = URLComponents(url: mutableURLRequest.url!, resolvingAgainstBaseURL: false)! urlComponents.query = "baz=qux" mutableURLRequest.url = urlComponents.url let parameters = ["foo": "bar"] // When let urlRequest = try encoding.CLDN_Encode(mutableURLRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "baz=qux&foo=bar") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeTwoStringKeyStringValueParameters() { do { // Given let parameters = ["foo": "bar", "baz": "qux"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "baz=qux&foo=bar") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyNSNumberIntegerValueParameter() { do { // Given let parameters = ["foo": NSNumber(value: 25)] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "foo=25") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyNSNumberBoolValueParameter() { do { // Given let parameters = ["foo": NSNumber(value: false)] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "foo=0") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyIntegerValueParameter() { do { // Given let parameters = ["foo": 1] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "foo=1") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyDoubleValueParameter() { do { // Given let parameters = ["foo": 1.1] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "foo=1.1") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyBoolValueParameter() { do { // Given let parameters = ["foo": true] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "foo=1") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyArrayValueParameter() { do { // Given let parameters = ["foo": ["a", 1, true]] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "foo%5B%5D=a&foo%5B%5D=1&foo%5B%5D=1") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyArrayValueParameterWithoutBrackets() { do { // Given let encoding = CLDNURLEncoding(arrayEncoding: .noBrackets) let parameters = ["foo": ["a", 1, true]] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "foo=a&foo=1&foo=1") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyDictionaryValueParameter() { do { // Given let parameters = ["foo": ["bar": 1]] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "foo%5Bbar%5D=1") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyNestedDictionaryValueParameter() { do { // Given let parameters = ["foo": ["bar": ["baz": 1]]] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "foo%5Bbar%5D%5Bbaz%5D=1") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyNestedDictionaryArrayValueParameter() { do { // Given let parameters = ["foo": ["bar": ["baz": ["a", 1, true]]]] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then let expectedQuery = "foo%5Bbar%5D%5Bbaz%5D%5B%5D=a&foo%5Bbar%5D%5Bbaz%5D%5B%5D=1&foo%5Bbar%5D%5Bbaz%5D%5B%5D=1" XCTAssertEqual(urlRequest.url?.query, expectedQuery) } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyNestedDictionaryArrayValueParameterWithoutBrackets() { do { // Given let encoding = CLDNURLEncoding(arrayEncoding: .noBrackets) let parameters = ["foo": ["bar": ["baz": ["a", 1, true]]]] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then let expectedQuery = "foo%5Bbar%5D%5Bbaz%5D=a&foo%5Bbar%5D%5Bbaz%5D=1&foo%5Bbar%5D%5Bbaz%5D=1" XCTAssertEqual(urlRequest.url?.query, expectedQuery) } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterLiteralBoolEncodingWorksAndDoesNotAffectNumbers() { do { // Given let encoding = CLDNURLEncoding(boolEncoding: .literal) let parameters: [String: Any] = [ // Must still encode to numbers "a": 1, "b": 0, "c": 1.0, "d": 0.0, "e": NSNumber(value: 1), "f": NSNumber(value: 0), "g": NSNumber(value: 1.0), "h": NSNumber(value: 0.0), // Must encode to literals "i": true, "j": false, "k": NSNumber(value: true), "l": NSNumber(value: false) ] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "a=1&b=0&c=1&d=0&e=1&f=0&g=1&h=0&i=true&j=false&k=true&l=false") } catch { XCTFail("Test encountered unexpected error: \(error)") } } // MARK: Tests - All Reserved / Unreserved / Illegal Characters According to RFC 3986 func testThatReservedCharactersArePercentEscapedMinusQuestionMarkAndForwardSlash() { do { // Given let generalDelimiters = ":#[]@" let subDelimiters = "!$&'()*+,;=" let parameters = ["reserved": "\(generalDelimiters)\(subDelimiters)"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then let expectedQuery = "reserved=%3A%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D" XCTAssertEqual(urlRequest.url?.query, expectedQuery) } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testThatReservedCharactersQuestionMarkAndForwardSlashAreNotPercentEscaped() { do { // Given let parameters = ["reserved": "?/"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "reserved=?/") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testThatUnreservedNumericCharactersAreNotPercentEscaped() { do { // Given let parameters = ["numbers": "0123456789"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "numbers=0123456789") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testThatUnreservedLowercaseCharactersAreNotPercentEscaped() { do { // Given let parameters = ["lowercase": "abcdefghijklmnopqrstuvwxyz"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "lowercase=abcdefghijklmnopqrstuvwxyz") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testThatUnreservedUppercaseCharactersAreNotPercentEscaped() { do { // Given let parameters = ["uppercase": "ABCDEFGHIJKLMNOPQRSTUVWXYZ"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "uppercase=ABCDEFGHIJKLMNOPQRSTUVWXYZ") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testThatIllegalASCIICharactersArePercentEscaped() { do { // Given let parameters = ["illegal": " \"#%<>[]\\^`{}|"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then let expectedQuery = "illegal=%20%22%23%25%3C%3E%5B%5D%5C%5E%60%7B%7D%7C" XCTAssertEqual(urlRequest.url?.query, expectedQuery) } catch { XCTFail("Test encountered unexpected error: \(error)") } } // MARK: Tests - Special Character Queries func testURLParameterEncodeStringWithAmpersandKeyStringWithAmpersandValueParameter() { do { // Given let parameters = ["foo&bar": "baz&qux", "foobar": "bazqux"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "foo%26bar=baz%26qux&foobar=bazqux") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringWithQuestionMarkKeyStringWithQuestionMarkValueParameter() { do { // Given let parameters = ["?foo?": "?bar?"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "?foo?=?bar?") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringWithSlashKeyStringWithQuestionMarkValueParameter() { do { // Given let parameters = ["foo": "/bar/baz/qux"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "foo=/bar/baz/qux") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringWithSpaceKeyStringWithSpaceValueParameter() { do { // Given let parameters = [" foo ": " bar "] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "%20foo%20=%20bar%20") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringWithPlusKeyStringWithPlusValueParameter() { do { // Given let parameters = ["+foo+": "+bar+"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "%2Bfoo%2B=%2Bbar%2B") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyPercentEncodedStringValueParameter() { do { // Given let parameters = ["percent": "%25"] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "percent=%2525") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringKeyNonLatinStringValueParameter() { do { // Given let parameters = [ "french": "français", "japanese": "日本語", "arabic": "العربية", "emoji": "😃" ] // When let urlRequest = try encoding.CLDN_Encode(self.urlRequest, with: parameters) // Then let expectedParameterValues = [ "arabic=%D8%A7%D9%84%D8%B9%D8%B1%D8%A8%D9%8A%D8%A9", "emoji=%F0%9F%98%83", "french=fran%C3%A7ais", "japanese=%E6%97%A5%E6%9C%AC%E8%AA%9E" ] let expectedQuery = expectedParameterValues.joined(separator: "&") XCTAssertEqual(urlRequest.url?.query, expectedQuery) } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringForRequestWithPrecomposedQuery() { do { // Given let url = URL(string: "https://example.com/movies?hd=[1]")! let parameters = ["page": "0"] // When let urlRequest = try encoding.CLDN_Encode(URLRequest(url: url), with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "hd=%5B1%5D&page=0") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringWithPlusKeyStringWithPlusValueParameterForRequestWithPrecomposedQuery() { do { // Given let url = URL(string: "https://example.com/movie?hd=[1]")! let parameters = ["+foo+": "+bar+"] // When let urlRequest = try encoding.CLDN_Encode(URLRequest(url: url), with: parameters) // Then XCTAssertEqual(urlRequest.url?.query, "hd=%5B1%5D&%2Bfoo%2B=%2Bbar%2B") } catch { XCTFail("Test encountered unexpected error: \(error)") } } func testURLParameterEncodeStringWithThousandsOfChineseCharacters() { do { // Given let repeatedCount = 2_000 let url = URL(string: "https://example.com/movies")! let parameters = ["chinese": String(repeating: "一二三四五六七八九十", count: repeatedCount)] // When let urlRequest = try encoding.CLDN_Encode(URLRequest(url: url), with: parameters) // Then var expected = "chinese=" for _ in 0.. URLRequest { throw AuthenticationError.expiredAccessToken } } private var sessionManager: CLDNSessionManager! override func setUp() { super.setUp() sessionManager = CLDNSessionManager() sessionManager.startRequestsImmediately = false sessionManager.adapter = AuthenticationAdapter() } func testDataRequestHasURLRequest() { // Given let urlString = "https://httpbin.org/" // When let request = sessionManager.request(urlString) // Then XCTAssertNotNil(request.request) XCTAssertEqual(request.request?.httpMethod, "GET") XCTAssertEqual(request.request?.url?.absoluteString, urlString) XCTAssertNil(request.response) } func testUploadDataRequestHasURLRequest() { // Given let urlString = "https://httpbin.org/" // When let request = sessionManager.upload(Data(), to: urlString) // Then XCTAssertNotNil(request.request) XCTAssertEqual(request.request?.httpMethod, "POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString) XCTAssertNil(request.response) } func testUploadFileRequestHasURLRequest() { // Given let urlString = "https://httpbin.org/" let imageURL = url(forResource: "rainbow", withExtension: "jpg") // When let request = sessionManager.upload(imageURL, to: urlString) // Then XCTAssertNotNil(request.request) XCTAssertEqual(request.request?.httpMethod, "POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString) XCTAssertNil(request.response) } func testUploadStreamRequestHasURLRequest() { // Given let urlString = "https://httpbin.org/" let imageURL = url(forResource: "rainbow", withExtension: "jpg") let imageStream = InputStream(url: imageURL)! // When let request = sessionManager.upload(imageStream, to: urlString) // Then XCTAssertNotNil(request.request) XCTAssertEqual(request.request?.httpMethod, "POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString) XCTAssertNil(request.response) } } // MARK: - class RequestResponseTestCase: BaseTestCase { func testRequestResponse() { // Given let urlString = "https://httpbin.org/get" let expectation = self.expectation(description: "GET request should succeed: \(urlString)") var response: CLDNDefaultDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]) .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) } func testRequestResponseWithProgress() { // Given let randomBytes = 4 * 1024 * 1024 let urlString = "https://httpbin.org/bytes/\(randomBytes)" let expectation = self.expectation(description: "Bytes download progress should be reported: \(urlString)") var progressValues: [Double] = [] var response: CLDNDefaultDataResponse? // When CLDNSessionManager.default.request(urlString) .downloadProgress { progress in progressValues.append(progress.fractionCompleted) } .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) var previousProgress: Double = progressValues.first ?? 0.0 for progress in progressValues { XCTAssertGreaterThanOrEqual(progress, previousProgress) previousProgress = progress } if let lastProgressValue = progressValues.last { XCTAssertEqual(lastProgressValue, 1.0) } else { XCTFail("last item in progressValues should not be nil") } } func testRequestResponseWithStream() { // Given let randomBytes = 4 * 1024 * 1024 let urlString = "https://httpbin.org/bytes/\(randomBytes)" let expectation = self.expectation(description: "Bytes download progress should be reported: \(urlString)") var progressValues: [Double] = [] var accumulatedData = [Data]() var response: CLDNDefaultDataResponse? // When CLDNSessionManager.default.request(urlString) .downloadProgress { progress in progressValues.append(progress.fractionCompleted) } .stream { data in accumulatedData.append(data) } .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNil(response?.data) XCTAssertNil(response?.error) XCTAssertGreaterThanOrEqual(accumulatedData.count, 1) var previousProgress: Double = progressValues.first ?? 0.0 for progress in progressValues { XCTAssertGreaterThanOrEqual(progress, previousProgress) previousProgress = progress } if let lastProgress = progressValues.last { XCTAssertEqual(lastProgress, 1.0) } else { XCTFail("last item in progressValues should not be nil") } } func testPOSTRequestWithUnicodeParameters() { // Given let urlString = "https://httpbin.org/post" let parameters = [ "french": "français", "japanese": "日本語", "arabic": "العربية", "emoji": "😃" ] let expectation = self.expectation(description: "request should succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, method: .post, parameters: parameters) .responseJSON { closureResponse in response = closureResponse expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) if let json = response?.result.value as? [String: Any], let form = json["form"] as? [String: String] { XCTAssertEqual(form["french"], parameters["french"]) XCTAssertEqual(form["japanese"], parameters["japanese"]) XCTAssertEqual(form["arabic"], parameters["arabic"]) XCTAssertEqual(form["emoji"], parameters["emoji"]) } else { XCTFail("form parameter in JSON should not be nil") } } func testPOSTRequestWithBase64EncodedImages() { // Given let urlString = "https://httpbin.org/post" let pngBase64EncodedString: String = { let URL = url(forResource: "unicorn", withExtension: "png") let data = try! Data(contentsOf: URL) return data.base64EncodedString(options: .lineLength64Characters) }() let jpegBase64EncodedString: String = { let URL = url(forResource: "rainbow", withExtension: "jpg") let data = try! Data(contentsOf: URL) return data.base64EncodedString(options: .lineLength64Characters) }() let parameters = [ "email": "user@alamofire.org", "png_image": pngBase64EncodedString, "jpeg_image": jpegBase64EncodedString ] let expectation = self.expectation(description: "request should succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, method: .post, parameters: parameters) .responseJSON { closureResponse in response = closureResponse expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) if let json = response?.result.value as? [String: Any], let form = json["form"] as? [String: String] { XCTAssertEqual(form["email"], parameters["email"]) XCTAssertEqual(form["png_image"], parameters["png_image"]) XCTAssertEqual(form["jpeg_image"], parameters["jpeg_image"]) } else { XCTFail("form parameter in JSON should not be nil") } } } // MARK: - extension CLDNRequest { fileprivate func preValidate(operation: @escaping () -> Void) -> Self { delegate.queue.addOperation { operation() } return self } fileprivate func postValidate(operation: @escaping () -> Void) -> Self { delegate.queue.addOperation { operation() } return self } } // MARK: - class RequestExtensionTestCase: BaseTestCase { func testThatRequestExtensionHasAccessToTaskDelegateQueue() { // Given let urlString = "https://httpbin.org/get" let expectation = self.expectation(description: "GET request should succeed: \(urlString)") var responses: [String] = [] // When CLDNSessionManager.default.request(urlString) .preValidate { responses.append("preValidate") } .validate() .postValidate { responses.append("postValidate") } .response { _ in responses.append("response") expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then if responses.count == 3 { XCTAssertEqual(responses[0], "preValidate") XCTAssertEqual(responses[1], "postValidate") XCTAssertEqual(responses[2], "response") } else { XCTFail("responses count should be equal to 3") } } } // MARK: - class RequestDescriptionTestCase: BaseTestCase { func testRequestDescription() { // Given let urlString = "https://httpbin.org/get" let request = CLDNSessionManager.default.request(urlString) let initialRequestDescription = request.description let expectation = self.expectation(description: "Request description should update: \(urlString)") var finalRequestDescription: String? var response: HTTPURLResponse? // When request.response { resp in finalRequestDescription = request.description response = resp.response expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertEqual(initialRequestDescription, "GET https://httpbin.org/get") XCTAssertEqual(finalRequestDescription, "GET https://httpbin.org/get (\(response?.statusCode ?? -1))") } } // MARK: - class RequestDebugDescriptionTestCase: BaseTestCase { // MARK: Properties let manager: CLDNSessionManager = { let manager = CLDNSessionManager(configuration: .default) manager.startRequestsImmediately = false return manager }() let managerWithAcceptLanguageHeader: CLDNSessionManager = { var headers = CLDNSessionManager.default.session.configuration.httpAdditionalHeaders ?? [:] headers["Accept-Language"] = "en-US" let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = headers let manager = CLDNSessionManager(configuration: configuration) manager.startRequestsImmediately = false return manager }() let managerWithContentTypeHeader: CLDNSessionManager = { var headers = CLDNSessionManager.default.session.configuration.httpAdditionalHeaders ?? [:] headers["Content-Type"] = "application/json" let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = headers let manager = CLDNSessionManager(configuration: configuration) manager.startRequestsImmediately = false return manager }() let managerDisallowingCookies: CLDNSessionManager = { let configuration = URLSessionConfiguration.default configuration.httpShouldSetCookies = false let manager = CLDNSessionManager(configuration: configuration) manager.startRequestsImmediately = false return manager }() // MARK: Tests func testGETRequestDebugDescription() { // Given let urlString = "https://httpbin.org/get" // When let request = manager.request(urlString) let components = cURLCommandComponents(for: request) // Then XCTAssertEqual(components[0..<3], ["$", "curl", "-v"]) XCTAssertFalse(components.contains("-X")) XCTAssertEqual(components.last, "\"\(urlString)\"") } func testGETRequestWithJSONHeaderDebugDescription() { // Given let urlString = "https://httpbin.org/get" // When let headers: [String: String] = [ "X-Custom-Header": "{\"key\": \"value\"}" ] let request = manager.request(urlString, headers: headers) // Then XCTAssertNotNil(request.debugDescription.range(of: "-H \"X-Custom-Header: {\\\"key\\\": \\\"value\\\"}\"")) } func testGETRequestWithDuplicateHeadersDebugDescription() { // Given let urlString = "https://httpbin.org/get" // When let headers = [ "Accept-Language": "en-GB" ] let request = managerWithAcceptLanguageHeader.request(urlString, headers: headers) let components = cURLCommandComponents(for: request) // Then XCTAssertEqual(components[0..<3], ["$", "curl", "-v"]) XCTAssertFalse(components.contains("-X")) XCTAssertEqual(components.last, "\"\(urlString)\"") let tokens = request.debugDescription.components(separatedBy: "Accept-Language:") XCTAssertTrue(tokens.count == 2, "command should contain a single Accept-Language header") XCTAssertNotNil(request.debugDescription.range(of: "-H \"Accept-Language: en-GB\"")) } func testPOSTRequestDebugDescription() { // Given let urlString = "https://httpbin.org/post" // When let request = manager.request(urlString, method: .post) let components = cURLCommandComponents(for: request) // Then XCTAssertEqual(components[0..<3], ["$", "curl", "-v"]) XCTAssertEqual(components[3..<5], ["-X", "POST"]) XCTAssertEqual(components.last, "\"\(urlString)\"") } func testPOSTRequestWithJSONParametersDebugDescription() { // Given let urlString = "https://httpbin.org/post" let parameters = [ "foo": "bar", "fo\"o": "b\"ar", "f'oo": "ba'r" ] // When let request = manager.request(urlString, method: .post, parameters: parameters, encoding: CLDNJSONEncoding.default) let components = cURLCommandComponents(for: request) // Then XCTAssertEqual(components[0..<3], ["$", "curl", "-v"]) XCTAssertEqual(components[3..<5], ["-X", "POST"]) XCTAssertNotNil(request.debugDescription.range(of: "-H \"Content-Type: application/json\"")) XCTAssertNotNil(request.debugDescription.range(of: "-d \"{")) XCTAssertNotNil(request.debugDescription.range(of: "\\\"f'oo\\\":\\\"ba'r\\\"")) XCTAssertNotNil(request.debugDescription.range(of: "\\\"fo\\\\\\\"o\\\":\\\"b\\\\\\\"ar\\\"")) XCTAssertNotNil(request.debugDescription.range(of: "\\\"foo\\\":\\\"bar\\")) XCTAssertEqual(components.last, "\"\(urlString)\"") } func testPOSTRequestWithCookieDebugDescription() { // Given let urlString = "https://httpbin.org/post" let properties = [ HTTPCookiePropertyKey.domain: "httpbin.org", HTTPCookiePropertyKey.path: "/post", HTTPCookiePropertyKey.name: "foo", HTTPCookiePropertyKey.value: "bar", ] let cookie = HTTPCookie(properties: properties)! manager.session.configuration.httpCookieStorage?.setCookie(cookie) // When let request = manager.request(urlString, method: .post) let components = cURLCommandComponents(for: request) // Then XCTAssertEqual(components[0..<3], ["$", "curl", "-v"]) XCTAssertEqual(components[3..<5], ["-X", "POST"]) XCTAssertEqual(components.last, "\"\(urlString)\"") XCTAssertEqual(components[5..<6], ["-b"]) } func testPOSTRequestWithCookiesDisabledDebugDescription() { // Given let urlString = "https://httpbin.org/post" let properties = [ HTTPCookiePropertyKey.domain: "httpbin.org", HTTPCookiePropertyKey.path: "/post", HTTPCookiePropertyKey.name: "foo", HTTPCookiePropertyKey.value: "bar", ] let cookie = HTTPCookie(properties: properties)! managerDisallowingCookies.session.configuration.httpCookieStorage?.setCookie(cookie) // When let request = managerDisallowingCookies.request(urlString, method: .post) let components = cURLCommandComponents(for: request) // Then let cookieComponents = components.filter { $0 == "-b" } XCTAssertTrue(cookieComponents.isEmpty) } func testMultipartFormDataRequestWithDuplicateHeadersDebugDescription() { // Given let urlString = "https://httpbin.org/post" let japaneseData = "日本語".data(using: .utf8, allowLossyConversion: false)! let expectation = self.expectation(description: "multipart form data encoding should succeed") var request: CLDNRequest? var components: [String] = [] // When managerWithContentTypeHeader.upload( multipartFormData: { multipartFormData in multipartFormData.append(japaneseData, withName: "japanese") }, to: urlString, encodingCompletion: { result in switch result { case .success(let upload, _, _): request = upload components = self.cURLCommandComponents(for: upload) expectation.fulfill() case .failure: expectation.fulfill() } } ) waitForExpectations(timeout: timeout, handler: nil) debugPrint(request!) // Then XCTAssertEqual(components[0..<3], ["$", "curl", "-v"]) XCTAssertTrue(components.contains("-X")) XCTAssertEqual(components.last, "\"\(urlString)\"") let tokens = request.debugDescription.components(separatedBy: "Content-Type:") XCTAssertTrue(tokens.count == 2, "command should contain a single Content-Type header") XCTAssertNotNil(request.debugDescription.range(of: "-H \"Content-Type: multipart/form-data;")) } func testThatRequestWithInvalidURLDebugDescription() { // Given let urlString = "invalid_url" // When let request = manager.request(urlString) let debugDescription = request.debugDescription // Then XCTAssertNotNil(debugDescription, "debugDescription should not crash") } // MARK: Test Helper Methods private func cURLCommandComponents(for request: CLDNRequest) -> [String] { let whitespaceCharacterSet = CharacterSet.whitespacesAndNewlines return request.debugDescription .components(separatedBy: whitespaceCharacterSet) .filter { $0 != "" && $0 != "\\" } } } ================================================ FILE: Example/Tests/BaseNetwork/Core/ResponseTests.swift ================================================ // // ResponseTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class ResponseTestCase: BaseTestCase { func testThatResponseReturnsSuccessResultWithValidData() { // Given let urlString = "https://httpbin.org/get" let expectation = self.expectation(description: "request should succeed") var response: CLDNDefaultDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } func testThatResponseReturnsFailureResultWithOptionalDataAndError() { // Given let urlString = "https://invalid-url-here.org/this/does/not/exist" let expectation = self.expectation(description: "request should fail with 404") var response: CLDNDefaultDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNotNil(response?.error) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } } // MARK: - class ResponseDataTestCase: BaseTestCase { func testThatResponseDataReturnsSuccessResultWithValidData() { // Given let urlString = "https://httpbin.org/get" let expectation = self.expectation(description: "request should succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).responseData { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } func testThatResponseDataReturnsFailureResultWithOptionalDataAndError() { // Given let urlString = "https://invalid-url-here.org/this/does/not/exist" let expectation = self.expectation(description: "request should fail with 404") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).responseData { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } } // MARK: - class ResponseStringTestCase: BaseTestCase { func testThatResponseStringReturnsSuccessResultWithValidString() { // Given let urlString = "https://httpbin.org/get" let expectation = self.expectation(description: "request should succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).responseString { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } func testThatResponseStringReturnsFailureResultWithOptionalDataAndError() { // Given let urlString = "https://invalid-url-here.org/this/does/not/exist" let expectation = self.expectation(description: "request should fail with 404") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).responseString { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } } // MARK: - class ResponseJSONTestCase: BaseTestCase { func testThatResponseJSONReturnsSuccessResultWithValidJSON() { // Given let urlString = "https://httpbin.org/get" let expectation = self.expectation(description: "request should succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } func testThatResponseStringReturnsFailureResultWithOptionalDataAndError() { // Given let urlString = "https://invalid-url-here.org/this/does/not/exist" let expectation = self.expectation(description: "request should fail with 404") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } func skipped_testThatResponseJSONReturnsSuccessResultForGETRequest() { // Given let urlString = "https://httpbin.org/" let expectation = self.expectation(description: "request should succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } if let responseDictionary = response?.result.value as? [String: Any], let args = responseDictionary["args"] as? [String: String] { XCTAssertEqual(args, ["foo": "bar"], "args should match parameters") } else { XCTFail("args should not be nil") } } func testThatResponseJSONReturnsSuccessResultForPOSTRequest() { // Given let urlString = "https://httpbin.org/post" let expectation = self.expectation(description: "request should succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, method: .post, parameters: ["foo": "bar"]).responseJSON { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } if let responseDictionary = response?.result.value as? [String: Any], let form = responseDictionary["form"] as? [String: String] { XCTAssertEqual(form, ["foo": "bar"], "form should match parameters") } else { XCTFail("form should not be nil") } } } // MARK: - class ResponseMapTestCase: BaseTestCase { func skipped_testThatMapTransformsSuccessValue() { // Given let urlString = "https://httpbin.org/header/foo" let expectation = self.expectation(description: "request should succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp.flatMap { json in // json["args"]["foo"] is "bar": use this invariant to test the map function return ((json as? [String: Any])?["args"] as? [String: Any])?["foo"] as? String ?? "invalid" } expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) XCTAssertEqual(response?.result.value, "bar") var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } func testThatMapPreservesFailureError() { // Given let urlString = "https://invalid-url-here.org/this/does/not/exist" let expectation = self.expectation(description: "request should fail with 404") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).responseData { resp in response = resp.map { _ in "ignored" } expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } } // MARK: - class ResponseFlatMapTestCase: BaseTestCase { func skipped_testThatFlatMapTransformsSuccessValue() { // Given let urlString = "https://httpbin.org/header/foo" let expectation = self.expectation(description: "request should succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp.flatMap { json in // json["args"]["foo"] is "bar": use this invariant to test the flatMap function return ((json as? [String: Any])?["args"] as? [String: Any])?["foo"] as? String ?? "invalid" } expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) XCTAssertEqual(response?.result.value, "bar") var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } func testThatFlatMapCatchesTransformationError() { // Given struct TransformError: Error {} let urlString = "https://httpbin.org/get" let expectation = self.expectation(description: "request should succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).responseData { resp in response = resp.flatMap { json in throw TransformError() } expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) if let error = response?.result.error { XCTAssertTrue(error is TransformError) } else { XCTFail("flatMap should catch the transformation error") } var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } func testThatFlatMapPreservesFailureError() { // Given let urlString = "https://invalid-url-here.org/this/does/not/exist" let expectation = self.expectation(description: "request should fail with 404") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString, parameters: ["foo": "bar"]).responseData { resp in response = resp.flatMap { _ in "ignored" } expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } } // MARK: - enum TestError: Error { case error(error: Error) } enum TransformationError: Error { case error func alwaysFails() throws -> TestError { throw TransformationError.error } } class ResponseMapErrorTestCase: BaseTestCase { func testThatMapErrorTransformsFailureValue() { // Given let urlString = "https://invalid-url-here.org/this/does/not/exist" let expectation = self.expectation(description: "request should not succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString).responseJSON { resp in response = resp.mapError { error in return TestError.error(error: error) } expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) guard let error = response?.error as? TestError, case .error = error else { XCTFail(); return } var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } func testThatMapErrorPreservesSuccessValue() { // Given let urlString = "https://httpbin.org/get" let expectation = self.expectation(description: "request should succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString).responseData { resp in response = resp.mapError { TestError.error(error: $0) } expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } } // MARK: - class ResponseFlatMapErrorTestCase: BaseTestCase { func testThatFlatMapErrorPreservesSuccessValue() { // Given let urlString = "https://httpbin.org/get" let expectation = self.expectation(description: "request should succeed") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString).responseData { resp in response = resp.flatMapError { TestError.error(error: $0) } expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } func testThatFlatMapErrorCatchesTransformationError() { // Given let urlString = "https://invalid-url-here.org/this/does/not/exist" let expectation = self.expectation(description: "request should fail") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString).responseData { resp in response = resp.flatMapError { _ in try TransformationError.error.alwaysFails() } expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) if let error = response?.result.error { XCTAssertTrue(error is TransformationError) } else { XCTFail("flatMapError should catch the transformation error") } var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } func testThatFlatMapErrorTransformsError() { // Given let urlString = "https://invalid-url-here.org/this/does/not/exist" let expectation = self.expectation(description: "request should fail") var response: CLDNDataResponse? // When CLDNSessionManager.default.request(urlString).responseData { resp in response = resp.flatMapError { TestError.error(error: $0) } expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) guard let error = response?.error as? TestError, case .error = error else { XCTFail(); return } var metrics: AnyObject? = nil if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { metrics = response?.metrics XCTAssertNotNil(metrics) } } } ================================================ FILE: Example/Tests/BaseNetwork/Core/ResultTests.swift ================================================ // // ResultTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class ResultTestCase: BaseTestCase { let error = CLDNError.responseValidationFailed(reason: .unacceptableStatusCode(code: 404)) // MARK: - Is Success Tests func testThatIsSuccessPropertyReturnsTrueForSuccessCase() { // Given, When let result = CLDNResult.success("success") // Then XCTAssertTrue(result.isSuccess, "result is success should be true for success case") } func testThatIsSuccessPropertyReturnsFalseForFailureCase() { // Given, When let result = CLDNResult.failure(error) // Then XCTAssertFalse(result.isSuccess, "result is success should be false for failure case") } // MARK: - Is Failure Tests func testThatIsFailurePropertyReturnsFalseForSuccessCase() { // Given, When let result = CLDNResult.success("success") // Then XCTAssertFalse(result.isFailure, "result is failure should be false for success case") } func testThatIsFailurePropertyReturnsTrueForFailureCase() { // Given, When let result = CLDNResult.failure(error) // Then XCTAssertTrue(result.isFailure, "result is failure should be true for failure case") } // MARK: - Value Tests func testThatValuePropertyReturnsValueForSuccessCase() { // Given, When let result = CLDNResult.success("success") // Then XCTAssertEqual(result.value ?? "", "success", "result value should match expected value") } func testThatValuePropertyReturnsNilForFailureCase() { // Given, When let result = CLDNResult.failure(error) // Then XCTAssertNil(result.value, "result value should be nil for failure case") } // MARK: - Error Tests func testThatErrorPropertyReturnsNilForSuccessCase() { // Given, When let result = CLDNResult.success("success") // Then XCTAssertNil(result.error, "result error should be nil for success case") } func testThatErrorPropertyReturnsErrorForFailureCase() { // Given, When let result = CLDNResult.failure(error) // Then XCTAssertNotNil(result.error, "result error should not be nil for failure case") } // MARK: - Description Tests func testThatDescriptionStringMatchesExpectedValueForSuccessCase() { // Given, When let result = CLDNResult.success("success") // Then XCTAssertEqual(result.description, "SUCCESS", "result description should match expected value for success case") } func testThatDescriptionStringMatchesExpectedValueForFailureCase() { // Given, When let result = CLDNResult.failure(error) // Then XCTAssertEqual(result.description, "FAILURE", "result description should match expected value for failure case") } // MARK: - Debug Description Tests func testThatDebugDescriptionStringMatchesExpectedValueForSuccessCase() { // Given, When let result = CLDNResult.success("success value") // Then XCTAssertEqual( result.debugDescription, "SUCCESS: success value", "result debug description should match expected value for success case" ) } func testThatDebugDescriptionStringMatchesExpectedValueForFailureCase() { // Given, When let result = CLDNResult.failure(error) // Then XCTAssertEqual( result.debugDescription, "FAILURE: \(error)", "result debug description should match expected value for failure case" ) } // MARK: - Initializer Tests func testThatInitializerFromThrowingClosureStoresResultAsASuccess() { // Given let value = "success value" // When let result1 = CLDNResult(value: { value }) let result2 = CLDNResult { value } // Then for result in [result1, result2] { XCTAssertTrue(result.isSuccess) XCTAssertEqual(result.value, value) } } func testThatInitializerFromThrowingClosureCatchesErrorAsAFailure() { // Given struct ResultError: Error {} // When let result1 = CLDNResult(value: { throw ResultError() }) let result2 = CLDNResult { throw ResultError() } // Then for result in [result1, result2] { XCTAssertTrue(result.isFailure) XCTAssertTrue(result.error! is ResultError) } } // MARK: - Unwrap Tests func testThatUnwrapReturnsSuccessValue() { // Given let result = CLDNResult.success("success value") // When let unwrappedValue = try? result.unwrap() // Then XCTAssertEqual(unwrappedValue, "success value") } func testThatUnwrapThrowsFailureError() { // Given struct ResultError: Error {} // When let result = CLDNResult.failure(ResultError()) // Then do { _ = try result.unwrap() XCTFail("result unwrapping should throw the failure error") } catch { XCTAssertTrue(error is ResultError) } } // MARK: - Map Tests func testThatMapTransformsSuccessValue() { // Given let result = CLDNResult.success("success value") // When #if swift(>=3.2) let mappedResult = result.map { $0.count } #else let mappedResult = result.map { $0.characters.count } #endif // Then XCTAssertEqual(mappedResult.value, 13) } func testThatMapPreservesFailureError() { // Given struct ResultError: Error {} let result = CLDNResult.failure(ResultError()) // When #if swift(>=3.2) let mappedResult = result.map { $0.count } #else let mappedResult = result.map { $0.characters.count } #endif // Then if let error = mappedResult.error { XCTAssertTrue(error is ResultError) } else { XCTFail("map should preserve the failure error") } } // MARK: - FlatMap Tests func testThatFlatMapTransformsSuccessValue() { // Given let result = CLDNResult.success("success value") // When #if swift(>=3.2) let mappedResult = result.map { $0.count } #else let mappedResult = result.map { $0.characters.count } #endif // Then XCTAssertEqual(mappedResult.value, 13) } func testThatFlatMapCatchesTransformationError() { // Given struct TransformError: Error {} let result = CLDNResult.success("success value") // When let mappedResult = result.flatMap { _ in throw TransformError() } // Then if let error = mappedResult.error { XCTAssertTrue(error is TransformError) } else { XCTFail("flatMap should catch the transformation error") } } func testThatFlatMapPreservesFailureError() { // Given struct ResultError: Error {} struct TransformError: Error {} let result = CLDNResult.failure(ResultError()) // When let mappedResult = result.flatMap { _ in throw TransformError() } // Then if let error = mappedResult.error { XCTAssertTrue(error is ResultError) } else { XCTFail("flatMap should preserve the failure error") } } // MARK: - Error Mapping Tests func testMapErrorTransformsErrorValue() { // Given struct ResultError: Error {} struct OtherError: Error { let error: Error } let result: CLDNResult = .failure(ResultError()) // When let mappedResult = result.mapError { OtherError(error: $0) } // Then if let error = mappedResult.error { XCTAssertTrue(error is OtherError) } else { XCTFail("mapError should transform error value") } } func testMapErrorPreservesSuccessError() { // Given struct ResultError: Error {} struct OtherError: Error { let error: Error } let result: CLDNResult = .success("success") // When let mappedResult = result.mapError { OtherError(error: $0) } // Then XCTAssertEqual(mappedResult.value, "success") } func testFlatMapErrorTransformsErrorValue() { // Given struct ResultError: Error {} struct OtherError: Error { let error: Error } let result: CLDNResult = .failure(ResultError()) // When let mappedResult = result.flatMapError { OtherError(error: $0) } // Then if let error = mappedResult.error { XCTAssertTrue(error is OtherError) } else { XCTFail("mapError should transform error value") } } func testFlatMapErrorCapturesThrownError() { // Given struct ResultError: Error {} struct OtherError: Error { let error: Error init(error: Error) throws { throw ThrownError() } } struct ThrownError: Error {} let result: CLDNResult = .failure(ResultError()) // When let mappedResult = result.flatMapError { try OtherError(error: $0) } // Then if let error = mappedResult.error { XCTAssertTrue(error is ThrownError) } else { XCTFail("mapError should capture thrown error value") } } // MARK: - With Value or Error Tests func testWithValueExecutesWhenSuccess() { // Given let result: CLDNResult = .success("success") var string = "failure" // When result.withValue { string = $0 } // Then XCTAssertEqual(string, "success") } func testWithValueDoesNotExecutesWhenFailure() { // Given struct ResultError: Error {} let result: CLDNResult = .failure(ResultError()) var string = "failure" // When result.withValue { string = $0 } // Then XCTAssertEqual(string, "failure") } func testWithErrorExecutesWhenFailure() { // Given struct ResultError: Error {} let result: CLDNResult = .failure(ResultError()) var string = "success" // When result.withError { string = "\(type(of: $0))" } // Then #if swift(>=4.0) XCTAssertEqual(string, "ResultError") #elseif swift(>=3.2) XCTAssertEqual(string, "ResultError #1") #else XCTAssertEqual(string, "(ResultError #1)") #endif } func testWithErrorDoesNotExecuteWhenSuccess() { // Given let result: CLDNResult = .success("success") var string = "success" // When result.withError { string = "\(type(of: $0))" } // Then XCTAssertEqual(string, "success") } // MARK: - If Success or Failure Tests func testIfSuccessExecutesWhenSuccess() { // Given let result: CLDNResult = .success("success") var string = "failure" // When result.ifSuccess { string = "success" } // Then XCTAssertEqual(string, "success") } func testIfSuccessDoesNotExecutesWhenFailure() { // Given struct ResultError: Error {} let result: CLDNResult = .failure(ResultError()) var string = "failure" // When result.ifSuccess { string = "success" } // Then XCTAssertEqual(string, "failure") } func testIfFailureExecutesWhenFailure() { // Given struct ResultError: Error {} let result: CLDNResult = .failure(ResultError()) var string = "success" // When result.ifFailure { string = "failure" } // Then XCTAssertEqual(string, "failure") } func testIfFailureDoesNotExecuteWhenSuccess() { // Given let result: CLDNResult = .success("success") var string = "success" // When result.ifFailure { string = "failure" } // Then XCTAssertEqual(string, "success") } // MARK: - Functional Chaining Tests func testFunctionalMethodsCanBeChained() { // Given struct ResultError: Error {} let result: CLDNResult = .success("first") var string = "first" var success = false // When let endResult = result .map { _ in "second" } .flatMap { _ in "third" } .withValue { if $0 == "third" { string = "fourth" } } .ifSuccess { success = true } // Then XCTAssertEqual(endResult.value, "third") XCTAssertEqual(string, "fourth") XCTAssertTrue(success) } } ================================================ FILE: Example/Tests/BaseNetwork/Core/SessionDelegateTests.swift ================================================ // // SessionDelegateTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class SessionDelegateTestCase: BaseTestCase { // prevents redundant call to redirect testing, currently there an issue with httpbin // https://github.com/postmanlabs/httpbin/issues/617 lazy var allowDelegateTest: Bool = { return ProcessInfo.processInfo.arguments.contains("TEST_REDIRECT") }() var manager: CLDNSessionManager! // MARK: - setup and teardown override func setUp() { super.setUp() manager = CLDNSessionManager(configuration: .ephemeral) } // MARK: - Tests - Session Invalidation func testThatSessionDidBecomeInvalidWithErrorClosureIsCalledWhenSet() { // Given let expectation = self.expectation(description: "Override closure should be called") var overrideClosureCalled = false var invalidationError: Error? manager.delegate.sessionDidBecomeInvalidWithError = { _, error in overrideClosureCalled = true invalidationError = error expectation.fulfill() } // When manager.session.invalidateAndCancel() waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertTrue(overrideClosureCalled) XCTAssertNil(invalidationError) } // MARK: - Tests - Session Challenges func testThatSessionDidReceiveChallengeClosureIsCalledWhenSet() { if #available(iOS 9.0, *) { // Given let expectation = self.expectation(description: "Override closure should be called") var overrideClosureCalled = false var response: HTTPURLResponse? manager.delegate.sessionDidReceiveChallenge = { session, challenge in overrideClosureCalled = true return (.performDefaultHandling, nil) } // When manager.request("https://httpbin.org/").responseJSON { closureResponse in response = closureResponse.response expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertTrue(overrideClosureCalled) XCTAssertEqual(response?.statusCode, 200) } else { // This test MUST be disabled on iOS 8.x because `respondsToSelector` is not being called for the // `URLSession:didReceiveChallenge:completionHandler:` selector when more than one test here is run // at a time. Whether we flush the URL session of wipe all the shared credentials, the behavior is // still the same. Until we find a better solution, we'll need to disable this test on iOS 8.x. } } func testThatSessionDidReceiveChallengeWithCompletionClosureIsCalledWhenSet() { if #available(iOS 9.0, *) { // Given let expectation = self.expectation(description: "Override closure should be called") var overrideClosureCalled = false var response: HTTPURLResponse? manager.delegate.sessionDidReceiveChallengeWithCompletion = { session, challenge, completion in overrideClosureCalled = true completion(.performDefaultHandling, nil) } // When manager.request("https://httpbin.org/").responseJSON { closureResponse in response = closureResponse.response expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertTrue(overrideClosureCalled) XCTAssertEqual(response?.statusCode, 200) } else { // This test MUST be disabled on iOS 8.x because `respondsToSelector` is not being called for the // `URLSession:didReceiveChallenge:completionHandler:` selector when more than one test here is run // at a time. Whether we flush the URL session of wipe all the shared credentials, the behavior is // still the same. Until we find a better solution, we'll need to disable this test on iOS 8.x. } } // MARK: - Tests - Redirects func testThatRequestWillPerformHTTPRedirectionByDefault() throws { try XCTSkipUnless(allowDelegateTest, "prevents the test from running, currently there's an issue with the remote server used") // Given let redirectURLString = "https://www.apple.com/" let urlString = "https://httpbin.org/redirect-to?url=\(redirectURLString)" let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)") var response: CLDNDefaultDataResponse? // When manager.request(urlString) .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) XCTAssertEqual(response?.response?.url?.absoluteString, redirectURLString) XCTAssertEqual(response?.response?.statusCode, 200) } func testThatRequestWillPerformRedirectionMultipleTimesByDefault() throws { try XCTSkipUnless(allowDelegateTest, "prevents the test from running, currently there's an issue with the remote server used") // Given let redirectURLString = "https://httpbin.org/get" let urlString = "https://httpbin.org/redirect/5" let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)") var response: CLDNDefaultDataResponse? // When manager.request(urlString) .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) XCTAssertEqual(response?.response?.url?.absoluteString, redirectURLString) XCTAssertEqual(response?.response?.statusCode, 200) } func testThatTaskOverrideClosureCanPerformHTTPRedirection() throws { try XCTSkipUnless(allowDelegateTest, "prevents the test from running, currently there's an issue with the remote server used") // Given let redirectURLString = "https://www.apple.com/" let urlString = "https://httpbin.org/redirect-to?url=\(redirectURLString)" let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)") let callbackExpectation = self.expectation(description: "Redirect callback should be made") let delegate: CLDNSessionDelegate = manager.delegate delegate.taskWillPerformHTTPRedirection = { _, _, _, request in callbackExpectation.fulfill() return request } var response: CLDNDefaultDataResponse? // When manager.request(urlString) .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) XCTAssertEqual(response?.response?.url?.absoluteString, redirectURLString) XCTAssertEqual(response?.response?.statusCode, 200) } func testThatTaskOverrideClosureWithCompletionCanPerformHTTPRedirection() throws { try XCTSkipUnless(allowDelegateTest, "prevents the test from running, currently there's an issue with the remote server used") // Given let redirectURLString = "https://www.apple.com/" let urlString = "https://httpbin.org/redirect-to?url=\(redirectURLString)" let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)") let callbackExpectation = self.expectation(description: "Redirect callback should be made") let delegate: CLDNSessionDelegate = manager.delegate delegate.taskWillPerformHTTPRedirectionWithCompletion = { _, _, _, request, completion in completion(request) callbackExpectation.fulfill() } var response: CLDNDefaultDataResponse? // When manager.request(urlString) .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) XCTAssertEqual(response?.response?.url?.absoluteString, redirectURLString) XCTAssertEqual(response?.response?.statusCode, 200) } func testThatTaskOverrideClosureCanCancelHTTPRedirection() throws { try XCTSkipUnless(allowDelegateTest, "prevents the test from running, currently there's an issue with the remote server used") // Given let redirectURLString = "https://www.apple.com" let urlString = "https://httpbin.org/redirect-to?url=\(redirectURLString)" let expectation = self.expectation(description: "Request should not redirect to \(redirectURLString)") let callbackExpectation = self.expectation(description: "Redirect callback should be made") let delegate: CLDNSessionDelegate = manager.delegate delegate.taskWillPerformHTTPRedirectionWithCompletion = { _, _, _, _, completion in callbackExpectation.fulfill() completion(nil) } var response: CLDNDefaultDataResponse? // When manager.request(urlString) .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) XCTAssertEqual(response?.response?.url?.absoluteString, urlString) XCTAssertEqual(response?.response?.statusCode, 302) } func testThatTaskOverrideClosureWithCompletionCanCancelHTTPRedirection() throws { try XCTSkipUnless(allowDelegateTest, "prevents the test from running, currently there's an issue with the remote server used") // Given let redirectURLString = "https://www.apple.com" let urlString = "https://httpbin.org/redirect-to?url=\(redirectURLString)" let expectation = self.expectation(description: "Request should not redirect to \(redirectURLString)") let callbackExpectation = self.expectation(description: "Redirect callback should be made") let delegate: CLDNSessionDelegate = manager.delegate delegate.taskWillPerformHTTPRedirection = { _, _, _, _ in callbackExpectation.fulfill() return nil } var response: CLDNDefaultDataResponse? // When manager.request(urlString) .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) XCTAssertEqual(response?.response?.url?.absoluteString, urlString) XCTAssertEqual(response?.response?.statusCode, 302) } func testThatTaskOverrideClosureIsCalledMultipleTimesForMultipleHTTPRedirects() throws { try XCTSkipUnless(allowDelegateTest, "prevents the test from running, currently there's an issue with the remote server used") // Given let redirectCount = 5 let redirectURLString = "https://httpbin.org/get" let urlString = "https://httpbin.org/redirect/\(redirectCount)" let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)") let delegate: CLDNSessionDelegate = manager.delegate var redirectExpectations = [XCTestExpectation]() for index in 0..? // When manager.request(urlString, headers: headers) .responseJSON { closureResponse in response = closureResponse expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) if let json = response?.result.value as? [String: Any], let headers = json["headers"] as? [String: String] { XCTAssertEqual(headers["Authorization"], "1234") XCTAssertEqual(headers["Custom-Header"], "foobar") } } // MARK: - Tests - Data Task Responses func testThatDataTaskDidReceiveResponseClosureIsCalledWhenSet() { // Given let expectation = self.expectation(description: "Override closure should be called") var overrideClosureCalled = false var response: HTTPURLResponse? manager.delegate.dataTaskDidReceiveResponse = { session, task, response in overrideClosureCalled = true return .allow } // When manager.request("https://httpbin.org/").responseJSON { closureResponse in response = closureResponse.response expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertTrue(overrideClosureCalled) XCTAssertEqual(response?.statusCode, 200) } func testThatDataTaskDidReceiveResponseWithCompletionClosureIsCalledWhenSet() { // Given let expectation = self.expectation(description: "Override closure should be called") var overrideClosureCalled = false var response: HTTPURLResponse? manager.delegate.dataTaskDidReceiveResponseWithCompletion = { session, task, response, completion in overrideClosureCalled = true completion(.allow) } // When manager.request("https://httpbin.org/").responseJSON { closureResponse in response = closureResponse.response expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertTrue(overrideClosureCalled) XCTAssertEqual(response?.statusCode, 200) } } ================================================ FILE: Example/Tests/BaseNetwork/Core/SessionManagerTests.swift ================================================ // // SessionManagerTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class SessionManagerTestCase: BaseTestCase { // MARK: Helper Types private class HTTPMethodAdapter: CLDNRequestAdapter { let method: CLDNHTTPMethod let throwsError: Bool init(method: CLDNHTTPMethod, throwsError: Bool = false) { self.method = method self.throwsError = throwsError } func CLDN_Adapt(_ urlRequest: URLRequest) throws -> URLRequest { guard !throwsError else { throw CLDNError.invalidURL(url: "") } var urlRequest = urlRequest urlRequest.httpMethod = method.rawValue return urlRequest } } private class RequestHandler: CLDNRequestAdapter, CLDNRequestRetrier { var adaptedCount = 0 var retryCount = 0 var retryErrors: [Error] = [] var shouldApplyAuthorizationHeader = false var throwsErrorOnSecondAdapt = false func CLDN_Adapt(_ urlRequest: URLRequest) throws -> URLRequest { if throwsErrorOnSecondAdapt && adaptedCount == 1 { throwsErrorOnSecondAdapt = false throw CLDNError.invalidURL(url: "") } var urlRequest = urlRequest adaptedCount += 1 if shouldApplyAuthorizationHeader && adaptedCount > 1 { if let header = CLDNRequest.authorizationHeader(user: "user", password: "password") { urlRequest.setValue(header.value, forHTTPHeaderField: header.key) } } return urlRequest } func CLDN_Should(_ manager: CLDNSessionManager, retry request: CLDNRequest, with error: Error, completion: @escaping CLDNRequestRetryCompletion) { retryCount += 1 retryErrors.append(error) if retryCount < 2 { completion(true, 0.0) } else { completion(false, 0.0) } } } private class UploadHandler: CLDNRequestAdapter, CLDNRequestRetrier { var adaptedCount = 0 var retryCount = 0 var retryErrors: [Error] = [] func CLDN_Adapt(_ urlRequest: URLRequest) throws -> URLRequest { adaptedCount += 1 if adaptedCount == 1 { throw CLDNError.invalidURL(url: "") } return urlRequest } func CLDN_Should(_ manager: CLDNSessionManager, retry request: CLDNRequest, with error: Error, completion: @escaping CLDNRequestRetryCompletion) { retryCount += 1 retryErrors.append(error) completion(true, 0.0) } } // prevents the test from running, currently there's an issue when Travis CI runs this test lazy var allowManagerTest: Bool = { return ProcessInfo.processInfo.arguments.contains("TEST_SESSION_MANAGER") }() // MARK: Tests - Initialization func testInitializerWithDefaultArguments() { // Given, When let manager = CLDNSessionManager() // Then XCTAssertNotNil(manager.session.delegate, "session delegate should not be nil") XCTAssertTrue(manager.delegate === manager.session.delegate, "manager delegate should equal session delegate") } func testInitializerWithSpecifiedArguments() { // Given let configuration = URLSessionConfiguration.default let delegate = CLDNSessionDelegate() // When let manager = CLDNSessionManager( configuration: configuration, delegate: delegate) // Then XCTAssertNotNil(manager.session.delegate, "session delegate should not be nil") XCTAssertTrue(manager.delegate === manager.session.delegate, "manager delegate should equal session delegate") } func testThatFailableInitializerSucceedsWithDefaultArguments() { // Given let delegate = CLDNSessionDelegate() let session: URLSession = { let configuration = URLSessionConfiguration.default return URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) }() // When let manager = CLDNSessionManager(session: session, delegate: delegate) // Then if let manager = manager { XCTAssertTrue(manager.delegate === manager.session.delegate, "manager delegate should equal session delegate") } else { XCTFail("manager should not be nil") } } func testThatFailableInitializerSucceedsWithSpecifiedArguments() { // Given let delegate = CLDNSessionDelegate() let session: URLSession = { let configuration = URLSessionConfiguration.default return URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) }() // When let manager = CLDNSessionManager(session: session, delegate: delegate) // Then if let manager = manager { XCTAssertTrue(manager.delegate === manager.session.delegate, "manager delegate should equal session delegate") } else { XCTFail("manager should not be nil") } } func testThatFailableInitializerFailsWithWhenDelegateDoesNotEqualSessionDelegate() { // Given let delegate = CLDNSessionDelegate() let session: URLSession = { let configuration = URLSessionConfiguration.default return URLSession(configuration: configuration, delegate: CLDNSessionDelegate(), delegateQueue: nil) }() // When let manager = CLDNSessionManager(session: session, delegate: delegate) // Then XCTAssertNil(manager, "manager should be nil") } func testThatFailableInitializerFailsWhenSessionDelegateIsNil() { // Given let delegate = CLDNSessionDelegate() let session: URLSession = { let configuration = URLSessionConfiguration.default return URLSession(configuration: configuration, delegate: nil, delegateQueue: nil) }() // When let manager = CLDNSessionManager(session: session, delegate: delegate) // Then XCTAssertNil(manager, "manager should be nil") } // MARK: Tests - Default HTTP Headers func testDefaultUserAgentHeader() { // Given, When let userAgent = CLDNSessionManager.defaultHTTPHeaders["User-Agent"] // Then let osNameVersion: String = { let version = ProcessInfo.processInfo.operatingSystemVersion let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" let osName: String = { #if os(iOS) return "iOS" #elseif os(watchOS) return "watchOS" #elseif os(tvOS) return "tvOS" #elseif os(macOS) return "OS X" #elseif os(Linux) return "Linux" #else return "Unknown" #endif }() return "\(osName) \(versionString)" }() let cloudinaryVersion: String = { guard let afInfo = Bundle(for: CLDNSessionManager.self).infoDictionary, let build = afInfo["CFBundleShortVersionString"] else { return "Unknown" } return "Cloudinary/\(build)" }() let info = Bundle.main.infoDictionary let executable = info?[kCFBundleExecutableKey as String] as? String ?? "Unknown" let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown" XCTAssertTrue(userAgent?.contains(cloudinaryVersion) == true) XCTAssertTrue(userAgent?.contains(osNameVersion) == true) XCTAssertTrue(userAgent?.contains("\(executable)/\(appVersion)") == true) } // MARK: Tests - Start Requests Immediately func testSetStartRequestsImmediatelyToFalseAndResumeRequest() { // Given let manager = CLDNSessionManager() manager.startRequestsImmediately = false let url = URL(string: "https://httpbin.org/")! let urlRequest = URLRequest(url: url) let expectation = self.expectation(description: "\(url)") var response: HTTPURLResponse? // When manager.request(urlRequest) .response { resp in response = resp.response expectation.fulfill() } .resume() waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response, "response should not be nil") XCTAssertTrue(response?.statusCode == 200, "response status code should be 200") } // MARK: Tests - Deinitialization func testReleasingManagerWithPendingRequestDeinitializesSuccessfully() { // Given var manager: CLDNSessionManager? = CLDNSessionManager() manager?.startRequestsImmediately = false let url = URL(string: "https://httpbin.org/get")! let urlRequest = URLRequest(url: url) // When let request = manager?.request(urlRequest) manager = nil // Then XCTAssertTrue(request?.task?.state == .suspended, "request task state should be '.Suspended'") XCTAssertNil(manager, "manager should be nil") } func testReleasingManagerWithPendingCanceledRequestDeinitializesSuccessfully() { // Given var manager: CLDNSessionManager? = CLDNSessionManager() manager!.startRequestsImmediately = false let url = URL(string: "https://httpbin.org/")! let urlRequest = URLRequest(url: url) // When let request = manager!.request(urlRequest) request.cancel() manager = nil // Then let state = request.task?.state XCTAssertTrue(state == .canceling || state == .completed, "state should be .Canceling or .Completed") XCTAssertNil(manager, "manager should be nil") } // MARK: Tests - Bad Requests func skipped_testThatDataRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given let sessionManager = CLDNSessionManager() let expectation = self.expectation(description: "CLDNRequest should fail with error") var response: CLDNDefaultDataResponse? // When sessionManager.request("https://httpbin.org/get/äëïöü").response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.data?.count, 0) XCTAssertNotNil(response?.error) if let error = response?.error as? CLDNError { XCTAssertTrue(error.isInvalidURLError) XCTAssertEqual(error.urlConvertible as? String, "https://httpbin.org/get/äëïöü") } else { XCTFail("error should not be nil") } } func skipped_testThatUploadDataRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given let sessionManager = CLDNSessionManager() let expectation = self.expectation(description: "Upload should fail with error") var response: CLDNDefaultDataResponse? // When sessionManager.upload(Data(), to: "https://httpbin.org/").response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.data?.count, 0) XCTAssertNotNil(response?.error) if let error = response?.error as? CLDNError { XCTAssertTrue(error.isInvalidURLError) XCTAssertEqual(error.urlConvertible as? String, "https://httpbin.org/get/äëïöü") } else { XCTFail("error should not be nil") } } func skipped_testThatUploadFileRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given let sessionManager = CLDNSessionManager() let expectation = self.expectation(description: "Upload should fail with error") var response: CLDNDefaultDataResponse? // When sessionManager.upload(URL(fileURLWithPath: "/invalid"), to: "https://httpbin.org/get/äëïöü").response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.data?.count, 0) XCTAssertNotNil(response?.error) if let error = response?.error as? CLDNError { XCTAssertTrue(error.isInvalidURLError) XCTAssertEqual(error.urlConvertible as? String, "https://httpbin.org/get/äëïöü") } else { XCTFail("error should not be nil") } } func skipped_testThatUploadStreamRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given let sessionManager = CLDNSessionManager() let expectation = self.expectation(description: "Upload should fail with error") var response: CLDNDefaultDataResponse? // When sessionManager.upload(InputStream(data: Data()), to: "https://httpbin.org/get/äëïöü").response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.data?.count, 0) XCTAssertNotNil(response?.error) if let error = response?.error as? CLDNError { XCTAssertTrue(error.isInvalidURLError) XCTAssertEqual(error.urlConvertible as? String, "https://httpbin.org/get/äëïöü") } else { XCTFail("error should not be nil") } } // MARK: Tests - CLDNRequest Adapter func testThatSessionManagerCallsRequestAdapterWhenCreatingDataRequest() { // Given let adapter = HTTPMethodAdapter(method: .post) let sessionManager = CLDNSessionManager() sessionManager.adapter = adapter sessionManager.startRequestsImmediately = false // When let request = sessionManager.request("https://httpbin.org/get") // Then XCTAssertEqual(request.task?.originalRequest?.httpMethod, adapter.method.rawValue) } func testThatSessionManagerCallsRequestAdapterWhenCreatingUploadRequestWithData() { // Given let adapter = HTTPMethodAdapter(method: .get) let sessionManager = CLDNSessionManager() sessionManager.adapter = adapter sessionManager.startRequestsImmediately = false // When let request = sessionManager.upload("data".data(using: .utf8)!, to: "https://httpbin.org/post") // Then XCTAssertEqual(request.task?.originalRequest?.httpMethod, adapter.method.rawValue) } func testThatSessionManagerCallsRequestAdapterWhenCreatingUploadRequestWithFile() { // Given let adapter = HTTPMethodAdapter(method: .get) let sessionManager = CLDNSessionManager() sessionManager.adapter = adapter sessionManager.startRequestsImmediately = false // When let fileURL = URL(fileURLWithPath: "/path/to/some/file.txt") let request = sessionManager.upload(fileURL, to: "https://httpbin.org/post") // Then XCTAssertEqual(request.task?.originalRequest?.httpMethod, adapter.method.rawValue) } func testThatSessionManagerCallsRequestAdapterWhenCreatingUploadRequestWithInputStream() { // Given let adapter = HTTPMethodAdapter(method: .get) let sessionManager = CLDNSessionManager() sessionManager.adapter = adapter sessionManager.startRequestsImmediately = false // When let inputStream = InputStream(data: "data".data(using: .utf8)!) let request = sessionManager.upload(inputStream, to: "https://httpbin.org/post") // Then XCTAssertEqual(request.task?.originalRequest?.httpMethod, adapter.method.rawValue) } func testThatRequestAdapterErrorThrowsResponseHandlerError() { // Given let adapter = HTTPMethodAdapter(method: .post, throwsError: true) let sessionManager = CLDNSessionManager() sessionManager.adapter = adapter sessionManager.startRequestsImmediately = false // When let request = sessionManager.request("https://httpbin.org/get") // Then if let error = request.delegate.error as? CLDNError { XCTAssertTrue(error.isInvalidURLError) XCTAssertEqual(error.urlConvertible as? String, "") } else { XCTFail("error should not be nil") } } // MARK: Tests - CLDNRequest Retrier func testThatSessionManagerCallsRequestRetrierWhenRequestEncountersError() throws { try XCTSkipUnless(allowManagerTest, "prevents the test from running, currently there's an issue when Travis CI runs this test") // Given let handler = RequestHandler() let sessionManager = CLDNSessionManager() sessionManager.adapter = handler sessionManager.retrier = handler let expectation = self.expectation(description: "request should eventually fail") var response: CLDNDataResponse? // When let request = sessionManager.request("https://httpbin.org/basic-auth/user/password") .validate() .responseJSON { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertEqual(handler.adaptedCount, 2) XCTAssertEqual(handler.retryCount, 2) XCTAssertEqual(request.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, false) XCTAssertTrue(sessionManager.delegate.requests.isEmpty) } func testThatSessionManagerCallsRequestRetrierWhenRequestInitiallyEncountersAdaptError() throws { try XCTSkipUnless(allowManagerTest, "prevents the test from running, currently there's an issue when Travis CI runs this test") // Given let handler = RequestHandler() handler.adaptedCount = 1 handler.throwsErrorOnSecondAdapt = true handler.shouldApplyAuthorizationHeader = true let sessionManager = CLDNSessionManager() sessionManager.adapter = handler sessionManager.retrier = handler let expectation = self.expectation(description: "request should eventually fail") var response: CLDNDataResponse? // When sessionManager.request("https://httpbin.org/basic-auth/user/password") .validate() .responseJSON { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertEqual(handler.adaptedCount, 2, "handler.adaptedCount should be equal to 2") XCTAssertEqual(handler.retryCount, 1, "handler.retryCount should be equal to 1") XCTAssertEqual(response?.result.isSuccess, true, "response?.result.isSuccess should be equal to true") XCTAssertTrue(sessionManager.delegate.requests.isEmpty, "delegate.requests.isEmpty should be empty") handler.retryErrors.forEach { XCTAssertFalse($0 is AdaptError, "retry error should not be AdaptError") } } func testThatSessionManagerCallsRequestRetrierWhenUploadInitiallyEncountersAdaptError() throws { try XCTSkipUnless(allowManagerTest, "prevents the test from running, currently there's an issue when Travis CI runs this test") // Given let handler = UploadHandler() let sessionManager = CLDNSessionManager() sessionManager.adapter = handler sessionManager.retrier = handler let expectation = self.expectation(description: "request should eventually fail") var response: CLDNDataResponse? let uploadData = "upload data".data(using: .utf8, allowLossyConversion: false)! // When sessionManager.upload(uploadData, to: "https://httpbin.org/post") .validate() .responseJSON { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertEqual(handler.adaptedCount, 2) XCTAssertEqual(handler.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, true) XCTAssertTrue(sessionManager.delegate.requests.isEmpty) handler.retryErrors.forEach { XCTAssertFalse($0 is AdaptError) } } func testThatSessionManagerCallsAdapterWhenRequestIsRetried() throws { try XCTSkipUnless(allowManagerTest, "prevents the test from running, currently there's an issue when Travis CI runs this test") // Given let handler = RequestHandler() handler.shouldApplyAuthorizationHeader = true let sessionManager = CLDNSessionManager() sessionManager.adapter = handler sessionManager.retrier = handler let expectation = self.expectation(description: "request should eventually fail") var response: CLDNDataResponse? // When let request = sessionManager.request("https://httpbin.org/basic-auth/user/password") .validate() .responseJSON { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertEqual(handler.adaptedCount, 2) XCTAssertEqual(handler.retryCount, 1) XCTAssertEqual(request.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, true) XCTAssertTrue(sessionManager.delegate.requests.isEmpty) } func testThatRequestAdapterErrorThrowsResponseHandlerErrorWhenRequestIsRetried() throws { try XCTSkipUnless(allowManagerTest, "prevents the test from running, currently there's an issue when Travis CI runs this test") // Given let handler = RequestHandler() handler.throwsErrorOnSecondAdapt = true let sessionManager = CLDNSessionManager() sessionManager.adapter = handler sessionManager.retrier = handler let expectation = self.expectation(description: "request should eventually fail") var response: CLDNDataResponse? // When let request = sessionManager.request("https://httpbin.org/basic-auth/user/password") .validate() .responseJSON { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertEqual(handler.adaptedCount, 1, "handler.adaptedCount count should be equal to 0") XCTAssertEqual(handler.retryCount, 1, "handler.retry count should be equal to 0") XCTAssertEqual(request.retryCount, 0, "result.retry count should be equal to 0") XCTAssertEqual(response?.result.isSuccess, false, "result should succeed") XCTAssertTrue(sessionManager.delegate.requests.isEmpty, "delegate.requests should be empty") if let error = response?.result.error as? CLDNError { XCTAssertTrue(error.isInvalidURLError, "error.isInvalidURLError should be true") XCTAssertEqual(error.urlConvertible as? String, "", "error.urlConvertible shold be true") } else { XCTFail("error should not be nil") } } } // MARK: - class SessionManagerConfigurationHeadersTestCase: BaseTestCase { enum ConfigurationType { case `default`, ephemeral, background } func testThatDefaultConfigurationHeadersAreSentWithRequest() { // Given, When, Then executeAuthorizationHeaderTest(for: .default) } func testThatEphemeralConfigurationHeadersAreSentWithRequest() { // Given, When, Then executeAuthorizationHeaderTest(for: .ephemeral) } #if os(macOS) func testThatBackgroundConfigurationHeadersAreSentWithRequest() { // Given, When, Then executeAuthorizationHeaderTest(for: .background) } #endif private func executeAuthorizationHeaderTest(for type: ConfigurationType) { // Given let manager: CLDNSessionManager = { let configuration: URLSessionConfiguration = { let configuration: URLSessionConfiguration switch type { case .default: configuration = .default case .ephemeral: configuration = .ephemeral case .background: let identifier = "org.cloudinary.test.manager-configuration-tests" configuration = .background(withIdentifier: identifier) } var headers = CLDNSessionManager.defaultHTTPHeaders headers["Authorization"] = "Bearer 123456" configuration.httpAdditionalHeaders = headers return configuration }() return CLDNSessionManager(configuration: configuration) }() let expectation = self.expectation(description: "request should complete successfully") var response: CLDNDataResponse? // When manager.request("https://httpbin.org/headers") .responseJSON { closureResponse in response = closureResponse expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then if let response = response { XCTAssertNotNil(response.request, "request should not be nil") XCTAssertNotNil(response.response, "response should not be nil") XCTAssertNotNil(response.data, "data should not be nil") XCTAssertTrue(response.result.isSuccess, "result should be a success") if let response = response.result.value as? [String: Any], let headers = response["headers"] as? [String: String], let authorization = headers["Authorization"] { XCTAssertEqual(authorization, "Bearer 123456", "authorization header value does not match") } else { XCTFail("failed to extract authorization header value") } } else { XCTFail("response should not be nil") } } } ================================================ FILE: Example/Tests/BaseNetwork/Core/UploadTests.swift ================================================ // // UploadTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class UploadFileInitializationTestCase: BaseTestCase { func testUploadClassMethodWithMethodURLAndFile() { // Given let urlString = "https://httpbin.org/" let imageURL = url(forResource: "rainbow", withExtension: "jpg") // When let request = CLDNSessionManager.default.upload(imageURL, to: urlString) // Then XCTAssertNotNil(request.request, "request should not be nil") XCTAssertEqual(request.request?.httpMethod ?? "", "POST", "request HTTP method should be POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString, "request URL string should be equal") XCTAssertNil(request.response, "response should be nil") } func testUploadClassMethodWithMethodURLHeadersAndFile() { // Given let urlString = "https://httpbin.org/" let headers = ["Authorization": "123456"] let imageURL = url(forResource: "rainbow", withExtension: "jpg") // When let request = CLDNSessionManager.default.upload(imageURL, to: urlString, method: .post, headers: headers) // Then XCTAssertNotNil(request.request, "request should not be nil") XCTAssertEqual(request.request?.httpMethod ?? "", "POST", "request HTTP method should be POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString, "request URL string should be equal") let authorizationHeader = request.request?.value(forHTTPHeaderField: "Authorization") ?? "" XCTAssertEqual(authorizationHeader, "123456", "Authorization header is incorrect") XCTAssertNil(request.response, "response should be nil") } } // MARK: - class UploadDataInitializationTestCase: BaseTestCase { func testUploadClassMethodWithMethodURLAndData() { // Given let urlString = "https://httpbin.org/" // When let request = CLDNSessionManager.default.upload(Data(), to: urlString) // Then XCTAssertNotNil(request.request, "request should not be nil") XCTAssertEqual(request.request?.httpMethod ?? "", "POST", "request HTTP method should be POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString, "request URL string should be equal") XCTAssertNil(request.response, "response should be nil") } func testUploadClassMethodWithMethodURLHeadersAndData() { // Given let urlString = "https://httpbin.org/" let headers = ["Authorization": "123456"] // When let request = CLDNSessionManager.default.upload(Data(), to: urlString, headers: headers) // Then XCTAssertNotNil(request.request, "request should not be nil") XCTAssertEqual(request.request?.httpMethod ?? "", "POST", "request HTTP method should be POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString, "request URL string should be equal") let authorizationHeader = request.request?.value(forHTTPHeaderField: "Authorization") ?? "" XCTAssertEqual(authorizationHeader, "123456", "Authorization header is incorrect") XCTAssertNil(request.response, "response should be nil") } } // MARK: - class UploadStreamInitializationTestCase: BaseTestCase { func testUploadClassMethodWithMethodURLAndStream() { // Given let urlString = "https://httpbin.org/" let imageURL = url(forResource: "rainbow", withExtension: "jpg") let imageStream = InputStream(url: imageURL)! // When let request = CLDNSessionManager.default.upload(imageStream, to: urlString) // Then XCTAssertNotNil(request.request, "request should not be nil") XCTAssertEqual(request.request?.httpMethod ?? "", "POST", "request HTTP method should be POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString, "request URL string should be equal") XCTAssertNil(request.response, "response should be nil") } func testUploadClassMethodWithMethodURLHeadersAndStream() { // Given let urlString = "https://httpbin.org/" let imageURL = url(forResource: "rainbow", withExtension: "jpg") let headers = ["Authorization": "123456"] let imageStream = InputStream(url: imageURL)! // When let request = CLDNSessionManager.default.upload(imageStream, to: urlString, headers: headers) // Then XCTAssertNotNil(request.request, "request should not be nil") XCTAssertEqual(request.request?.httpMethod ?? "", "POST", "request HTTP method should be POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString, "request URL string should be equal") let authorizationHeader = request.request?.value(forHTTPHeaderField: "Authorization") ?? "" XCTAssertEqual(authorizationHeader, "123456", "Authorization header is incorrect") XCTAssertNil(request.response, "response should be nil") } } // MARK: - class UploadDataTestCase: BaseTestCase { func testUploadDataRequest() { // Given let urlString = "https://httpbin.org/post" let data = "Lorem ipsum dolor sit amet".data(using: .utf8, allowLossyConversion: false)! let expectation = self.expectation(description: "Upload request should succeed: \(urlString)") var response: CLDNDefaultDataResponse? // When CLDNSessionManager.default.upload(data, to: urlString) .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNil(response?.error) } func skipped_testUploadDataRequestWithProgress() { // Given let urlString = "https://httpbin.org/post" let data: Data = { var text = "" for _ in 1...3_000 { text += "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " } return text.data(using: .utf8, allowLossyConversion: false)! }() let expectation = self.expectation(description: "Bytes upload progress should be reported: \(urlString)") var uploadProgressValues: [Double] = [] var downloadProgressValues: [Double] = [] var response: CLDNDefaultDataResponse? // When CLDNSessionManager.default.upload(data, to: urlString) .uploadProgress { progress in uploadProgressValues.append(progress.fractionCompleted) } .downloadProgress { progress in downloadProgressValues.append(progress.fractionCompleted) } .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) var previousUploadProgress: Double = uploadProgressValues.first ?? 0.0 for progress in uploadProgressValues { XCTAssertGreaterThanOrEqual(progress, previousUploadProgress) previousUploadProgress = progress } if let lastProgressValue = uploadProgressValues.last { XCTAssertEqual(lastProgressValue, 1.0) } else { XCTFail("last item in uploadProgressValues should not be nil") } var previousDownloadProgress: Double = downloadProgressValues.first ?? 0.0 for progress in downloadProgressValues { XCTAssertGreaterThanOrEqual(progress, previousDownloadProgress) previousDownloadProgress = progress } if let lastProgressValue = downloadProgressValues.last { XCTAssertEqual(lastProgressValue, 1.0) } else { XCTFail("last item in downloadProgressValues should not be nil") } } } // MARK: - class UploadMultipartFormDataTestCase: BaseTestCase { // MARK: Tests func testThatUploadingMultipartFormDataSetsContentTypeHeader() { // Given let urlString = "https://httpbin.org/post" let uploadData = "upload_data".data(using: .utf8, allowLossyConversion: false)! let expectation = self.expectation(description: "multipart form data upload should succeed") var formData: CLDNMultipartFormData? var response: CLDNDefaultDataResponse? // When CLDNSessionManager.default.upload( multipartFormData: { multipartFormData in multipartFormData.append(uploadData, withName: "upload_data") formData = multipartFormData }, to: urlString, encodingCompletion: { result in switch result { case .success(let upload, _, _): upload.response { resp in response = resp expectation.fulfill() } case .failure: expectation.fulfill() } } ) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) if let request = response?.request, let multipartFormData = formData, let contentType = request.value(forHTTPHeaderField: "Content-Type") { XCTAssertEqual(contentType, multipartFormData.contentType) } else { XCTFail("Content-Type header value should not be nil") } } func testThatUploadingMultipartFormDataSucceedsWithDefaultParameters() { // Given let urlString = "https://httpbin.org/post" let frenchData = "français".data(using: .utf8, allowLossyConversion: false)! let japaneseData = "日本語".data(using: .utf8, allowLossyConversion: false)! let expectation = self.expectation(description: "multipart form data upload should succeed") var response: CLDNDefaultDataResponse? // When CLDNSessionManager.default.upload( multipartFormData: { multipartFormData in multipartFormData.append(frenchData, withName: "french") multipartFormData.append(japaneseData, withName: "japanese") }, to: urlString, encodingCompletion: { result in switch result { case .success(let upload, _, _): upload.response { resp in response = resp expectation.fulfill() } case .failure: expectation.fulfill() } } ) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) } func testThatUploadingMultipartFormDataWhileStreamingFromMemoryMonitorsProgress() { executeMultipartFormDataUploadRequestWithProgress(streamFromDisk: false) } func testThatUploadingMultipartFormDataWhileStreamingFromDiskMonitorsProgress() { executeMultipartFormDataUploadRequestWithProgress(streamFromDisk: true) } func testThatUploadingMultipartFormDataBelowMemoryThresholdStreamsFromMemory() { // Given let urlString = "https://httpbin.org/post" let frenchData = "français".data(using: .utf8, allowLossyConversion: false)! let japaneseData = "日本語".data(using: .utf8, allowLossyConversion: false)! let expectation = self.expectation(description: "multipart form data upload should succeed") var streamingFromDisk: Bool? var streamFileURL: URL? // When CLDNSessionManager.default.upload( multipartFormData: { multipartFormData in multipartFormData.append(frenchData, withName: "french") multipartFormData.append(japaneseData, withName: "japanese") }, to: urlString, encodingCompletion: { result in switch result { case let .success(upload, uploadStreamingFromDisk, uploadStreamFileURL): streamingFromDisk = uploadStreamingFromDisk streamFileURL = uploadStreamFileURL upload.response { _ in expectation.fulfill() } case .failure: expectation.fulfill() } } ) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(streamingFromDisk, "streaming from disk should not be nil") XCTAssertNil(streamFileURL, "stream file URL should be nil") if let streamingFromDisk = streamingFromDisk { XCTAssertFalse(streamingFromDisk, "streaming from disk should be false") } } func testThatUploadingMultipartFormDataBelowMemoryThresholdSetsContentTypeHeader() { // Given let urlString = "https://httpbin.org/post" let uploadData = "upload data".data(using: .utf8, allowLossyConversion: false)! let expectation = self.expectation(description: "multipart form data upload should succeed") var formData: CLDNMultipartFormData? var request: URLRequest? var streamingFromDisk: Bool? // When CLDNSessionManager.default.upload( multipartFormData: { multipartFormData in multipartFormData.append(uploadData, withName: "upload_data") formData = multipartFormData }, to: urlString, encodingCompletion: { result in switch result { case let .success(upload, uploadStreamingFromDisk, _): streamingFromDisk = uploadStreamingFromDisk upload.response { resp in request = resp.request expectation.fulfill() } case .failure: expectation.fulfill() } } ) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(streamingFromDisk, "streaming from disk should not be nil") if let streamingFromDisk = streamingFromDisk { XCTAssertFalse(streamingFromDisk, "streaming from disk should be false") } if let request = request, let multipartFormData = formData, let contentType = request.value(forHTTPHeaderField: "Content-Type") { XCTAssertEqual(contentType, multipartFormData.contentType, "Content-Type header value should match") } else { XCTFail("Content-Type header value should not be nil") } } func testThatUploadingMultipartFormDataAboveMemoryThresholdStreamsFromDisk() { // Given let urlString = "https://httpbin.org/post" let frenchData = "français".data(using: .utf8, allowLossyConversion: false)! let japaneseData = "日本語".data(using: .utf8, allowLossyConversion: false)! let expectation = self.expectation(description: "multipart form data upload should succeed") var streamingFromDisk: Bool? var streamFileURL: URL? // When CLDNSessionManager.default.upload( multipartFormData: { multipartFormData in multipartFormData.append(frenchData, withName: "french") multipartFormData.append(japaneseData, withName: "japanese") }, usingThreshold: 0, to: urlString, encodingCompletion: { result in switch result { case let .success(upload, uploadStreamingFromDisk, uploadStreamFileURL): streamingFromDisk = uploadStreamingFromDisk streamFileURL = uploadStreamFileURL upload.response { _ in expectation.fulfill() } case .failure: expectation.fulfill() } } ) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(streamingFromDisk, "streaming from disk should not be nil") XCTAssertNotNil(streamFileURL, "stream file URL should not be nil") if let streamingFromDisk = streamingFromDisk, let streamFilePath = streamFileURL?.path { XCTAssertTrue(streamingFromDisk, "streaming from disk should be true") XCTAssertFalse(FileManager.default.fileExists(atPath: streamFilePath), "stream file path should not exist") } } func testThatUploadingMultipartFormDataAboveMemoryThresholdSetsContentTypeHeader() { // Given let urlString = "https://httpbin.org/post" let uploadData = "upload data".data(using: .utf8, allowLossyConversion: false)! let expectation = self.expectation(description: "multipart form data upload should succeed") var formData: CLDNMultipartFormData? var request: URLRequest? var streamingFromDisk: Bool? // When CLDNSessionManager.default.upload( multipartFormData: { multipartFormData in multipartFormData.append(uploadData, withName: "upload_data") formData = multipartFormData }, usingThreshold: 0, to: urlString, encodingCompletion: { result in switch result { case let .success(upload, uploadStreamingFromDisk, _): streamingFromDisk = uploadStreamingFromDisk upload.response { resp in request = resp.request expectation.fulfill() } case .failure: expectation.fulfill() } } ) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(streamingFromDisk, "streaming from disk should not be nil") if let streamingFromDisk = streamingFromDisk { XCTAssertTrue(streamingFromDisk, "streaming from disk should be true") } if let request = request, let multipartFormData = formData, let contentType = request.value(forHTTPHeaderField: "Content-Type") { XCTAssertEqual(contentType, multipartFormData.contentType, "Content-Type header value should match") } else { XCTFail("Content-Type header value should not be nil") } } #if os(macOS) func testThatUploadingMultipartFormDataOnBackgroundSessionWritesDataToFileToAvoidCrash() { // Given let manager: SessionManager = { let identifier = "org.cloudinary.uploadtests.\(UUID().uuidString)" let configuration = URLSessionConfiguration.background(withIdentifier: identifier) return SessionManager(configuration: configuration, serverTrustPolicyManager: nil) }() let urlString = "https://httpbin.org/post" let french = "français".data(using: .utf8, allowLossyConversion: false)! let japanese = "日本語".data(using: .utf8, allowLossyConversion: false)! let expectation = self.expectation(description: "multipart form data upload should succeed") var request: URLRequest? var response: HTTPURLResponse? var data: Data? var error: Error? var streamingFromDisk: Bool? // When manager.upload( multipartFormData: { multipartFormData in multipartFormData.append(french, withName: "french") multipartFormData.append(japanese, withName: "japanese") }, to: urlString, encodingCompletion: { result in switch result { case let .success(upload, uploadStreamingFromDisk, _): streamingFromDisk = uploadStreamingFromDisk upload.response { defaultResponse in request = defaultResponse.request response = defaultResponse.response data = defaultResponse.data error = defaultResponse.error expectation.fulfill() } case .failure: expectation.fulfill() } } ) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(request, "request should not be nil") XCTAssertNotNil(response, "response should not be nil") XCTAssertNotNil(data, "data should not be nil") XCTAssertNil(error, "error should be nil") if let streamingFromDisk = streamingFromDisk { XCTAssertTrue(streamingFromDisk, "streaming from disk should be true") } else { XCTFail("streaming from disk should not be nil") } } #endif // MARK: Combined Test Execution private func executeMultipartFormDataUploadRequestWithProgress(streamFromDisk: Bool) { // Given let urlString = "https://httpbin.org/post" let loremData1: Data = { var loremValues: [String] = [] for _ in 1...1_500 { loremValues.append("Lorem ipsum dolor sit amet, consectetur adipiscing elit.") } return loremValues.joined(separator: " ").data(using: .utf8, allowLossyConversion: false)! }() let loremData2: Data = { var loremValues: [String] = [] for _ in 1...1_500 { loremValues.append("Lorem ipsum dolor sit amet, nam no graeco recusabo appellantur.") } return loremValues.joined(separator: " ").data(using: .utf8, allowLossyConversion: false)! }() let expectation = self.expectation(description: "multipart form data upload should succeed") var uploadProgressValues: [Double] = [] var downloadProgressValues: [Double] = [] var response: CLDNDefaultDataResponse? // When CLDNSessionManager.default.upload( multipartFormData: { multipartFormData in multipartFormData.append(loremData1, withName: "lorem1") multipartFormData.append(loremData2, withName: "lorem2") }, usingThreshold: streamFromDisk ? 0 : 100_000_000, to: urlString, encodingCompletion: { result in switch result { case .success(let upload, _, _): upload .uploadProgress { progress in uploadProgressValues.append(progress.fractionCompleted) } .downloadProgress { progress in downloadProgressValues.append(progress.fractionCompleted) } .response { resp in response = resp expectation.fulfill() } case .failure: expectation.fulfill() } } ) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) var previousUploadProgress: Double = uploadProgressValues.first ?? 0.0 for progress in uploadProgressValues { XCTAssertGreaterThanOrEqual(progress, previousUploadProgress) previousUploadProgress = progress } if let lastProgressValue = uploadProgressValues.last { XCTAssertEqual(lastProgressValue, 1.0) } else { XCTFail("last item in uploadProgressValues should not be nil") } var previousDownloadProgress: Double = downloadProgressValues.first ?? 0.0 for progress in downloadProgressValues { XCTAssertGreaterThanOrEqual(progress, previousDownloadProgress) previousDownloadProgress = progress } if let lastProgressValue = downloadProgressValues.last { XCTAssertEqual(lastProgressValue, 1.0) } else { XCTFail("last item in downloadProgressValues should not be nil") } } } ================================================ FILE: Example/Tests/BaseNetwork/Extensions/CLDNDataResponse+CloudinaryTests.swift ================================================ // // CLDNDataResponse+CloudinaryTests.swift // Cloudinary_Tests // // Created on 30/03/2020. // Copyright (c) 2020 Cloudinary. All rights reserved. // @testable import Cloudinary import Foundation // MARK: - MAP Method extension CLDNDataResponse { /// Evaluates the specified closure when the result of this `CLDNDataResponse` is a success, passing the unwrapped /// result value as a parameter. /// /// Use the `map` method with a closure that does not throw. For example: /// /// let possibleData: CLDNDataResponse = ... /// let possibleInt = possibleData.map { $0.count } /// /// - parameter transform: A closure that takes the success value of the instance's result. /// /// - returns: A `CLDNDataResponse` whose result wraps the value returned by the given closure. If this instance's /// result is a failure, returns a response wrapping the same failure. internal func map(_ transform: (Value) -> T) -> CLDNDataResponse { var response = CLDNDataResponse( request: request, response: self.response, data: data, result: result.map(transform), timeline: timeline ) response._metrics = _metrics return response } /// Evaluates the given closure when the result of this `CLDNDataResponse` is a success, passing the unwrapped result /// value as a parameter. /// /// Use the `flatMap` method with a closure that may throw an error. For example: /// /// let possibleData: CLDNDataResponse = ... /// let possibleObject = possibleData.flatMap { /// try JSONSerialization.jsonObject(with: $0) /// } /// /// - parameter transform: A closure that takes the success value of the instance's result. /// /// - returns: A success or failure `CLDNDataResponse` depending on the result of the given closure. If this instance's /// result is a failure, returns the same failure. internal func flatMap(_ transform: (Value) throws -> T) -> CLDNDataResponse { var response = CLDNDataResponse( request: request, response: self.response, data: data, result: result.flatMap(transform), timeline: timeline ) response._metrics = _metrics return response } /// Evaluates the specified closure when the `CLDNDataResponse` is a failure, passing the unwrapped error as a parameter. /// /// Use the `mapError` function with a closure that does not throw. For example: /// /// let possibleData: CLDNDataResponse = ... /// let withMyError = possibleData.mapError { MyError.error($0) } /// /// - Parameter transform: A closure that takes the error of the instance. /// - Returns: A `CLDNDataResponse` instance containing the result of the transform. internal func mapError(_ transform: (Error) -> E) -> CLDNDataResponse { var response = CLDNDataResponse( request: request, response: self.response, data: data, result: result.mapError(transform), timeline: timeline ) response._metrics = _metrics return response } /// Evaluates the specified closure when the `CLDNDataResponse` is a failure, passing the unwrapped error as a parameter. /// /// Use the `flatMapError` function with a closure that may throw an error. For example: /// /// let possibleData: CLDNDataResponse = ... /// let possibleObject = possibleData.flatMapError { /// try someFailableFunction(taking: $0) /// } /// /// - Parameter transform: A throwing closure that takes the error of the instance. /// /// - Returns: A `CLDNDataResponse` instance containing the result of the transform. internal func flatMapError(_ transform: (Error) throws -> E) -> CLDNDataResponse { var response = CLDNDataResponse( request: request, response: self.response, data: data, result: result.flatMapError(transform), timeline: timeline ) response._metrics = _metrics return response } } ================================================ FILE: Example/Tests/BaseNetwork/Extensions/CLDNError+CloudinaryTests.swift ================================================ // // CLDNError+CloudinaryTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary extension CLDNError { // ParameterEncodingFailureReason var isMissingURLFailed: Bool { if case let .parameterEncodingFailed(reason) = self, reason.isMissingURL { return true } return false } var isJSONEncodingFailed: Bool { if case let .parameterEncodingFailed(reason) = self, reason.isJSONEncodingFailed { return true } return false } var isPropertyListEncodingFailed: Bool { if case let .parameterEncodingFailed(reason) = self, reason.isPropertyListEncodingFailed { return true } return false } // MultipartEncodingFailureReason var isBodyPartURLInvalid: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isBodyPartURLInvalid { return true } return false } var isBodyPartFilenameInvalid: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isBodyPartFilenameInvalid { return true } return false } var isBodyPartFileNotReachable: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isBodyPartFileNotReachable { return true } return false } var isBodyPartFileNotReachableWithError: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isBodyPartFileNotReachableWithError { return true } return false } var isBodyPartFileIsDirectory: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isBodyPartFileIsDirectory { return true } return false } var isBodyPartFileSizeNotAvailable: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isBodyPartFileSizeNotAvailable { return true } return false } var isBodyPartFileSizeQueryFailedWithError: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isBodyPartFileSizeQueryFailedWithError { return true } return false } var isBodyPartInputStreamCreationFailed: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isBodyPartInputStreamCreationFailed { return true } return false } var isOutputStreamCreationFailed: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isOutputStreamCreationFailed { return true } return false } var isOutputStreamFileAlreadyExists: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isOutputStreamFileAlreadyExists { return true } return false } var isOutputStreamURLInvalid: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isOutputStreamURLInvalid { return true } return false } var isOutputStreamWriteFailed: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isOutputStreamWriteFailed { return true } return false } var isInputStreamReadFailed: Bool { if case let .multipartEncodingFailed(reason) = self, reason.isInputStreamReadFailed { return true } return false } // ResponseSerializationFailureReason var isInputDataNil: Bool { if case let .responseSerializationFailed(reason) = self, reason.isInputDataNil { return true } return false } var isInputDataNilOrZeroLength: Bool { if case let .responseSerializationFailed(reason) = self, reason.isInputDataNilOrZeroLength { return true } return false } var isInputFileNil: Bool { if case let .responseSerializationFailed(reason) = self, reason.isInputFileNil { return true } return false } var isInputFileReadFailed: Bool { if case let .responseSerializationFailed(reason) = self, reason.isInputFileReadFailed { return true } return false } var isStringSerializationFailed: Bool { if case let .responseSerializationFailed(reason) = self, reason.isStringSerializationFailed { return true } return false } var isJSONSerializationFailed: Bool { if case let .responseSerializationFailed(reason) = self, reason.isJSONSerializationFailed { return true } return false } var isPropertyListSerializationFailed: Bool { if case let .responseSerializationFailed(reason) = self, reason.isPropertyListSerializationFailed { return true } return false } // ResponseValidationFailureReason var isDataFileNil: Bool { if case let .responseValidationFailed(reason) = self, reason.isDataFileNil { return true } return false } var isDataFileReadFailed: Bool { if case let .responseValidationFailed(reason) = self, reason.isDataFileReadFailed { return true } return false } var isMissingContentType: Bool { if case let .responseValidationFailed(reason) = self, reason.isMissingContentType { return true } return false } var isUnacceptableContentType: Bool { if case let .responseValidationFailed(reason) = self, reason.isUnacceptableContentType { return true } return false } var isUnacceptableStatusCode: Bool { if case let .responseValidationFailed(reason) = self, reason.isUnacceptableStatusCode { return true } return false } } // MARK: - extension CLDNError.ParameterEncodingFailureReason { var isMissingURL: Bool { if case .missingURL = self { return true } return false } var isJSONEncodingFailed: Bool { if case .jsonEncodingFailed = self { return true } return false } var isPropertyListEncodingFailed: Bool { if case .propertyListEncodingFailed = self { return true } return false } } // MARK: - extension CLDNError.MultipartEncodingFailureReason { var isBodyPartURLInvalid: Bool { if case .bodyPartURLInvalid = self { return true } return false } var isBodyPartFilenameInvalid: Bool { if case .bodyPartFilenameInvalid = self { return true } return false } var isBodyPartFileNotReachable: Bool { if case .bodyPartFileNotReachable = self { return true } return false } var isBodyPartFileNotReachableWithError: Bool { if case .bodyPartFileNotReachableWithError = self { return true } return false } var isBodyPartFileIsDirectory: Bool { if case .bodyPartFileIsDirectory = self { return true } return false } var isBodyPartFileSizeNotAvailable: Bool { if case .bodyPartFileSizeNotAvailable = self { return true } return false } var isBodyPartFileSizeQueryFailedWithError: Bool { if case .bodyPartFileSizeQueryFailedWithError = self { return true } return false } var isBodyPartInputStreamCreationFailed: Bool { if case .bodyPartInputStreamCreationFailed = self { return true } return false } var isOutputStreamCreationFailed: Bool { if case .outputStreamCreationFailed = self { return true } return false } var isOutputStreamFileAlreadyExists: Bool { if case .outputStreamFileAlreadyExists = self { return true } return false } var isOutputStreamURLInvalid: Bool { if case .outputStreamURLInvalid = self { return true } return false } var isOutputStreamWriteFailed: Bool { if case .outputStreamWriteFailed = self { return true } return false } var isInputStreamReadFailed: Bool { if case .inputStreamReadFailed = self { return true } return false } } // MARK: - extension CLDNError.ResponseSerializationFailureReason { var isInputDataNil: Bool { if case .inputDataNil = self { return true } return false } var isInputDataNilOrZeroLength: Bool { if case .inputDataNilOrZeroLength = self { return true } return false } var isInputFileNil: Bool { if case .inputFileNil = self { return true } return false } var isInputFileReadFailed: Bool { if case .inputFileReadFailed = self { return true } return false } var isStringSerializationFailed: Bool { if case .stringSerializationFailed = self { return true } return false } var isJSONSerializationFailed: Bool { if case .jsonSerializationFailed = self { return true } return false } var isPropertyListSerializationFailed: Bool { if case .propertyListSerializationFailed = self { return true } return false } } // MARK: - extension CLDNError.ResponseValidationFailureReason { var isDataFileNil: Bool { if case .dataFileNil = self { return true } return false } var isDataFileReadFailed: Bool { if case .dataFileReadFailed = self { return true } return false } var isMissingContentType: Bool { if case .missingContentType = self { return true } return false } var isUnacceptableContentType: Bool { if case .unacceptableContentType = self { return true } return false } var isUnacceptableStatusCode: Bool { if case .unacceptableStatusCode = self { return true } return false } } ================================================ FILE: Example/Tests/BaseNetwork/Extensions/CLDNResult+CloudinaryTests.swift ================================================ // // CLDNResult+CloudinaryTests.swift // Cloudinary_Tests // // Created on 30/03/2020. // Copyright (c) 2020 Cloudinary. All rights reserved. // @testable import Cloudinary import Foundation // MARK: - MAP Method extension CLDNResult { /// Evaluates the specified closure when the `CLDNResult` is a success, passing the unwrapped value as a parameter. /// /// Use the `map` method with a closure that does not throw. For example: /// /// let possibleData: CLDNResult = .success(Data()) /// let possibleInt = possibleData.map { $0.count } /// try print(possibleInt.unwrap()) /// // Prints "0" /// /// let noData: CLDNResult = .failure(error) /// let noInt = noData.map { $0.count } /// try print(noInt.unwrap()) /// // Throws error /// /// - parameter transform: A closure that takes the success value of the `CLDNResult` instance. /// /// - returns: A `CLDNResult` containing the result of the given closure. If this instance is a failure, returns the /// same failure. internal func map(_ transform: (Value) -> T) -> CLDNResult { switch self { case .success(let value): return .success(transform(value)) case .failure(let error): return .failure(error) } } /// Evaluates the specified closure when the `CLDNResult` is a success, passing the unwrapped value as a parameter. /// /// Use the `flatMap` method with a closure that may throw an error. For example: /// /// let possibleData: CLDNResult = .success(Data(...)) /// let possibleObject = possibleData.flatMap { /// try JSONSerialization.jsonObject(with: $0) /// } /// /// - parameter transform: A closure that takes the success value of the instance. /// /// - returns: A `CLDNResult` containing the result of the given closure. If this instance is a failure, returns the /// same failure. internal func flatMap(_ transform: (Value) throws -> T) -> CLDNResult { switch self { case .success(let value): do { return try .success(transform(value)) } catch { return .failure(error) } case .failure(let error): return .failure(error) } } /// Evaluates the specified closure when the `CLDNResult` is a failure, passing the unwrapped error as a parameter. /// /// Use the `mapError` function with a closure that does not throw. For example: /// /// let possibleData: CLDNResult = .failure(someError) /// let withMyError: CLDNResult = possibleData.mapError { MyError.error($0) } /// /// - Parameter transform: A closure that takes the error of the instance. /// - Returns: A `CLDNResult` instance containing the result of the transform. If this instance is a success, returns /// the same instance. internal func mapError(_ transform: (Error) -> T) -> CLDNResult { switch self { case .failure(let error): return .failure(transform(error)) case .success: return self } } /// Evaluates the specified closure when the `CLDNResult` is a failure, passing the unwrapped error as a parameter. /// /// Use the `flatMapError` function with a closure that may throw an error. For example: /// /// let possibleData: CLDNResult = .success(Data(...)) /// let possibleObject = possibleData.flatMapError { /// try someFailableFunction(taking: $0) /// } /// /// - Parameter transform: A throwing closure that takes the error of the instance. /// /// - Returns: A `CLDNResult` instance containing the result of the transform. If this instance is a success, returns /// the same instance. internal func flatMapError(_ transform: (Error) throws -> T) -> CLDNResult { switch self { case .failure(let error): do { return try .failure(transform(error)) } catch { return .failure(error) } case .success: return self } } } ================================================ FILE: Example/Tests/BaseNetwork/Extensions/FileManager+CloudinaryTests.swift ================================================ // // FileManager+CloudinaryTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Foundation extension FileManager { // MARK: - Common Directories static var temporaryDirectoryPath: String { return NSTemporaryDirectory() } static var temporaryDirectoryURL: URL { return URL(fileURLWithPath: FileManager.temporaryDirectoryPath, isDirectory: true) } // MARK: - File System Modification @discardableResult static func createDirectory(atPath path: String) -> Bool { do { try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) return true } catch { return false } } @discardableResult static func createDirectory(at url: URL) -> Bool { return createDirectory(atPath: url.path) } @discardableResult static func removeItem(atPath path: String) -> Bool { do { try FileManager.default.removeItem(atPath: path) return true } catch { return false } } @discardableResult static func removeItem(at url: URL) -> Bool { return removeItem(atPath: url.path) } @discardableResult static func removeAllItemsInsideDirectory(atPath path: String) -> Bool { let enumerator = FileManager.default.enumerator(atPath: path) var result = true while let fileName = enumerator?.nextObject() as? String { let success = removeItem(atPath: path + "/\(fileName)") if !success { result = false } } return result } @discardableResult static func removeAllItemsInsideDirectory(at url: URL) -> Bool { return removeAllItemsInsideDirectory(atPath: url.path) } } ================================================ FILE: Example/Tests/BaseNetwork/Features/CacheTests.swift ================================================ // // CacheTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest /// This test case tests all implemented cache policies against various `Cache-Control` header values. These tests /// are meant to cover the main cases of `Cache-Control` header usage, but are by no means exhaustive. /// /// These tests work as follows: /// /// - Set up an `URLCache` /// - Set up an `Cloudinary.CLDNSessionManager` /// - Execute requests for all `Cache-Control` header values to prime the `NSURLCache` with cached responses /// - Start up a new test /// - Execute another round of the same requests with a given `URLRequestCachePolicy` /// - Verify whether the response came from the cache or from the network /// - This is determined by whether the cached response timestamp matches the new response timestamp /// /// An important thing to note is the difference in behavior between iOS and macOS. On iOS, a response with /// a `Cache-Control` header value of `no-store` is still written into the `NSURLCache` where on macOS, it is not. /// The different tests below reflect and demonstrate this behavior. /// /// For information about `Cache-Control` HTTP headers, please refer to RFC 2616 - Section 14.9. class CacheTestCase: BaseTestCase { // MARK: - struct CacheControl { static let publicControl = "public" static let privateControl = "private" static let maxAgeNonExpired = "max-age=3600" static let maxAgeExpired = "max-age=0" static let noCache = "no-cache" static let noStore = "no-store" static var allValues: [String] { return [ CacheControl.publicControl, CacheControl.privateControl, CacheControl.maxAgeNonExpired, CacheControl.maxAgeExpired, CacheControl.noCache, CacheControl.noStore ] } } // MARK: - Properties var urlCache: URLCache! var manager: CLDNSessionManager! let urlString = "https://httpbin.org/response-headers" let requestTimeout: TimeInterval = 30 var requests: [String: URLRequest] = [:] var timestamps: [String: String] = [:] // MARK: - setup and teardown override func setUp() { super.setUp() urlCache = { let capacity = 50 * 1024 * 1024 // MBs let urlCache = URLCache(memoryCapacity: capacity, diskCapacity: capacity, diskPath: nil) return urlCache }() manager = { let configuration: URLSessionConfiguration = { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = CLDNSessionManager.defaultHTTPHeaders configuration.requestCachePolicy = .useProtocolCachePolicy configuration.urlCache = urlCache return configuration }() let manager = CLDNSessionManager(configuration: configuration) return manager }() // primeCachedResponses() } override func tearDown() { super.tearDown() requests.removeAll() timestamps.removeAll() urlCache.removeAllCachedResponses() } // MARK: - Cache Priming Methods /** Executes a request for all `Cache-Control` header values to load the response into the `URLCache`. This implementation leverages dispatch groups to execute all the requests as well as wait an additional second before returning. This ensures the cache contains responses for all requests that are at least one second old. This allows the tests to distinguish whether the subsequent responses come from the cache or the network based on the timestamp of the response. */ func primeCachedResponses() { let dispatchGroup = DispatchGroup() let serialQueue = DispatchQueue(label: "com.cloudinary.cache-tests") for cacheControl in CacheControl.allValues { dispatchGroup.enter() let request = startRequest( cacheControl: cacheControl, queue: serialQueue, completion: { _, response in let timestamp = response!.allHeaderFields["Date"] as! String self.timestamps[cacheControl] = timestamp dispatchGroup.leave() } ) requests[cacheControl] = request } // Wait for all requests to complete _ = dispatchGroup.wait(timeout: DispatchTime.now() + Double(Int64(30.0 * Float(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) // Pause for 2 additional seconds to ensure all timestamps will be different dispatchGroup.enter() serialQueue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(2.0 * Float(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { dispatchGroup.leave() } // Wait for our 2 second pause to complete _ = dispatchGroup.wait(timeout: DispatchTime.now() + Double(Int64(10.0 * Float(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) } // MARK: - Request Helper Methods func urlRequest(cacheControl: String, cachePolicy: NSURLRequest.CachePolicy) -> URLRequest { let parameters = ["Cache-Control": cacheControl] let url = URL(string: urlString)! var urlRequest = URLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: requestTimeout) urlRequest.httpMethod = CLDNHTTPMethod.get.rawValue do { return try CLDNURLEncoding.default.CLDN_Encode(urlRequest, with: parameters) } catch { return urlRequest } } @discardableResult func startRequest( cacheControl: String, cachePolicy: NSURLRequest.CachePolicy = .useProtocolCachePolicy, queue: DispatchQueue = DispatchQueue.main, completion: @escaping (URLRequest?, HTTPURLResponse?) -> Void) -> URLRequest { let urlRequest = self.urlRequest(cacheControl: cacheControl, cachePolicy: cachePolicy) let request = manager.request(urlRequest) request.response( queue: queue, completionHandler: { response in completion(response.request, response.response) } ) return urlRequest } // MARK: - Test Execution and Verification func executeTest( cachePolicy: NSURLRequest.CachePolicy, cacheControl: String, shouldReturnCachedResponse: Bool) { // Given let expectation = self.expectation(description: "GET request to httpbin") var response: HTTPURLResponse? // When startRequest(cacheControl: cacheControl, cachePolicy: cachePolicy) { _, responseResponse in response = responseResponse expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then verifyResponse(response, forCacheControl: cacheControl, isCachedResponse: shouldReturnCachedResponse) } func verifyResponse(_ response: HTTPURLResponse?, forCacheControl cacheControl: String, isCachedResponse: Bool) { guard let cachedResponseTimestamp = timestamps[cacheControl] else { XCTFail("cached response timestamp should not be nil") return } if let response = response, let timestamp = response.allHeaderFields["Date"] as? String { if isCachedResponse { XCTAssertEqual(timestamp, cachedResponseTimestamp, "timestamps should be equal") } else { XCTAssertNotEqual(timestamp, cachedResponseTimestamp, "timestamps should not be equal") } } else { XCTFail("response should not be nil") } } // MARK: - Cache Helper Methods private func isCachedResponseForNoStoreHeaderExpected() -> Bool { #if os(iOS) if #available(iOS 8.3, *) { return false } else { return true } #else return false #endif } // MARK: - Tests // func testURLCacheContainsCachedResponsesForAllRequests() { // // Given // let publicRequest = requests[CacheControl.publicControl]! // let privateRequest = requests[CacheControl.privateControl]! // let maxAgeNonExpiredRequest = requests[CacheControl.maxAgeNonExpired]! // let maxAgeExpiredRequest = requests[CacheControl.maxAgeExpired]! // let noCacheRequest = requests[CacheControl.noCache]! // let noStoreRequest = requests[CacheControl.noStore]! // // // When // let publicResponse = urlCache.cachedResponse(for: publicRequest) // let privateResponse = urlCache.cachedResponse(for: privateRequest) // let maxAgeNonExpiredResponse = urlCache.cachedResponse(for: maxAgeNonExpiredRequest) // let maxAgeExpiredResponse = urlCache.cachedResponse(for: maxAgeExpiredRequest) // let noCacheResponse = urlCache.cachedResponse(for: noCacheRequest) // let noStoreResponse = urlCache.cachedResponse(for: noStoreRequest) // // // Then // XCTAssertNotNil(publicResponse, "\(CacheControl.publicControl) response should not be nil") // XCTAssertNotNil(privateResponse, "\(CacheControl.privateControl) response should not be nil") // XCTAssertNotNil(maxAgeNonExpiredResponse, "\(CacheControl.maxAgeNonExpired) response should not be nil") // XCTAssertNotNil(maxAgeExpiredResponse, "\(CacheControl.maxAgeExpired) response should not be nil") // XCTAssertNotNil(noCacheResponse, "\(CacheControl.noCache) response should not be nil") // // if isCachedResponseForNoStoreHeaderExpected() { // XCTAssertNotNil(noStoreResponse, "\(CacheControl.noStore) response should not be nil") // } else { // XCTAssertNil(noStoreResponse, "\(CacheControl.noStore) response should be nil") // } // } // func skipped_testDefaultCachePolicy() { // let cachePolicy: NSURLRequest.CachePolicy = .useProtocolCachePolicy // // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.publicControl, shouldReturnCachedResponse: false) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.privateControl, shouldReturnCachedResponse: false) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeNonExpired, shouldReturnCachedResponse: true) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeExpired, shouldReturnCachedResponse: false) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noCache, shouldReturnCachedResponse: false) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noStore, shouldReturnCachedResponse: false) // } // func testIgnoreLocalCacheDataPolicy() { // let cachePolicy: NSURLRequest.CachePolicy = .reloadIgnoringLocalCacheData // // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.publicControl, shouldReturnCachedResponse: false) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.privateControl, shouldReturnCachedResponse: false) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeNonExpired, shouldReturnCachedResponse: false) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeExpired, shouldReturnCachedResponse: false) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noCache, shouldReturnCachedResponse: false) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noStore, shouldReturnCachedResponse: false) // } // func testUseLocalCacheDataIfExistsOtherwiseLoadFromNetworkPolicy() { // let cachePolicy: NSURLRequest.CachePolicy = .returnCacheDataElseLoad // // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.publicControl, shouldReturnCachedResponse: true) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.privateControl, shouldReturnCachedResponse: true) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeNonExpired, shouldReturnCachedResponse: true) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeExpired, shouldReturnCachedResponse: true) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noCache, shouldReturnCachedResponse: true) // // if isCachedResponseForNoStoreHeaderExpected() { // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noStore, shouldReturnCachedResponse: true) // } else { // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noStore, shouldReturnCachedResponse: false) // } // } // func testUseLocalCacheDataAndDontLoadFromNetworkPolicy() { // let cachePolicy: NSURLRequest.CachePolicy = .returnCacheDataDontLoad // // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.publicControl, shouldReturnCachedResponse: true) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.privateControl, shouldReturnCachedResponse: true) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeNonExpired, shouldReturnCachedResponse: true) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeExpired, shouldReturnCachedResponse: true) // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noCache, shouldReturnCachedResponse: true) // // if isCachedResponseForNoStoreHeaderExpected() { // executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noStore, shouldReturnCachedResponse: true) // } else { // // Given // let expectation = self.expectation(description: "GET request to httpbin") // var response: HTTPURLResponse? // // // When // startRequest(cacheControl: CacheControl.noStore, cachePolicy: cachePolicy) { _, responseResponse in // response = responseResponse // expectation.fulfill() // } // // waitForExpectations(timeout: timeout, handler: nil) // // // Then // XCTAssertNil(response, "response should be nil") // } // } } ================================================ FILE: Example/Tests/BaseNetwork/Features/MultipartFormDataTests.swift ================================================ // // MultipartFormDataTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest struct EncodingCharacters { static let crlf = "\r\n" } struct BoundaryGenerator { enum BoundaryType { case initial, encapsulated, final } static func boundary(forBoundaryType boundaryType: BoundaryType, boundaryKey: String) -> String { let boundary: String switch boundaryType { case .initial: boundary = "--\(boundaryKey)\(EncodingCharacters.crlf)" case .encapsulated: boundary = "\(EncodingCharacters.crlf)--\(boundaryKey)\(EncodingCharacters.crlf)" case .final: boundary = "\(EncodingCharacters.crlf)--\(boundaryKey)--\(EncodingCharacters.crlf)" } return boundary } static func boundaryData(boundaryType: BoundaryType, boundaryKey: String) -> Data { return BoundaryGenerator.boundary( forBoundaryType: boundaryType, boundaryKey: boundaryKey ).data(using: .utf8, allowLossyConversion: false)! } } private func temporaryFileURL() -> URL { return BaseTestCase.testDirectoryURL.appendingPathComponent(UUID().uuidString) } // MARK: - class MultipartFormDataPropertiesTestCase: BaseTestCase { func testThatContentTypeContainsBoundary() { // Given let multipartFormData = CLDNMultipartFormData() // When let boundary = multipartFormData.boundary // Then let expectedContentType = "multipart/form-data; boundary=\(boundary)" XCTAssertEqual(multipartFormData.contentType, expectedContentType, "contentType should match expected value") } func testThatContentLengthMatchesTotalBodyPartSize() { // Given let multipartFormData = CLDNMultipartFormData() let data1 = "Lorem ipsum dolor sit amet.".data(using: .utf8, allowLossyConversion: false)! let data2 = "Vim at integre alterum.".data(using: .utf8, allowLossyConversion: false)! // When multipartFormData.append(data1, withName: "data1") multipartFormData.append(data2, withName: "data2") // Then let expectedContentLength = UInt64(data1.count + data2.count) XCTAssertEqual(multipartFormData.contentLength, expectedContentLength, "content length should match expected value") } } // MARK: - class MultipartFormDataEncodingTestCase: BaseTestCase { let crlf = EncodingCharacters.crlf func testEncodingDataBodyPart() { // Given let multipartFormData = CLDNMultipartFormData() let data = "Lorem ipsum dolor sit amet.".data(using: .utf8, allowLossyConversion: false)! multipartFormData.append(data, withName: "data") var encodedData: Data? // When do { encodedData = try multipartFormData.encode() } catch { // No-op } // Then XCTAssertNotNil(encodedData, "encoded data should not be nil") if let encodedData = encodedData { let boundary = multipartFormData.boundary let expectedData = ( BoundaryGenerator.boundary(forBoundaryType: .initial, boundaryKey: boundary) + "Content-Disposition: form-data; name=\"data\"\(crlf)\(crlf)" + "Lorem ipsum dolor sit amet." + BoundaryGenerator.boundary(forBoundaryType: .final, boundaryKey: boundary) ).data(using: .utf8, allowLossyConversion: false)! XCTAssertEqual(encodedData, expectedData, "encoded data should match expected data") } } func testEncodingMultipleDataBodyParts() { // Given let multipartFormData = CLDNMultipartFormData() let frenchData = Data("français".utf8) let japaneseData = Data("日本語".utf8) let emojiData = Data("😃👍🏻🍻🎉".utf8) multipartFormData.append(frenchData, withName: "french") multipartFormData.append(japaneseData, withName: "japanese", mimeType: "text/plain") multipartFormData.append(emojiData, withName: "emoji", mimeType: "text/plain") var encodedData: Data? // When do { encodedData = try multipartFormData.encode() } catch { // No-op } // Then XCTAssertNotNil(encodedData, "encoded data should not be nil") if let encodedData = encodedData { let boundary = multipartFormData.boundary let expectedString = ( BoundaryGenerator.boundary(forBoundaryType: .initial, boundaryKey: boundary) + "Content-Disposition: form-data; name=\"french\"\(crlf)\(crlf)" + "français" + BoundaryGenerator.boundary(forBoundaryType: .encapsulated, boundaryKey: boundary) + "Content-Type: text/plain\(crlf)" + "Content-Disposition: form-data; name=\"japanese\"\(crlf)\(crlf)" + "日本語" + BoundaryGenerator.boundary(forBoundaryType: .encapsulated, boundaryKey: boundary) + "Content-Type: text/plain\(crlf)" + "Content-Disposition: form-data; name=\"emoji\"\(crlf)\(crlf)" + "😃👍🏻🍻🎉" + BoundaryGenerator.boundary(forBoundaryType: .final, boundaryKey: boundary) ) let expectedData = Data(expectedString.utf8) // XCTAssertEqual(encodedData, expectedData, "encoded data should match expected data") XCTAssertEqual(encodedData.count, expectedData.count) } } func testEncodingFileBodyPart() { // Given let multipartFormData = CLDNMultipartFormData() let unicornImageURL = url(forResource: "unicorn", withExtension: "png") multipartFormData.append(unicornImageURL, withName: "unicorn") var encodedData: Data? // When do { encodedData = try multipartFormData.encode() } catch { // No-op } // Then XCTAssertNotNil(encodedData, "encoded data should not be nil") if let encodedData = encodedData { let boundary = multipartFormData.boundary var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data(( "Content-Type: image/png\(crlf)" + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary)) // XCTAssertEqual(encodedData, expectedData, "data should match expected data") XCTAssertEqual(encodedData.count, expectedData.count) } } func testEncodingMultipleFileBodyParts() { // Given let multipartFormData = CLDNMultipartFormData() let unicornImageURL = url(forResource: "unicorn", withExtension: "png") let rainbowImageURL = url(forResource: "rainbow", withExtension: "jpg") multipartFormData.append(unicornImageURL, withName: "unicorn") multipartFormData.append(rainbowImageURL, withName: "rainbow") var encodedData: Data? // When do { encodedData = try multipartFormData.encode() } catch { // No-op } // Then XCTAssertNotNil(encodedData, "encoded data should not be nil") if let encodedData = encodedData { let boundary = multipartFormData.boundary var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data(( "Content-Type: image/png\(crlf)" + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedData.append(Data(( "Content-Type: image/jpeg\(crlf)" + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: rainbowImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary)) // XCTAssertEqual(encodedData, expectedData, "data should match expected data") XCTAssertEqual(encodedData.count, expectedData.count) } } func testEncodingStreamBodyPart() { // Given let multipartFormData = CLDNMultipartFormData() let unicornImageURL = url(forResource: "unicorn", withExtension: "png") let unicornDataLength = UInt64((try! Data(contentsOf: unicornImageURL)).count) let unicornStream = InputStream(url: unicornImageURL)! multipartFormData.append( unicornStream, withLength: unicornDataLength, name: "unicorn", fileName: "unicorn.png", mimeType: "image/png" ) var encodedData: Data? // When do { encodedData = try multipartFormData.encode() } catch { // No-op } // Then XCTAssertNotNil(encodedData, "encoded data should not be nil") if let encodedData = encodedData { let boundary = multipartFormData.boundary var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data(( "Content-Type: image/png\(crlf)" + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary)) // XCTAssertEqual(encodedData, expectedData, "data should match expected data") XCTAssertEqual(encodedData.count, expectedData.count) } } func testEncodingMultipleStreamBodyParts() { // Given let multipartFormData = CLDNMultipartFormData() let unicornImageURL = url(forResource: "unicorn", withExtension: "png") let unicornDataLength = UInt64((try! Data(contentsOf: unicornImageURL)).count) let unicornStream = InputStream(url: unicornImageURL)! let rainbowImageURL = url(forResource: "rainbow", withExtension: "jpg") let rainbowDataLength = UInt64((try! Data(contentsOf: rainbowImageURL)).count) let rainbowStream = InputStream(url: rainbowImageURL)! multipartFormData.append( unicornStream, withLength: unicornDataLength, name: "unicorn", fileName: "unicorn.png", mimeType: "image/png" ) multipartFormData.append( rainbowStream, withLength: rainbowDataLength, name: "rainbow", fileName: "rainbow.jpg", mimeType: "image/jpeg" ) var encodedData: Data? // When do { encodedData = try multipartFormData.encode() } catch { // No-op } // Then XCTAssertNotNil(encodedData, "encoded data should not be nil") if let encodedData = encodedData { let boundary = multipartFormData.boundary var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data(( "Content-Type: image/png\(crlf)" + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedData.append(Data(( "Content-Type: image/jpeg\(crlf)" + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: rainbowImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary)) // XCTAssertEqual(encodedData, expectedData, "data should match expected data") XCTAssertEqual(encodedData.count, expectedData.count) } } func testEncodingMultipleBodyPartsWithVaryingTypes() { // Given let multipartFormData = CLDNMultipartFormData() let loremData = Data("Lorem ipsum.".utf8) let unicornImageURL = url(forResource: "unicorn", withExtension: "png") let rainbowImageURL = url(forResource: "rainbow", withExtension: "jpg") let rainbowDataLength = UInt64((try! Data(contentsOf: rainbowImageURL)).count) let rainbowStream = InputStream(url: rainbowImageURL)! multipartFormData.append(loremData, withName: "lorem") multipartFormData.append(unicornImageURL, withName: "unicorn") multipartFormData.append( rainbowStream, withLength: rainbowDataLength, name: "rainbow", fileName: "rainbow.jpg", mimeType: "image/jpeg" ) var encodedData: Data? // When do { encodedData = try multipartFormData.encode() } catch { // No-op } // Then XCTAssertNotNil(encodedData, "encoded data should not be nil") if let encodedData = encodedData { let boundary = multipartFormData.boundary var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data( "Content-Disposition: form-data; name=\"lorem\"\(crlf)\(crlf)".utf8 ) ) expectedData.append(loremData) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedData.append(Data(( "Content-Type: image/png\(crlf)" + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedData.append(Data(( "Content-Type: image/jpeg\(crlf)" + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: rainbowImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary)) // XCTAssertEqual(encodedData, expectedData, "data should match expected data") XCTAssertEqual(encodedData.count, expectedData.count) } } } // MARK: - class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { let crlf = EncodingCharacters.crlf func testWritingEncodedDataBodyPartToDisk() { // Given let fileURL = temporaryFileURL() let multipartFormData = CLDNMultipartFormData() let data = "Lorem ipsum dolor sit amet.".data(using: .utf8, allowLossyConversion: false)! multipartFormData.append(data, withName: "data") var encodingError: Error? // When do { try multipartFormData.writeEncodedData(to: fileURL) } catch { encodingError = error } // Then XCTAssertNil(encodingError, "encoding error should be nil") if let fileData = try? Data(contentsOf: fileURL) { let boundary = multipartFormData.boundary let expectedFileData = ( BoundaryGenerator.boundary(forBoundaryType: .initial, boundaryKey: boundary) + "Content-Disposition: form-data; name=\"data\"\(crlf)\(crlf)" + "Lorem ipsum dolor sit amet." + BoundaryGenerator.boundary(forBoundaryType: .final, boundaryKey: boundary) ).data(using: .utf8, allowLossyConversion: false)! XCTAssertEqual(fileData, expectedFileData, "file data should match expected file data") } else { XCTFail("file data should not be nil") } } func testWritingMultipleEncodedDataBodyPartsToDisk() { // Given let fileURL = temporaryFileURL() let multipartFormData = CLDNMultipartFormData() let frenchData = "français".data(using: .utf8, allowLossyConversion: false)! let japaneseData = "日本語".data(using: .utf8, allowLossyConversion: false)! let emojiData = "😃👍🏻🍻🎉".data(using: .utf8, allowLossyConversion: false)! multipartFormData.append(frenchData, withName: "french") multipartFormData.append(japaneseData, withName: "japanese") multipartFormData.append(emojiData, withName: "emoji") var encodingError: Error? // When do { try multipartFormData.writeEncodedData(to: fileURL) } catch { encodingError = error } // Then XCTAssertNil(encodingError, "encoding error should be nil") if let fileData = try? Data(contentsOf: fileURL) { let boundary = multipartFormData.boundary let expectedFileData = ( BoundaryGenerator.boundary(forBoundaryType: .initial, boundaryKey: boundary) + "Content-Disposition: form-data; name=\"french\"\(crlf)\(crlf)" + "français" + BoundaryGenerator.boundary(forBoundaryType: .encapsulated, boundaryKey: boundary) + "Content-Disposition: form-data; name=\"japanese\"\(crlf)\(crlf)" + "日本語" + BoundaryGenerator.boundary(forBoundaryType: .encapsulated, boundaryKey: boundary) + "Content-Disposition: form-data; name=\"emoji\"\(crlf)\(crlf)" + "😃👍🏻🍻🎉" + BoundaryGenerator.boundary(forBoundaryType: .final, boundaryKey: boundary) ).data(using: .utf8, allowLossyConversion: false)! XCTAssertEqual(fileData, expectedFileData, "file data should match expected file data") } else { XCTFail("file data should not be nil") } } func testWritingEncodedFileBodyPartToDisk() { // Given let fileURL = temporaryFileURL() let multipartFormData = CLDNMultipartFormData() let unicornImageURL = url(forResource: "unicorn", withExtension: "png") multipartFormData.append(unicornImageURL, withName: "unicorn") var encodingError: Error? // When do { try multipartFormData.writeEncodedData(to: fileURL) } catch { encodingError = error } // Then XCTAssertNil(encodingError, "encoding error should be nil") if let fileData = try? Data(contentsOf: fileURL) { let boundary = multipartFormData.boundary var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data(( "Content-Type: image/png\(crlf)" + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary)) // XCTAssertEqual(fileData, expectedFileData, "file data should match expected file data") XCTAssertEqual(fileData.count, expectedFileData.count) } else { XCTFail("file data should not be nil") } } func testWritingMultipleEncodedFileBodyPartsToDisk() { // Given let fileURL = temporaryFileURL() let multipartFormData = CLDNMultipartFormData() let unicornImageURL = url(forResource: "unicorn", withExtension: "png") let rainbowImageURL = url(forResource: "rainbow", withExtension: "jpg") multipartFormData.append(unicornImageURL, withName: "unicorn") multipartFormData.append(rainbowImageURL, withName: "rainbow") var encodingError: Error? // When do { try multipartFormData.writeEncodedData(to: fileURL) } catch { encodingError = error } // Then XCTAssertNil(encodingError, "encoding error should be nil") if let fileData = try? Data(contentsOf: fileURL) { let boundary = multipartFormData.boundary var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data(( "Content-Type: image/png\(crlf)" + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedFileData.append(Data(( "Content-Type: image/jpeg\(crlf)" + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: rainbowImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary)) // XCTAssertEqual(fileData, expectedFileData, "file data should match expected file data") XCTAssertEqual(fileData.count, expectedFileData.count) } else { XCTFail("file data should not be nil") } } func testWritingEncodedStreamBodyPartToDisk() { // Given let fileURL = temporaryFileURL() let multipartFormData = CLDNMultipartFormData() let unicornImageURL = url(forResource: "unicorn", withExtension: "png") let unicornDataLength = UInt64((try! Data(contentsOf: unicornImageURL)).count) let unicornStream = InputStream(url: unicornImageURL)! multipartFormData.append( unicornStream, withLength: unicornDataLength, name: "unicorn", fileName: "unicorn.png", mimeType: "image/png" ) var encodingError: Error? // When do { try multipartFormData.writeEncodedData(to: fileURL) } catch { encodingError = error } // Then XCTAssertNil(encodingError, "encoding error should be nil") if let fileData = try? Data(contentsOf: fileURL) { let boundary = multipartFormData.boundary var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data(( "Content-Type: image/png\(crlf)" + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary)) // XCTAssertEqual(fileData, expectedFileData, "file data should match expected file data") XCTAssertEqual(fileData.count, expectedFileData.count) } else { XCTFail("file data should not be nil") } } func testWritingMultipleEncodedStreamBodyPartsToDisk() { // Given let fileURL = temporaryFileURL() let multipartFormData = CLDNMultipartFormData() let unicornImageURL = url(forResource: "unicorn", withExtension: "png") let unicornDataLength = UInt64((try! Data(contentsOf: unicornImageURL)).count) let unicornStream = InputStream(url: unicornImageURL)! let rainbowImageURL = url(forResource: "rainbow", withExtension: "jpg") let rainbowDataLength = UInt64((try! Data(contentsOf: rainbowImageURL)).count) let rainbowStream = InputStream(url: rainbowImageURL)! multipartFormData.append( unicornStream, withLength: unicornDataLength, name: "unicorn", fileName: "unicorn.png", mimeType: "image/png" ) multipartFormData.append( rainbowStream, withLength: rainbowDataLength, name: "rainbow", fileName: "rainbow.jpg", mimeType: "image/jpeg" ) var encodingError: Error? // When do { try multipartFormData.writeEncodedData(to: fileURL) } catch { encodingError = error } // Then XCTAssertNil(encodingError, "encoding error should be nil") if let fileData = try? Data(contentsOf: fileURL) { let boundary = multipartFormData.boundary var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data(( "Content-Type: image/png\(crlf)" + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedFileData.append(Data(( "Content-Type: image/jpeg\(crlf)" + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: rainbowImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary)) // XCTAssertEqual(fileData, expectedFileData, "file data should match expected file data") XCTAssertEqual(fileData.count, expectedFileData.count) } else { XCTFail("file data should not be nil") } } func testWritingMultipleEncodedBodyPartsWithVaryingTypesToDisk() { // Given let fileURL = temporaryFileURL() let multipartFormData = CLDNMultipartFormData() let loremData = Data("Lorem ipsum.".utf8) let unicornImageURL = url(forResource: "unicorn", withExtension: "png") let rainbowImageURL = url(forResource: "rainbow", withExtension: "jpg") let rainbowDataLength = UInt64((try! Data(contentsOf: rainbowImageURL)).count) let rainbowStream = InputStream(url: rainbowImageURL)! multipartFormData.append(loremData, withName: "lorem") multipartFormData.append(unicornImageURL, withName: "unicorn") multipartFormData.append( rainbowStream, withLength: rainbowDataLength, name: "rainbow", fileName: "rainbow.jpg", mimeType: "image/jpeg" ) var encodingError: Error? // When do { try multipartFormData.writeEncodedData(to: fileURL) } catch { encodingError = error } // Then XCTAssertNil(encodingError, "encoding error should be nil") if let fileData = try? Data(contentsOf: fileURL) { let boundary = multipartFormData.boundary var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data( "Content-Disposition: form-data; name=\"lorem\"\(crlf)\(crlf)".utf8 ) ) expectedFileData.append(loremData) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedFileData.append(Data(( "Content-Type: image/png\(crlf)" + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedFileData.append(Data(( "Content-Type: image/jpeg\(crlf)" + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: rainbowImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary)) // XCTAssertEqual(fileData, expectedFileData, "file data should match expected file data") XCTAssertEqual(fileData.count, expectedFileData.count) } else { XCTFail("file data should not be nil") } } } // MARK: - class MultipartFormDataFailureTestCase: BaseTestCase { func testThatAppendingFileBodyPartWithInvalidLastPathComponentReturnsError() { // Given let fileURL = NSURL(string: "")! as URL let multipartFormData = CLDNMultipartFormData() multipartFormData.append(fileURL, withName: "empty_data") var encodingError: Error? // When do { _ = try multipartFormData.encode() } catch { encodingError = error } // Then XCTAssertNotNil(encodingError, "encoding error should not be nil") if let error = encodingError as? CLDNError { XCTAssertTrue(error.isBodyPartFilenameInvalid) let expectedFailureReason = "The URL provided does not have a valid filename: \(fileURL)" XCTAssertEqual(error.localizedDescription, expectedFailureReason, "failure reason does not match expected value") } else { XCTFail("Error should be CLDNError.") } } func testThatAppendingFileBodyPartThatIsNotFileURLReturnsError() { // Given let fileURL = URL(string: "https://example.com/image.jpg")! let multipartFormData = CLDNMultipartFormData() multipartFormData.append(fileURL, withName: "empty_data") var encodingError: Error? // When do { _ = try multipartFormData.encode() } catch { encodingError = error } // Then XCTAssertNotNil(encodingError, "encoding error should not be nil") if let error = encodingError as? CLDNError { XCTAssertTrue(error.isBodyPartURLInvalid) let expectedFailureReason = "The URL provided is not a file URL: \(fileURL)" XCTAssertEqual(error.localizedDescription, expectedFailureReason, "error failure reason does not match expected value") } else { XCTFail("Error should be CLDNError.") } } func testThatAppendingFileBodyPartThatIsNotReachableReturnsError() { // Given let filePath = (NSTemporaryDirectory() as NSString).appendingPathComponent("does_not_exist.jpg") let fileURL = URL(fileURLWithPath: filePath) let multipartFormData = CLDNMultipartFormData() multipartFormData.append(fileURL, withName: "empty_data") var encodingError: Error? // When do { _ = try multipartFormData.encode() } catch { encodingError = error } // Then XCTAssertNotNil(encodingError, "encoding error should not be nil") if let error = encodingError as? CLDNError { XCTAssertTrue(error.isBodyPartFileNotReachableWithError) } else { XCTFail("Error should be CLDNError.") } } func testThatAppendingFileBodyPartThatIsDirectoryReturnsError() { // Given let directoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let multipartFormData = CLDNMultipartFormData() multipartFormData.append(directoryURL, withName: "empty_data", fileName: "empty", mimeType: "application/octet") var encodingError: Error? // When do { _ = try multipartFormData.encode() } catch { encodingError = error } // Then XCTAssertNotNil(encodingError, "encoding error should not be nil") if let error = encodingError as? CLDNError { XCTAssertTrue(error.isBodyPartFileIsDirectory) let expectedFailureReason = "The URL provided is a directory: \(directoryURL)" XCTAssertEqual(error.localizedDescription, expectedFailureReason, "error failure reason does not match expected value") } else { XCTFail("Error should be CLDNError.") } } func testThatWritingEncodedDataToExistingFileURLFails() { // Given let fileURL = temporaryFileURL() var writerError: Error? do { try "dummy data".write(to: fileURL, atomically: true, encoding: String.Encoding.utf8) } catch { writerError = error } let multipartFormData = CLDNMultipartFormData() let data = "Lorem ipsum dolor sit amet.".data(using: .utf8, allowLossyConversion: false)! multipartFormData.append(data, withName: "data") var encodingError: Error? // When do { try multipartFormData.writeEncodedData(to: fileURL) } catch { encodingError = error } // Then XCTAssertNil(writerError, "writer error should be nil") XCTAssertNotNil(encodingError, "encoding error should not be nil") if let encodingError = encodingError as? CLDNError { XCTAssertTrue(encodingError.isOutputStreamFileAlreadyExists) } } func testThatWritingEncodedDataToBadURLFails() { // Given let fileURL = URL(string: "/this/is/not/a/valid/url")! let multipartFormData = CLDNMultipartFormData() let data = "Lorem ipsum dolor sit amet.".data(using: .utf8, allowLossyConversion: false)! multipartFormData.append(data, withName: "data") var encodingError: Error? // When do { try multipartFormData.writeEncodedData(to: fileURL) } catch { encodingError = error } // Then XCTAssertNotNil(encodingError, "encoding error should not be nil") if let encodingError = encodingError as? CLDNError { XCTAssertTrue(encodingError.isOutputStreamURLInvalid) } } } ================================================ FILE: Example/Tests/BaseNetwork/Features/ResponseSerializationTests.swift ================================================ // // ResponseSerializationTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest private func httpURLResponse(forStatusCode statusCode: Int, headers: CLDNHTTPHeaders = [:]) -> HTTPURLResponse { let url = URL(string: "https://httpbin.org/get")! return HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headers)! } // MARK: - class DataResponseSerializationTestCase: BaseTestCase { // MARK: Properties private let error = CLDNError.responseSerializationFailed(reason: .inputDataNil) // MARK: Tests - Data Response Serializer func testThatDataResponseSerializerSucceedsWhenDataIsNotNil() { // Given let serializer = CLDNDataRequest.dataResponseSerializer() let data = "data".data(using: .utf8)! // When let result = serializer.serializeResponse(nil, nil, data, nil) // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) } func testThatDataResponseSerializerFailsWhenDataIsNil() { // Given let serializer = CLDNDataRequest.dataResponseSerializer() // When let result = serializer.serializeResponse(nil, nil, nil, nil) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError { XCTAssertTrue(error.isInputDataNil) } else { XCTFail("error should not be nil") } } func testThatDataResponseSerializerFailsWhenErrorIsNotNil() { // Given let serializer = CLDNDataRequest.dataResponseSerializer() // When let result = serializer.serializeResponse(nil, nil, nil, error) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError { XCTAssertTrue(error.isInputDataNil) } else { XCTFail("error should not be nil") } } func testThatDataResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { // Given let serializer = CLDNDataRequest.dataResponseSerializer() let response = httpURLResponse(forStatusCode: 200) // When let result = serializer.serializeResponse(nil, response, nil, nil) // Then XCTAssertTrue(result.isFailure, "result is failure should be true") XCTAssertNil(result.value, "result value should be nil") XCTAssertNotNil(result.error, "result error should not be nil") if let error = result.error as? CLDNError { XCTAssertTrue(error.isInputDataNil) } else { XCTFail("error should not be nil") } } func testThatDataResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given let serializer = CLDNDataRequest.dataResponseSerializer() let response = httpURLResponse(forStatusCode: 204) // When let result = serializer.serializeResponse(nil, response, nil, nil) // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) if let data = result.value { XCTAssertEqual(data.count, 0) } } // MARK: Tests - String Response Serializer func testThatStringResponseSerializerFailsWhenDataIsNil() { // Given let serializer = CLDNDataRequest.stringResponseSerializer() // When let result = serializer.serializeResponse(nil, nil, nil, nil) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError { XCTAssertTrue(error.isInputDataNil) } else { XCTFail("error should not be nil") } } func testThatStringResponseSerializerSucceedsWhenDataIsEmpty() { // Given let serializer = CLDNDataRequest.stringResponseSerializer() // When let result = serializer.serializeResponse(nil, nil, Data(), nil) // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) } func testThatStringResponseSerializerSucceedsWithUTF8DataAndNoProvidedEncoding() { let serializer = CLDNDataRequest.stringResponseSerializer() let data = "data".data(using: .utf8)! // When let result = serializer.serializeResponse(nil, nil, data, nil) // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) } func testThatStringResponseSerializerSucceedsWithUTF8DataAndUTF8ProvidedEncoding() { let serializer = CLDNDataRequest.stringResponseSerializer(encoding: .utf8) let data = "data".data(using: .utf8)! // When let result = serializer.serializeResponse(nil, nil, data, nil) // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) } func testThatStringResponseSerializerSucceedsWithUTF8DataUsingResponseTextEncodingName() { let serializer = CLDNDataRequest.stringResponseSerializer() let data = "data".data(using: .utf8)! let response = httpURLResponse(forStatusCode: 200, headers: ["Content-Type": "image/jpeg; charset=utf-8"]) // When let result = serializer.serializeResponse(nil, response, data, nil) // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) } func testThatStringResponseSerializerFailsWithUTF32DataAndUTF8ProvidedEncoding() { // Given let serializer = CLDNDataRequest.stringResponseSerializer(encoding: .utf8) let data = "random data".data(using: .utf32)! // When let result = serializer.serializeResponse(nil, nil, data, nil) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError, let failedEncoding = error.failedStringEncoding { XCTAssertTrue(error.isStringSerializationFailed) XCTAssertEqual(failedEncoding, .utf8) } else { XCTFail("error should not be nil") } } func testThatStringResponseSerializerFailsWithUTF32DataAndUTF8ResponseEncoding() { // Given let serializer = CLDNDataRequest.stringResponseSerializer() let data = "random data".data(using: .utf32)! let response = httpURLResponse(forStatusCode: 200, headers: ["Content-Type": "image/jpeg; charset=utf-8"]) // When let result = serializer.serializeResponse(nil, response, data, nil) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError, let failedEncoding = error.failedStringEncoding { XCTAssertTrue(error.isStringSerializationFailed) XCTAssertEqual(failedEncoding, .utf8) } else { XCTFail("error should not be nil") } } func testThatStringResponseSerializerFailsWhenErrorIsNotNil() { // Given let serializer = CLDNDataRequest.stringResponseSerializer() // When let result = serializer.serializeResponse(nil, nil, nil, error) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError { XCTAssertTrue(error.isInputDataNil) } else { XCTFail("error should not be nil") } } func testThatStringResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { // Given let serializer = CLDNDataRequest.stringResponseSerializer() let response = httpURLResponse(forStatusCode: 200) // When let result = serializer.serializeResponse(nil, response, nil, nil) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError { XCTAssertTrue(error.isInputDataNil) } else { XCTFail("error should not be nil") } } func testThatStringResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given let serializer = CLDNDataRequest.stringResponseSerializer() let response = httpURLResponse(forStatusCode: 205) // When let result = serializer.serializeResponse(nil, response, nil, nil) // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) if let string = result.value { XCTAssertEqual(string, "") } } // MARK: Tests - JSON Response Serializer func testThatJSONResponseSerializerFailsWhenDataIsNil() { // Given let serializer = CLDNDataRequest.jsonResponseSerializer() // When let result = serializer.serializeResponse(nil, nil, nil, nil) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError { XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } } func testThatJSONResponseSerializerFailsWhenDataIsEmpty() { // Given let serializer = CLDNDataRequest.jsonResponseSerializer() // When let result = serializer.serializeResponse(nil, nil, Data(), nil) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError { XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } } func testThatJSONResponseSerializerSucceedsWhenDataIsValidJSON() { // Given let serializer = CLDNDataRequest.jsonResponseSerializer() let data = "{\"json\": true}".data(using: .utf8)! // When let result = serializer.serializeResponse(nil, nil, data, nil) // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) } func testThatJSONResponseSerializerFailsWhenDataIsInvalidJSON() { // Given let serializer = CLDNDataRequest.jsonResponseSerializer() let data = "definitely not valid json".data(using: .utf8)! // When let result = serializer.serializeResponse(nil, nil, data, nil) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError, let underlyingError = error.underlyingError as? CocoaError { XCTAssertTrue(error.isJSONSerializationFailed) XCTAssertEqual(underlyingError.errorCode, 3840) } else { XCTFail("error should not be nil") } } func testThatJSONResponseSerializerFailsWhenErrorIsNotNil() { // Given let serializer = CLDNDataRequest.jsonResponseSerializer() // When let result = serializer.serializeResponse(nil, nil, nil, error) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError { XCTAssertTrue(error.isInputDataNil) } else { XCTFail("error should not be nil") } } func testThatJSONResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { // Given let serializer = CLDNDataRequest.jsonResponseSerializer() let response = httpURLResponse(forStatusCode: 200) // When let result = serializer.serializeResponse(nil, response, nil, nil) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError { XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } } func testThatJSONResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given let serializer = CLDNDataRequest.jsonResponseSerializer() let response = httpURLResponse(forStatusCode: 204) // When let result = serializer.serializeResponse(nil, response, nil, nil) // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) if let json = result.value as? NSNull { XCTAssertEqual(json, NSNull()) } } } // MARK: - class DownloadResponseSerializationTestCase: BaseTestCase { // MARK: Properties private let error = CLDNError.responseSerializationFailed(reason: .inputFileNil) private var jsonEmptyDataFileURL: URL { return url(forResource: "empty_data", withExtension: "json") } private var jsonValidDataFileURL: URL { return url(forResource: "valid_data", withExtension: "json") } private var jsonInvalidDataFileURL: URL { return url(forResource: "invalid_data", withExtension: "json") } private var plistEmptyDataFileURL: URL { return url(forResource: "empty", withExtension: "data") } private var plistValidDataFileURL: URL { return url(forResource: "valid", withExtension: "data") } private var plistInvalidDataFileURL: URL { return url(forResource: "invalid", withExtension: "data") } private var stringEmptyDataFileURL: URL { return url(forResource: "empty_string", withExtension: "txt") } private var stringUTF8DataFileURL: URL { return url(forResource: "utf8_string", withExtension: "txt") } private var stringUTF32DataFileURL: URL { return url(forResource: "utf32_string", withExtension: "txt") } private var invalidFileURL: URL { return URL(fileURLWithPath: "/this/file/does/not/exist.txt") } // MARK: Tests - Data Response Serializer func testThatDataResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given let serializer = CLDNDataRequest.dataResponseSerializer() let response = httpURLResponse(forStatusCode: 205) // When let result = serializer.serializeResponse(nil, response, nil, nil) // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) if let data = result.value { XCTAssertEqual(data.count, 0) } } // MARK: Tests - String Response Serializer func testThatStringResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { // Given let serializer = CLDNDataRequest.stringResponseSerializer() let response = httpURLResponse(forStatusCode: 200) // When let result = serializer.serializeResponse(nil, response, nil, nil) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError { XCTAssertTrue(error.isInputDataNil) } else { XCTFail("error should not be nil") } } func testThatStringResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given let serializer = CLDNDataRequest.stringResponseSerializer() let response = httpURLResponse(forStatusCode: 204) // When let result = serializer.serializeResponse(nil, response, nil, nil) // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) if let string = result.value { XCTAssertEqual(string, "") } } // MARK: Tests - JSON Response Serializer func testThatJSONResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { // Given let serializer = CLDNDataRequest.jsonResponseSerializer() let response = httpURLResponse(forStatusCode: 200) // When let result = serializer.serializeResponse(nil, response, nil, nil) // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) if let error = result.error as? CLDNError { XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } } func testThatJSONResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given let serializer = CLDNDataRequest.jsonResponseSerializer() let response = httpURLResponse(forStatusCode: 205) // When let result = serializer.serializeResponse(nil, response, nil, nil) // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) if let json = result.value as? NSNull { XCTAssertEqual(json, NSNull()) } } } ================================================ FILE: Example/Tests/BaseNetwork/Features/URLProtocolTests.swift ================================================ // // URLProtocolTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class ProxyURLProtocol: URLProtocol { // MARK: Properties struct PropertyKeys { static let handledByForwarderURLProtocol = "HandledByProxyURLProtocol" } lazy var session: URLSession = { let configuration: URLSessionConfiguration = { let configuration = URLSessionConfiguration.ephemeral configuration.httpAdditionalHeaders = CLDNSessionManager.defaultHTTPHeaders return configuration }() let session = Foundation.URLSession(configuration: configuration, delegate: self, delegateQueue: nil) return session }() var activeTask: URLSessionTask? // MARK: Class Request Methods override class func canInit(with request: URLRequest) -> Bool { if URLProtocol.property(forKey: PropertyKeys.handledByForwarderURLProtocol, in: request) != nil { return false } return true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { if let headers = request.allHTTPHeaderFields { do { return try CLDNURLEncoding.default.CLDN_Encode(request, with: headers) } catch { return request } } return request } override class func requestIsCacheEquivalent(_ a: URLRequest, to b: URLRequest) -> Bool { return false } // MARK: Loading Methods override func startLoading() { // rdar://26849668 - URLProtocol had some API's that didnt make the value type conversion let urlRequest = (request.urlRequest! as NSURLRequest).mutableCopy() as! NSMutableURLRequest URLProtocol.setProperty(true, forKey: PropertyKeys.handledByForwarderURLProtocol, in: urlRequest) activeTask = session.dataTask(with: urlRequest as URLRequest) activeTask?.resume() } override func stopLoading() { activeTask?.cancel() } } // MARK: - extension ProxyURLProtocol: URLSessionDataDelegate { // MARK: NSURLSessionDelegate func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { client?.urlProtocol(self, didLoad: data) } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let response = task.response { client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) } client?.urlProtocolDidFinishLoading(self) } } // MARK: - class URLProtocolTestCase: BaseTestCase { var manager: CLDNSessionManager! // MARK: Setup and Teardown override func setUp() { super.setUp() manager = { let configuration: URLSessionConfiguration = { let configuration = URLSessionConfiguration.default configuration.protocolClasses = [ProxyURLProtocol.self] configuration.httpAdditionalHeaders = ["Session-Configuration-Header": "foo"] return configuration }() return CLDNSessionManager(configuration: configuration) }() } // MARK: Tests func skipped_testThatURLProtocolReceivesRequestHeadersAndSessionConfigurationHeaders() { // Given let urlString = "https://httpbin.org/headers" let url = URL(string: urlString)! var urlRequest = URLRequest(url: url) urlRequest.httpMethod = CLDNHTTPMethod.get.rawValue urlRequest.setValue("foobar", forHTTPHeaderField: "request-header") let expectation = self.expectation(description: "GET request should succeed") var response: CLDNDefaultDataResponse? // When manager.request(urlRequest) .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) if let headers = response?.response?.allHeaderFields as? [String: String] { XCTAssertEqual(headers["request-header"], "foobar") // Configuration headers are only passed in on iOS 9.0+ if #available(iOS 9.0, *) { XCTAssertEqual(headers["session-configuration-header"], "foo") } else { XCTAssertNil(headers["session-configuration-header"]) } } else { XCTFail("headers should not be nil") } } } ================================================ FILE: Example/Tests/BaseNetwork/Features/ValidationTests.swift ================================================ // // ValidationTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class StatusCodeValidationTestCase: BaseTestCase { func testThatValidationForRequestWithAcceptableStatusCodeResponseSucceeds() { // Given let urlString = "https://httpbin.org/status/200" let expectation1 = self.expectation(description: "request should return 200 status code") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate(statusCode: 200..<300) .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(requestError) } func testThatValidationForRequestWithUnacceptableStatusCodeResponseFails() { // Given let urlString = "https://httpbin.org/status/404" let expectation1 = self.expectation(description: "request should return 404 status code") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate(statusCode: [200]) .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(requestError) for error in [requestError] { if let error = error as? CLDNError, let statusCode = error.responseCode { XCTAssertTrue(error.isUnacceptableStatusCode) XCTAssertEqual(statusCode, 404) } else { XCTFail("Error should not be nil, should be an CLDNError, and should have an associated statusCode.") } } } func testThatValidationForRequestWithNoAcceptableStatusCodesFails() { // Given let urlString = "https://httpbin.org/status/201" let expectation1 = self.expectation(description: "request should return 201 status code") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate(statusCode: []) .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(requestError) for error in [requestError] { if let error = error as? CLDNError, let statusCode = error.responseCode { XCTAssertTrue(error.isUnacceptableStatusCode) XCTAssertEqual(statusCode, 201) } else { XCTFail("Error should not be nil, should be an CLDNError, and should have an associated statusCode.") } } } } // MARK: - class ContentTypeValidationTestCase: BaseTestCase { func testThatValidationForRequestWithAcceptableContentTypeResponseSucceeds() { // Given let urlString = "https://httpbin.org/ip" let expectation1 = self.expectation(description: "request should succeed and return ip") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate(contentType: ["application/json"]) .validate(contentType: ["application/json; charset=utf-8"]) .validate(contentType: ["application/json; q=0.8; charset=utf-8"]) .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(requestError) } func testThatValidationForRequestWithAcceptableWildcardContentTypeResponseSucceeds() { // Given let urlString = "https://httpbin.org/ip" let expectation1 = self.expectation(description: "request should succeed and return ip") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate(contentType: ["*/*"]) .validate(contentType: ["application/*"]) .validate(contentType: ["*/json"]) .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(requestError) } func testThatValidationForRequestWithUnacceptableContentTypeResponseFails() { // Given let urlString = "https://httpbin.org/" let expectation1 = self.expectation(description: "request should succeed and return html") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate(contentType: ["application/octet-stream"]) .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(requestError) for error in [requestError] { if let error = error as? CLDNError { XCTAssertTrue(error.isUnacceptableContentType) XCTAssertEqual(error.responseContentType, "text/html") XCTAssertEqual(error.acceptableContentTypes?.first, "application/octet-stream") } else { XCTFail("error should not be nil") } } } func testThatValidationForRequestWithNoAcceptableContentTypeResponseFails() { // Given let urlString = "https://httpbin.org/" let expectation1 = self.expectation(description: "request should succeed and return html") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate(contentType: []) .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(requestError) for error in [requestError] { if let error = error as? CLDNError { XCTAssertTrue(error.isUnacceptableContentType) XCTAssertEqual(error.responseContentType, "text/html") XCTAssertTrue(error.acceptableContentTypes?.isEmpty ?? false) } else { XCTFail("error should not be nil") } } } func skipped_testThatValidationForRequestWithNoAcceptableContentTypeResponseSucceedsWhenNoDataIsReturned() { // Given let urlString = "https://mockbin.org/" let expectation1 = self.expectation(description: "request should succeed and return no data") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate(contentType: []) .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(requestError) } func testThatValidationForRequestWithAcceptableWildcardContentTypeResponseSucceedsWhenResponseIsNil() { // Given class MockManager: CLDNSessionManager { override func request(_ urlRequest: CLDNURLRequestConvertible) -> CLDNDataRequest { do { let originalRequest = try urlRequest.CLDN_AsURLRequest() let originalTask = CLDNDataRequest.Requestable(urlRequest: originalRequest) let task = try originalTask.CLDN_Task(session: session, adapter: adapter, queue: queue) let request = MockDataRequest(session: session, requestTask: .data(originalTask, task)) delegate[task] = request if startRequestsImmediately { request.resume() } return request } catch { let request = CLDNDataRequest(session: session, requestTask: .data(nil, nil), error: error) if startRequestsImmediately { request.resume() } return request } } } class MockDataRequest: CLDNDataRequest { override var response: HTTPURLResponse? { return MockHTTPURLResponse( url: request!.url!, statusCode: 204, httpVersion: "HTTP/1.1", headerFields: nil ) } } class MockHTTPURLResponse: HTTPURLResponse { override var mimeType: String? { return nil } } let manager: CLDNSessionManager = { let configuration: URLSessionConfiguration = { let configuration = URLSessionConfiguration.ephemeral configuration.httpAdditionalHeaders = CLDNSessionManager.defaultHTTPHeaders return configuration }() return MockManager(configuration: configuration) }() let urlString = "https://httpbin.org/delete" let expectation1 = self.expectation(description: "request should be stubbed and return 204 status code") var requestResponse: CLDNDefaultDataResponse? // When manager.request(urlString, method: .delete) .validate(contentType: ["*/*"]) .response { resp in requestResponse = resp expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(requestResponse?.response) XCTAssertNotNil(requestResponse?.data) XCTAssertNil(requestResponse?.error) XCTAssertEqual(requestResponse?.response?.statusCode, 204) XCTAssertNil(requestResponse?.response?.mimeType) } } // MARK: - class MultipleValidationTestCase: BaseTestCase { func testThatValidationForRequestWithAcceptableStatusCodeAndContentTypeResponseSucceeds() { // Given let urlString = "https://httpbin.org/ip" let expectation1 = self.expectation(description: "request should succeed and return ip") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate(statusCode: 200..<300) .validate(contentType: ["application/json"]) .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(requestError) } func testThatValidationForRequestWithUnacceptableStatusCodeAndContentTypeResponseFailsWithStatusCodeError() { // Given let urlString = "https://httpbin.org/" let expectation1 = self.expectation(description: "request should succeed and return status code 200") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate(statusCode: 400..<600) .validate(contentType: ["application/octet-stream"]) .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(requestError) for error in [requestError] { if let error = error as? CLDNError { XCTAssertTrue(error.isUnacceptableStatusCode) XCTAssertEqual(error.responseCode, 200) } else { XCTFail("error should not be nil") } } } func testThatValidationForRequestWithUnacceptableStatusCodeAndContentTypeResponseFailsWithContentTypeError() { // Given let urlString = "https://httpbin.org/" let expectation1 = self.expectation(description: "request should succeed and return html") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate(contentType: ["application/octet-stream"]) .validate(statusCode: 400..<600) .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(requestError) for error in [requestError] { if let error = error as? CLDNError { XCTAssertTrue(error.isUnacceptableContentType) XCTAssertEqual(error.responseContentType, "text/html") XCTAssertEqual(error.acceptableContentTypes?.first, "application/octet-stream") } else { XCTFail("error should not be nil") } } } } // MARK: - class AutomaticValidationTestCase: BaseTestCase { func testThatValidationForRequestWithAcceptableStatusCodeAndContentTypeResponseSucceeds() { // Given let url = URL(string: "https://httpbin.org/ip")! var urlRequest = URLRequest(url: url) urlRequest.setValue("application/json", forHTTPHeaderField: "Accept") let expectation1 = self.expectation(description: "request should succeed and return ip") var requestError: Error? // When CLDNSessionManager.default.request(urlRequest).validate().response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(requestError) } func testThatValidationForRequestWithUnacceptableStatusCodeResponseFails() { // Given let urlString = "https://httpbin.org/status/404" let expectation1 = self.expectation(description: "request should return 404 status code") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate() .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(requestError) for error in [requestError] { if let error = error as? CLDNError, let statusCode = error.responseCode { XCTAssertTrue(error.isUnacceptableStatusCode) XCTAssertEqual(statusCode, 404) } else { XCTFail("error should not be nil") } } } func testThatValidationForRequestWithAcceptableWildcardContentTypeResponseSucceeds() { // Given let url = URL(string: "https://httpbin.org/ip")! var urlRequest = URLRequest(url: url) urlRequest.setValue("application/*", forHTTPHeaderField: "Accept") let expectation1 = self.expectation(description: "request should succeed and return ip") var requestError: Error? // When CLDNSessionManager.default.request(urlRequest).validate().response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(requestError) } func testThatValidationForRequestWithAcceptableComplexContentTypeResponseSucceeds() { // Given let url = URL(string: "https://httpbin.org/")! var urlRequest = URLRequest(url: url) let headerValue = "text/xml, application/xml, application/xhtml+xml, text/html;q=0.9, text/plain;q=0.8,*/*;q=0.5" urlRequest.setValue(headerValue, forHTTPHeaderField: "Accept") let expectation1 = self.expectation(description: "request should succeed and return xml") var requestError: Error? // When CLDNSessionManager.default.request(urlRequest).validate().response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(requestError) } func testThatValidationForRequestWithUnacceptableContentTypeResponseFails() { // Given let url = URL(string: "https://httpbin.org/")! var urlRequest = URLRequest(url: url) urlRequest.setValue("application/json", forHTTPHeaderField: "Accept") let expectation1 = self.expectation(description: "request should succeed and return html") var requestError: Error? // When CLDNSessionManager.default.request(urlRequest).validate().response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(requestError) for error in [requestError] { if let error = error as? CLDNError { XCTAssertTrue(error.isUnacceptableContentType) XCTAssertEqual(error.responseContentType, "text/html") XCTAssertEqual(error.acceptableContentTypes?.first, "application/json") } else { XCTFail("error should not be nil") } } } } // MARK: - private enum ValidationError: Error { case missingData, missingFile, fileReadFailed } extension CLDNDataRequest { func validateDataExists() -> Self { return validate { request, response, data in guard data != nil else { return .failure(ValidationError.missingData) } return .success } } func validate(with error: Error) -> Self { return validate { _, _, _ in .failure(error) } } } // MARK: - class CustomValidationTestCase: BaseTestCase { func testThatCustomValidationClosureHasAccessToServerResponseData() { // Given let urlString = "https://httpbin.org/get" let expectation1 = self.expectation(description: "request should return 200 status code") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate { request, response, data in guard data != nil else { return .failure(ValidationError.missingData) } return .success } .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(requestError) } func testThatCustomValidationCanThrowCustomError() { // Given let urlString = "https://httpbin.org/get" let expectation1 = self.expectation(description: "request should return 200 status code") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate { _, _, _ in .failure(ValidationError.missingData) } .validate { _, _, _ in .failure(ValidationError.missingFile) } // should be ignored .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertEqual(requestError as? ValidationError, ValidationError.missingData) } func testThatValidationExtensionHasAccessToServerResponseData() { // Given let urlString = "https://httpbin.org/get" let expectation1 = self.expectation(description: "request should return 200 status code") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validateDataExists() .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(requestError) } func testThatValidationExtensionCanThrowCustomError() { // Given let urlString = "https://httpbin.org/get" let expectation1 = self.expectation(description: "request should return 200 status code") var requestError: Error? // When CLDNSessionManager.default.request(urlString) .validate(with: ValidationError.missingData) .validate(with: ValidationError.missingFile) // should be ignored .response { resp in requestError = resp.error expectation1.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertEqual(requestError as? ValidationError, ValidationError.missingData) } } ================================================ FILE: Example/Tests/BaseNetwork/ObjcBaseTestCase.h ================================================ // // ObjcBaseTestCase.h // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "Cloudinary_Tests-Swift.h" @interface ObjcBaseTestCase: XCTestCase @property (nonatomic, assign) NSTimeInterval timeout; - (BOOL)shouldSkipTest; @end ================================================ FILE: Example/Tests/BaseNetwork/ObjcBaseTestCase.m ================================================ // // ObjcBaseTestCase.m // Cloudinary_Tests // // Created by Oz Deutsch on 24/09/2020. // Copyright © 2020 CocoaPods. All rights reserved. // #import "ObjcBaseTestCase.h" @implementation ObjcBaseTestCase - (BOOL)setUpWithError:(NSError *__autoreleasing _Nullable *)error { XCTSkipIf([self shouldSkipTest], "test skipped"); return [super setUpWithError:error]; } /** override this method to skip tests when needed */ - (BOOL)shouldSkipTest { return false; } @end ================================================ FILE: Example/Tests/BaseNetwork/Resources/Responses/JSON/empty_data.json ================================================ ================================================ FILE: Example/Tests/BaseNetwork/Resources/Responses/JSON/invalid_data.json ================================================ this is not going "to" be happy json data at all { "10": whoops } ================================================ FILE: Example/Tests/BaseNetwork/Resources/Responses/JSON/valid_data.json ================================================ { "team": "royals", "score": 23 } ================================================ FILE: Example/Tests/BaseNetwork/Resources/Responses/Property List/empty.data ================================================ ================================================ FILE: Example/Tests/BaseNetwork/Resources/Responses/Property List/invalid.data ================================================ this is not going "to" be happy json data at all { "10": whoops } ================================================ FILE: Example/Tests/BaseNetwork/Resources/Responses/Property List/valid.data ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString 4.0.0-beta.2 CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: Example/Tests/BaseNetwork/Resources/Responses/String/empty_string.txt ================================================ ================================================ FILE: Example/Tests/BaseNetwork/Resources/Responses/String/utf8_string.txt ================================================ random data ================================================ FILE: Example/Tests/ConfigurationTests/CLDAnalyticsTests.swift ================================================ // // CLDAnalyticsTests.swift // Cloudinary_Tests // // Created by Adi Mizrahi on 26/07/2022. // Copyright © 2022 CocoaPods. All rights reserved. // import Foundation import Cloudinary import XCTest class CLDAnalyticsTests: BaseTestCase { func test_analyicsString() { var analyticsString = CLDAnalytics().generateAnalyticsSignature(sdkVersion: "1.24.0",techVersion: "12.0", osType: "B", osVersion: "12.0") XCTAssertEqual(analyticsString, "DAEAlhAMBMA0") analyticsString = CLDAnalytics().generateAnalyticsSignature(sdkVersion: "1.24.0-beta.6",techVersion: "12.0", osType: "B", osVersion: "12.0") XCTAssertEqual(analyticsString, "DAEAlhAMBMA0") analyticsString = CLDAnalytics().generateAnalyticsSignature(sdkVersion: "1.24.0",techVersion: "16.3", osType: "B", osVersion: "16.3") XCTAssertEqual(analyticsString, "DAEAlhE8BQD0") analyticsString = CLDAnalytics().generateAnalyticsSignature(sdkVersion: "1.24.0",techVersion: "17.1", osType: "B", osVersion: "17.1") XCTAssertEqual(analyticsString, "DAEAlhB1BRB0") } func test_errorAnalytics() { var analyticsString = CLDAnalytics().generateAnalyticsSignature(sdkVersion: "1.24.0",techVersion: "0") XCTAssertEqual(analyticsString, "E") analyticsString = CLDAnalytics().generateAnalyticsSignature(sdkVersion: "0",techVersion: "12.0") XCTAssertEqual(analyticsString, "E") analyticsString = CLDAnalytics().generateAnalyticsSignature(sdkVersion: "",techVersion: "12.0") XCTAssertEqual(analyticsString, "E") analyticsString = CLDAnalytics().generateAnalyticsSignature(sdkVersion: "1.24.0",techVersion: "") XCTAssertEqual(analyticsString, "E") analyticsString = CLDAnalytics().generateAnalyticsSignature(sdkVersion: "43.21.26",techVersion: "5.0", osVersion: "17.1") XCTAssertEqual(analyticsString, "DAE;;;AFBRB0") } func test_analyticsInitialized() { let analytics = CLDAnalytics(sdkVersion: "1.24.0",techVersion: "12.0", osType: "B", osVersion: "12.0") XCTAssertEqual(analytics.generateAnalyticsSignature(), "DAEAlhAMBMA0") analytics.setSDKVersion(version: "1.25.0") XCTAssertEqual(analytics.generateAnalyticsSignature(), "DAEAnFAMBMA0") analytics.setTechVersion(version: "13.0") XCTAssertEqual(analytics.generateAnalyticsSignature(), "DAEAnFANBMA0") analytics.setOsVersion(version: "17.1") XCTAssertEqual(analytics.generateAnalyticsSignature(), "DAEAnFANBRB0") } func test_analyticsFeatureFlag() { let analytics = CLDAnalytics(sdkVersion: "1.24.0",techVersion: "12.0", osType: "B", osVersion: "12.0", featureFlag: "E") XCTAssertEqual(analytics.generateAnalyticsSignature(), "DAEAlhAMBMAE") } } ================================================ FILE: Example/Tests/ConfigurationTests/CLDConfigurationTests.m ================================================ // // ObjcCLDConfigurationTests.m // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "ObjcBaseTestCase.h" @interface ObjcCLDConfigurationTests : ObjcBaseTestCase @property (nonatomic, strong, nullable) CLDConfiguration* sut; @end @implementation ObjcCLDConfigurationTests // MARK: - setup and teardown - (void)setUp { [super setUp]; } - (void)tearDown { [super tearDown]; self.sut = nil; } // MARK: - timeout - (void)test_initTimeout_setNSNumber_shouldStoreValue { // Given NSNumber* input = [[NSNumber alloc] initWithInt:10000]; NSNumber* expectedResult = [[NSNumber alloc] initWithInt:10000]; // When self.sut = [[CLDConfiguration alloc] initWithCloudName:@"" apiKey:nil apiSecret:nil privateCdn:NO secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:NO signatureAlgorithm:SignatureAlgorithmSha1 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:input analytics:NO]; // Then XCTAssertEqualObjects(self.sut.timeout, expectedResult, "Init with timeout = number, should be stored in property"); } - (void)test_initTimeout_nil_shouldStoreFalseValue { // When self.sut = [[CLDConfiguration alloc] initWithCloudName:@"" apiKey:nil apiSecret:nil privateCdn:NO secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:NO signatureAlgorithm:SignatureAlgorithmSha1 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; // Then XCTAssertNil(self.sut.timeout, "Init with timeout = nil, should not be stored in property"); } - (void)test_initTimeout_optionsString_shouldStoreValue { // Given NSString* keyCloudName = @"cloud_name"; NSString* inputCloudName = @"foo"; NSString* keyTimeout = @"timeout"; NSString* inputTimeout = @"10000"; NSNumber* expectedResult = [[NSNumber alloc] initWithInt:10000]; // When self.sut = [[CLDConfiguration alloc] initWithOptions:@{keyCloudName: inputCloudName, keyTimeout: inputTimeout}]; // Then XCTAssertEqualObjects(self.sut.timeout, expectedResult, "Init with timeout = number, should be stored in property"); } - (void)test_initTimeout_optionsNSNumber_shouldStoreValue { // Given NSString* keyCloudName = @"cloud_name"; NSString* inputCloudName = @"foo"; NSString* keyTimeout = @"timeout"; NSNumber* inputTimeout = [[NSNumber alloc] initWithInt:10000]; NSNumber* expectedResult = [[NSNumber alloc] initWithInt:10000]; // When self.sut = [[CLDConfiguration alloc] initWithOptions:@{keyCloudName: inputCloudName, keyTimeout: inputTimeout}]; // Then XCTAssertEqualObjects(self.sut.timeout, expectedResult, "Init with timeout = number, should be stored in property"); } - (void)test_initTimeout_cloudinaryUrl_shouldStoreValue { // Given NSString* longUrlSignatureQuery = @"?timeout=10000"; NSString* testedUrl = @"cloudinary://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test"; NSString* fullUrl = [NSString stringWithFormat:@"%@%@",testedUrl,longUrlSignatureQuery]; NSNumber* expectedResult = [[NSNumber alloc] initWithInt:10000]; // When self.sut = [[CLDConfiguration alloc] initWithCloudinaryUrl:fullUrl]; // Then XCTAssertEqualObjects(self.sut.timeout, expectedResult, "Init with cloudinaryUrl with valid timeout, should be stored in property"); } // MARK: - long url signature - (void)test_initLongUrlSignature_true_shouldStoreValue { // Given BOOL input = YES; // When self.sut = [[CLDConfiguration alloc] initWithCloudName:@"" apiKey:nil apiSecret:nil privateCdn:NO secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:input signatureAlgorithm:SignatureAlgorithmSha1 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; // Then XCTAssertTrue(self.sut.longUrlSignature, "Init with longUrlSignature = true, should be stored in property"); } - (void)test_initLongUrlSignature_default_shouldStoreFalseValue { // When self.sut = [[CLDConfiguration alloc] initWithCloudName:@"" apiKey:nil apiSecret:nil privateCdn:NO secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:NO signatureAlgorithm:SignatureAlgorithmSha1 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; // Then XCTAssertFalse(self.sut.longUrlSignature, "Init without longUrlSignature should store the default false value"); } - (void)test_initLongUrlSignature_optionsString_shouldStoreValue { // Given NSString* keyCloudName = @"cloud_name"; NSString* inputCloudName = @"foo"; NSString* keyLongUrlSignature = @"long_url_signature"; NSString* inputLongUrlSignature = @"true"; // When self.sut = [[CLDConfiguration alloc] initWithOptions:@{keyCloudName: inputCloudName, keyLongUrlSignature: inputLongUrlSignature}]; // Then XCTAssertTrue(self.sut.longUrlSignature, "Init with options with longUrlSignature = true, should be stored in property"); } - (void)test_initLongUrlSignature_optionsBool_shouldStoreValue { // Given NSString* keyCloudName = @"cloud_name"; NSString* inputCloudName = @"foo"; NSString* keyLongUrlSignature = @"long_url_signature"; NSNumber* inputLongUrlSignature = @YES; // When self.sut = [[CLDConfiguration alloc] initWithOptions:@{keyCloudName: inputCloudName, keyLongUrlSignature: inputLongUrlSignature}]; // Then XCTAssertTrue(self.sut.longUrlSignature, "Init with options with longUrlSignature = true, should be stored in property"); } - (void)test_initLongUrlSignature_cloudinaryUrl_shouldStoreValue { // Given NSString* longUrlSignatureQuery = @"?long_url_signature=true"; NSString* testedUrl = @"cloudinary://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test"; NSString* fullUrl = [NSString stringWithFormat:@"%@%@",testedUrl,longUrlSignatureQuery]; // When self.sut = [[CLDConfiguration alloc] initWithCloudinaryUrl:fullUrl]; // Then XCTAssertTrue(self.sut.longUrlSignature, "Init with cloudinaryUrl with valid longUrlSignature = true, should be stored in property"); } // MARK: - signature algorithm - (void)test_initSignatureAlgorithm_setSha256_shouldStoreValue { // Given BOOL input = SignatureAlgorithmSha256; // When self.sut = [[CLDConfiguration alloc] initWithCloudName:@"" apiKey:nil apiSecret:nil privateCdn:NO secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:NO signatureAlgorithm:input signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; // Then XCTAssertEqual(self.sut.signatureAlgorithm, SignatureAlgorithmSha256, "Init with signatureAlgorithm should store that value in property"); } - (void)test_initSignatureAlgorithm_default_shouldStoreDefaultValue { // When self.sut = [[CLDConfiguration alloc] initWithCloudName:@"" apiKey:nil apiSecret:nil privateCdn:NO secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:NO signatureAlgorithm:0 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; // Then XCTAssertEqual(self.sut.signatureAlgorithm, SignatureAlgorithmSha1, "Init without signatureAlgorithm should store the default .sha1 value"); } - (void)test_initSignatureAlgorithm_optionsSha256_shouldStoreValue { // Given NSString* keyCloudName = @"cloud_name"; NSString* inputCloudName = @"foo"; NSString* keySignatureAlgorithm = @"signature_algorithm"; NSString* inputSignatureAlgorithm = @"sha256"; // When self.sut = [[CLDConfiguration alloc] initWithOptions:@{keyCloudName: inputCloudName, keySignatureAlgorithm: inputSignatureAlgorithm}]; // Then XCTAssertEqual(self.sut.signatureAlgorithm, SignatureAlgorithmSha256, "Init with options with signatureAlgorithm should store that value"); } - (void)test_initSignatureAlgorithm_optionsSha1_shouldStoreValue { // Given NSString* keyCloudName = @"cloud_name"; NSString* inputCloudName = @"foo"; NSString* keySignatureAlgorithm = @"signature_algorithm"; NSString* inputSignatureAlgorithm = @"sha1"; // When self.sut = [[CLDConfiguration alloc] initWithOptions:@{keyCloudName: inputCloudName, keySignatureAlgorithm: inputSignatureAlgorithm}]; // Then XCTAssertEqual(self.sut.signatureAlgorithm, SignatureAlgorithmSha1, "Init with options with signatureAlgorithm should store that value"); } - (void)test_initSignatureAlgorithm_optionsInvalidString_shouldStoreValue { // Given NSString* keyCloudName = @"cloud_name"; NSString* inputCloudName = @"foo"; NSString* keySignatureAlgorithm = @"signature_algorithm"; NSString* inputSignatureAlgorithm = @"notSha"; // When self.sut = [[CLDConfiguration alloc] initWithOptions:@{keyCloudName: inputCloudName, keySignatureAlgorithm: inputSignatureAlgorithm}]; // Then XCTAssertEqual(self.sut.signatureAlgorithm, SignatureAlgorithmSha1, "Init with options with invalid signatureAlgorithm should store the defualt .sha1 value"); } - (void)test_initSignatureAlgorithm_cloudinaryUrl_shouldStoreValue { // Given NSString* signatureAlgorithmQuery = @"?signature_algorithm=sha256"; NSString* testedUrl = @"cloudinary://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test"; NSString* fullUrl = [NSString stringWithFormat:@"%@%@",testedUrl,signatureAlgorithmQuery]; // When self.sut = [[CLDConfiguration alloc] initWithCloudinaryUrl:fullUrl]; // Then XCTAssertEqual(self.sut.signatureAlgorithm, SignatureAlgorithmSha256, "Init with cloudinaryUrl with valid signatureAlgorithm should store that value"); } @end ================================================ FILE: Example/Tests/ConfigurationTests/CLDConfigurationTests.swift ================================================ // // CLDConfigurationTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class CLDConfigurationTests: BaseTestCase { var sut : CLDConfiguration! // MARK: - setup and teardown override func setUp() { super.setUp() sut = CLDConfiguration(cloudName: "") } override func tearDown() { sut = nil super.tearDown() } // MARK: - long url signature func test_initLongUrlSignature_true_shouldStoreValue() { // Given let input = true // When sut = CLDConfiguration(cloudName: "", longUrlSignature: input) // Then XCTAssertTrue(sut.longUrlSignature, "Init with longUrlSignature = true, should be stored in property") } func test_initLongUrlSignature_default_shouldStoreFalseValue() { // When sut = CLDConfiguration(cloudName: "") // Then XCTAssertFalse(sut.longUrlSignature, "Init without longUrlSignature should store the default false value") } func test_initLongUrlSignature_optionsString_shouldStoreValue() { // Given let keyCloudName = CLDConfiguration.ConfigParam.CloudName.rawValue let inputCloudName = "foo" as AnyObject let keyLongUrlSignature = CLDConfiguration.ConfigParam.LongUrlSignature.rawValue let inputLongUrlSignature = "true" as AnyObject // When sut = CLDConfiguration(options: [keyCloudName: inputCloudName, keyLongUrlSignature: inputLongUrlSignature]) // Then XCTAssertTrue(sut.longUrlSignature, "Init with options with longUrlSignature = true, should be stored in property") } func test_initLongUrlSignature_optionsBool_shouldStoreValue() { // Given let keyCloudName = CLDConfiguration.ConfigParam.CloudName.rawValue let inputCloudName = "foo" as AnyObject let keyLongUrlSignature = CLDConfiguration.ConfigParam.LongUrlSignature.rawValue let inputLongUrlSignature = true as AnyObject // When sut = CLDConfiguration(options: [keyCloudName: inputCloudName, keyLongUrlSignature: inputLongUrlSignature]) // Then XCTAssertTrue(sut.longUrlSignature, "Init with options with longUrlSignature = true, should be stored in property") } func test_initLongUrlSignature_cloudinaryUrl_shouldStoreValue() { // Given let longUrlSignatureQuery = ("?\(CLDConfiguration.ConfigParam.LongUrlSignature.description)=true") let testedUrl = "cloudinary://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test" let fullUrl = testedUrl + longUrlSignatureQuery // When sut = CLDConfiguration(cloudinaryUrl: fullUrl) // Then XCTAssertTrue(sut.longUrlSignature, "Init with cloudinaryUrl with valid longUrlSignature = true, should be stored in property") } // MARK: - signature algorithm func test_initSignatureAlgorithm_setSha256_shouldStoreValue() { // Given let input = CLDConfiguration.SignatureAlgorithm.sha256 // When sut = CLDConfiguration(cloudName: String(), signatureAlgorithm: input) // Then XCTAssertEqual(sut.signatureAlgorithm, .sha256, "Init with signatureAlgorithm should store that value") } func test_initSignatureAlgorithm_default_shouldStoreDefaultValue() { // When sut = CLDConfiguration(cloudName: String()) // Then XCTAssertEqual(sut.signatureAlgorithm, .sha1, "Init without signatureAlgorithm should store the default .sha1 value") } func test_initSignatureAlgorithm_optionsString_shouldStoreValue() { // Given let keyCloudName = CLDConfiguration.ConfigParam.CloudName.rawValue let inputCloudName = "foo" as AnyObject let keySignatureAlgorithm = CLDConfiguration.ConfigParam.SignatureAlgorithm.rawValue let inputSignatureAlgorithm = "sha256" as AnyObject // When sut = CLDConfiguration(options: [keyCloudName: inputCloudName, keySignatureAlgorithm: inputSignatureAlgorithm]) // Then XCTAssertEqual(sut.signatureAlgorithm, .sha256, "Init with options with signatureAlgorithm should store that value") } func test_initSignatureAlgorithm_optionsEnum_shouldStoreValue() { // Given let keyCloudName = CLDConfiguration.ConfigParam.CloudName.rawValue let inputCloudName = "foo" as AnyObject let keySignatureAlgorithm = CLDConfiguration.ConfigParam.SignatureAlgorithm.rawValue let inputSignatureAlgorithm = CLDConfiguration.SignatureAlgorithm.sha256 as AnyObject // When sut = CLDConfiguration(options: [keyCloudName: inputCloudName, keySignatureAlgorithm: inputSignatureAlgorithm]) // Then XCTAssertEqual(sut.signatureAlgorithm, .sha256, "Init with options with signatureAlgorithm should store that value") } func test_initSignatureAlgorithm_optionsInvalidString_shouldStoreValue() { // Given let keyCloudName = CLDConfiguration.ConfigParam.CloudName.rawValue let inputCloudName = "foo" as AnyObject let keySignatureAlgorithm = CLDConfiguration.ConfigParam.SignatureAlgorithm.rawValue let inputSignatureAlgorithm = "notSha" as AnyObject // When sut = CLDConfiguration(options: [keyCloudName: inputCloudName, keySignatureAlgorithm: inputSignatureAlgorithm]) // Then XCTAssertEqual(sut.signatureAlgorithm, .sha1, "Init with options with invalid signatureAlgorithm should store the default .sha1 value") } func test_initSignatureAlgorithm_cloudinaryUrl_shouldStoreValue() { // Given let signatureAlgorithmQuery = ("?\(CLDConfiguration.ConfigParam.SignatureAlgorithm.description)=sha256") let testedUrl = "cloudinary://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test" let fullUrl = testedUrl + signatureAlgorithmQuery // When sut = CLDConfiguration(cloudinaryUrl: fullUrl) // Then XCTAssertEqual(sut.signatureAlgorithm, .sha256,"Init with cloudinaryUrl with valid signatureAlgorithm should store that value") } // MARK: - timeout func test_initTimeout_setNSNumber_shouldStoreValue() { // Given let input = NSNumber(integerLiteral: 10000) let expectedResult = NSNumber(integerLiteral: 10000) // When sut = CLDConfiguration(cloudName: "", timeout: input) // Then XCTAssertEqual(sut.timeout, expectedResult, "Init with timeout = number, should be stored in property") } func test_initTimeout_nilIsDefault_shouldStoreFalseValue() { // When sut = CLDConfiguration(cloudName: "") // Then XCTAssertFalse(sut.longUrlSignature, "Init without longUrlSignature should store the default false value") } func test_initTimeout_optionsString_shouldStoreValue() { // Given let keyCloudName = CLDConfiguration.ConfigParam.CloudName.rawValue let inputCloudName = "foo" as AnyObject let keyTimeout = CLDConfiguration.ConfigParam.Timeout.rawValue let inputTimeout = "10000" as AnyObject let expectedResult = NSNumber(integerLiteral: 10000) // When sut = CLDConfiguration(options: [keyCloudName: inputCloudName, keyTimeout: inputTimeout]) // Then XCTAssertEqual(sut.timeout, expectedResult, "Init with timeout = number, should be stored in property") } func test_initTimeout_optionsInt_shouldStoreValue() { // Given let keyCloudName = CLDConfiguration.ConfigParam.CloudName.rawValue let inputCloudName = "foo" as AnyObject let keyTimeout = CLDConfiguration.ConfigParam.Timeout.rawValue let inputTimeout = 10000 as AnyObject let expectedResult = NSNumber(integerLiteral: 10000) // When sut = CLDConfiguration(options: [keyCloudName: inputCloudName, keyTimeout: inputTimeout]) // Then XCTAssertEqual(sut.timeout, expectedResult, "Init with timeout = number, should be stored in property") } func test_initTimeout_cloudinaryUrl_shouldStoreValue() { // Given let timeoutQuery = ("?\(CLDConfiguration.ConfigParam.Timeout.description)=10000") let testedUrl = "cloudinary://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test" let fullUrl = testedUrl + timeoutQuery let expectedResult = NSNumber(integerLiteral: 10000) // When sut = CLDConfiguration(cloudinaryUrl: fullUrl) // Then XCTAssertEqual(sut.timeout, expectedResult, "Init with cloudinaryUrl with valid timeout, should be stored in property") } } ================================================ FILE: Example/Tests/CryptoUtilsTests/CryptoUtilsTests.m ================================================ // // CryptoUtilsTests.m // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import #import "ObjcBaseTestCase.h" @interface ObjCCryptoUtilsTests : ObjcBaseTestCase @end @implementation ObjCCryptoUtilsTests - (void)setUp { [super setUp]; } - (void)tearDown { [super tearDown]; } // MARK: - SHA256 - (void)test_SHA256Base64_emptyString_shouldReturnHashedString { // Given NSString* initialString = @""; NSString* expectedResult = @"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"; // When NSString* actualResult = [CryptoObjcHelper sha256_base64WithString:initialString]; // Then XCTAssertNotNil(actualResult, @"Hashed string should not be nil"); XCTAssertEqualObjects(actualResult, expectedResult, "Hashed string should be equal to expected result"); } - (void)test_SHA256Base64_specialString_shouldReturnHashedString { // Given NSString* initialString = @"🧼:|}!@#$%^&*()±§`~+_=-"; NSString* expectedResult = @"jXJPEpRxcbKlIzNzH1RzAsaDeDR87Ir0cENeW8b5t-g"; // When NSString* actualResult = [CryptoObjcHelper sha256_base64WithString:initialString]; // Then XCTAssertNotNil(actualResult, @"Hashed string should not be nil"); XCTAssertEqualObjects(actualResult, expectedResult, "Hashed string should be equal to expected result"); } - (void)test_SHA256Base64_string_shouldReturnHashedString { // Given NSString* initialString = @"cryptoString"; NSString* expectedResult = @"Ncsntkv4ywCQbf4Xz3pTYrxglVm02y4_X9nmCR8uNt0"; // When NSString* actualResult = [CryptoObjcHelper sha256_base64WithString:initialString]; // Then XCTAssertNotNil(actualResult, @"Hashed string should not be nil"); XCTAssertEqualObjects(actualResult, expectedResult, "Hashed string should be equal to expected result"); } @end ================================================ FILE: Example/Tests/CryptoUtilsTests/CryptoUtilsTests.swift ================================================ // // CryptoUtilsTests.swift // CloudinaryTests // // Created by Nitzan Jaitman on 14/11/2017. // Copyright © 2017 Cloudinary. All rights reserved. // import Foundation import XCTest @testable import Cloudinary @objc @objcMembers public class CryptoObjcHelper: NSObject { @objc public class func sha256_base64(string: String) -> String { return string.sha256_base64() } } class CryptoUtilsTests: BaseTestCase { override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } // MARK: - MD5 func testMd5() { let md5 = "sadnkjqndlk3j43qdaoni834j032df8j0a9sdfu03124".cld_md5() XCTAssertEqual("e75465c18b05f1a7e665bbafa9266ef4", md5) } // MARK - SHA1 func testSha1Base8(){ let value = "sadnkjqndlk3j43qdaoni834j032df8j0a9sdfu03124" let secret = "rnkjd123af43214na" let shar1WithSecret = value.sha1_base8(secret) let sha1 = value.sha1_base8(nil) XCTAssertEqual("6aab31b1ad46b21ba8b1a64d451a762395ae222a", shar1WithSecret) XCTAssertEqual("89f30d95fd5e83ce93247677e4a301e279a48ca2", sha1) } func testSha1Base64(){ let value = "sadnkjqndlk3j43qdaoni834j032df8j0a9sdfu03124" let sha1 = value.sha1_base64() XCTAssertEqual("ifMNlf1eg86TJHZ35KMB4nmkjKI", sha1) } // MARK: - SHA256 func test_SHA256Base64_emptyString_shouldReturnHashedString() { // Given let initialString = "" let expectedResult = "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU" // When let actualResult = initialString.sha256_base64() // Then XCTAssertNotNil(actualResult, "Hashed string should not be nil") XCTAssertEqual(actualResult, expectedResult, "Hashed string should be equal to expected result") } func test_SHA256Base64_specialString_shouldReturnHashedString() { // Given let initialString = "🧼:|}!@#$%^&*()±§`~+_=-" let expectedResult = "jXJPEpRxcbKlIzNzH1RzAsaDeDR87Ir0cENeW8b5t-g" // When let actualResult = initialString.sha256_base64() // Then XCTAssertNotNil(actualResult, "Hashed string should not be nil") XCTAssertEqual(actualResult, expectedResult, "Hashed string should be equal to expected result") } func test_SHA256Base64_string_shouldReturnHashedString() { // Given let initialString = "cryptoString" let expectedResult = "Ncsntkv4ywCQbf4Xz3pTYrxglVm02y4_X9nmCR8uNt0" // When let actualResult = initialString.sha256_base64() // Then XCTAssertNotNil(actualResult, "Hashed string should not be nil") XCTAssertEqual(actualResult, expectedResult, "Hashed string should be equal to expected result") } } ================================================ FILE: Example/Tests/FileUtilsTests.swift ================================================ // // FileUtilsTests.swift // CloudinaryTests // // Created by Nitzan Jaitman on 24/09/2017. // Copyright © 2017 Cloudinary. All rights reserved. // import Foundation import XCTest @testable import Cloudinary class FileUtilsTests: BaseTestCase { override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } func testSplitFile() { let url = Bundle(for: FileUtilsTests.self).url(forResource: "borderCollie", withExtension: "jpg")! let (_, files) = CLDFileUtils.splitFile(url: url, chunkSize: 1024 * 20)! let totalSize = CLDFileUtils.getFileSize(url: url) var sum: Int64 = 0 for file in files { XCTAssertTrue(FileManager.default.fileExists(atPath: file.url.path)) sum += Int64(file.length) } XCTAssertEqual(sum, totalSize) } func testRemoveFiles(){ let url = Bundle.init(for: FileUtilsTests.self).url(forResource: "borderCollie", withExtension: "jpg")! let (base, files) = CLDFileUtils.splitFile(url: url, chunkSize: 1024 * 20)! for file in files { XCTAssertTrue(FileManager.default.fileExists(atPath: file.url.path)) } CLDFileUtils.removeFile(file: base!) XCTAssertFalse(FileManager.default.fileExists(atPath: base!.path)) } } ================================================ FILE: Example/Tests/GenerateUrlTests/UrlTests.m ================================================ // // ObjCUrlTests.m // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "ObjcBaseTestCase.h" @interface ObjCUrlTests : ObjcBaseTestCase @property (nonatomic, strong, nullable) CLDCloudinary *sut; @end @implementation ObjCUrlTests NSString* prefix = @"https://res.cloudinary.com/test123"; - (void)setUp { [super setUp]; CLDConfiguration *config = [[CLDConfiguration alloc] initWithCloudinaryUrl:@"cloudinary://a:b@test123?analytics=false"]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; } - (void)tearDown { [super tearDown]; self.sut = nil; } - (void)testCrop { CLDTransformation *trans = [[CLDTransformation alloc]init]; [trans setWidth:@"100"]; [trans setHeight:@"101"]; [trans setCrop:@"crop"]; CLDUrl *url = [self.sut createUrl]; [url setTransformation:trans]; NSString *generatedUrl = [url generate:@"test" signUrl:NO]; XCTAssertEqualObjects(generatedUrl, [prefix stringByAppendingString: @"/image/upload/c_crop,h_101,w_100/test"]); } // MARK: - gravity - (void)test_gravityEnum_shouldReturnExpectedValues { [self testGravityUrl:CLDGravityCenter expectedValue:@"g_center,w_100"]; [self testGravityUrl:CLDGravityAuto expectedValue:@"g_auto,w_100"]; [self testGravityUrl:CLDGravityFace expectedValue:@"g_face,w_100"]; [self testGravityUrl:CLDGravityFaceCenter expectedValue:@"g_face:center,w_100"]; [self testGravityUrl:CLDGravityFaces expectedValue:@"g_faces,w_100"]; [self testGravityUrl:CLDGravityFacesCenter expectedValue:@"g_faces:center,w_100"]; [self testGravityUrl:CLDGravityAdvFace expectedValue:@"g_adv_face,w_100"]; [self testGravityUrl:CLDGravityAdvFaces expectedValue:@"g_adv_faces,w_100"]; [self testGravityUrl:CLDGravityAdvEyes expectedValue:@"g_adv_eyes,w_100"]; [self testGravityUrl:CLDGravityNorth expectedValue:@"g_north,w_100"]; [self testGravityUrl:CLDGravityNorthWest expectedValue:@"g_north_west,w_100"]; [self testGravityUrl:CLDGravityNorthEast expectedValue:@"g_north_east,w_100"]; [self testGravityUrl:CLDGravitySouth expectedValue:@"g_south,w_100"]; [self testGravityUrl:CLDGravitySouthWest expectedValue:@"g_south_west,w_100"]; [self testGravityUrl:CLDGravitySouthEast expectedValue:@"g_south_east,w_100"]; [self testGravityUrl:CLDGravityWest expectedValue:@"g_west,w_100"]; [self testGravityUrl:CLDGravityEast expectedValue:@"g_east,w_100"]; [self testGravityUrl:CLDGravityXyCenter expectedValue:@"g_xy_center,w_100"]; [self testGravityUrl:CLDGravityCustom expectedValue:@"g_custom,w_100"]; [self testGravityUrl:CLDGravityCustomFace expectedValue:@"g_custom:face,w_100"]; [self testGravityUrl:CLDGravityCustomFaces expectedValue:@"g_custom:faces,w_100"]; [self testGravityUrl:CLDGravityCustomAdvFace expectedValue:@"g_custom:adv_face,w_100"]; [self testGravityUrl:CLDGravityCustomAdvFaces expectedValue:@"g_custom:adv_faces,w_100"]; [self testGravityUrl:CLDGravityAutoOcrText expectedValue:@"g_auto:ocr_text,w_100"]; [self testGravityUrl:CLDGravityOcrText expectedValue:@"g_ocr_text,w_100"]; [self testGravityUrl:CLDGravityOcrTextAdvOcr expectedValue:@"g_ocr_text:adv_ocr,w_100"]; } - (void)testGravityUrl:(CLDGravity)gravity expectedValue:(NSString*)expectedValue { // Given NSString* inputWidth = @"100"; NSString* inputPublicId = @"publicId"; BOOL inputSignUrl = false; NSString* expectedResult = [NSString stringWithFormat:@"%@/image/upload/%@/%@", prefix, expectedValue, inputPublicId]; // When CLDTransformation* transformation = [[[[CLDTransformation alloc] init] setWidth:inputWidth] setGravityWithGravity:gravity]; NSString* actualResult = [[[self.sut createUrl] setTransformation:transformation] generate:inputPublicId signUrl:inputSignUrl]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, @"Creating url with gravity enum should return expected result"); } // MARK: - long url signing - (void)test_longUrlSign_emptyApiSecret_shouldCreateExpectedSigning { // Given CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:@"" privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:YES signatureAlgorithm:SignatureAlgorithmSha1 signatureVersion: 2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:YES]; NSString* expectedResult = @"DUB-5kBqEhbyNmZ0oan_cTYdW-9HAh-O"; // When NSString* actualResult = [url componentsSeparatedByString:@"--"][1]; // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil"); XCTAssertTrue(actualResult.length <= 32, "encrypted component should not be longer than 32 charecters"); XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration with longUrlSignature = YES and call generate with signUrl = YES, should encrypt the ApiSecret with SHA256_base64"); } - (void)test_longUrlSign_normalApiSecret_shouldCreateExpectedSigning { // Given CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:@"apiSecret" privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:YES signatureAlgorithm:SignatureAlgorithmSha1 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:YES]; NSString* expectedResult = @"UHH8qJ2eIEoPHdVQP08BMEN9f4YUDavr"; // When NSString* actualResult = [url componentsSeparatedByString:@"--"][1]; // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil"); XCTAssertTrue(actualResult.length <= 32, "encrypted component should not be longer than 32 charecters"); XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration with longUrlSignature = YES and call generate with signUrl = YES, should encrypt the ApiSecret with SHA256_base64"); } - (void)test_longUrlSign_longApiSecret_shouldCreateExpectedSigning { // Given NSString* longString = @"abcdefghijklmnopqrstuvwxyz1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6"; CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:longString privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:YES signatureAlgorithm:SignatureAlgorithmSha1 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:YES]; NSString* expectedResult = @"7k8KYHY20iQ6sNTJIWb05ti7bYo1HG3R"; // When NSString* actualResult = [url componentsSeparatedByString:@"--"][1]; // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil"); XCTAssertTrue(actualResult.length <= 32, "encrypted component should not be longer than 32 charecters"); XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration with longUrlSignature and call generate with signUrl = YES, should encrypt the ApiSecret with SHA256_base64"); } - (void)test_longUrlSign_specialApiSecret_shouldCreateExpectedSigning { // Given NSString* specialString = @"🔭!@#$%^&*()_+±§?><`~"; CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:specialString privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:YES signatureAlgorithm:SignatureAlgorithmSha1 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:true]; NSString* expectedResult = @"g12ptQdGPID3Un4aOxZSuiEithIdT2Wm"; // When NSString* actualResult = [url componentsSeparatedByString:@"--"][1]; // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil"); XCTAssertTrue(actualResult.length <= 32, "encrypted component should not be longer than 32 charecters"); XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration with longUrlSignature and call generate with signUrl = YES, should encrypt the ApiSecret with SHA256_base64"); } - (void)test_longUrlSign_unset_shouldCreateExpectedSigning { // Given CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:@"apiSecret" privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:NO signatureAlgorithm:SignatureAlgorithmSha1 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:YES]; NSString* expectedResult = @"FhXe8ZZ3"; // When NSString* actualResult = [url componentsSeparatedByString:@"--"][1]; // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil"); XCTAssertTrue(actualResult.length <= 8, "encrypted component should not be longer than 8 charecters"); XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration with longUrlSignature = NO and call generate with signUrl = YES, should encrypt the ApiSecret with SHA1_base64"); } - (void)test_longUrlSign_signUrlFalse_shouldCreateExpectedSigning { // Given CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:@"apiSecret" privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:YES signatureAlgorithm:SignatureAlgorithmSha1 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:NO]; NSString* expectedResult = @"http://test123-res.cloudinary.com/image/upload/test.jpg"; // When NSString* actualResult = url; // Then XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration with longUrlSignature = YES and call generate with signUrl = NO, should not encrypt and add the ApiSecret to the url"); } - (void)test_longUrlSign_unset_shouldCreateExpectedUrl { // Given NSString* url = [[self.sut createUrl] generate:@"sample.jpg" signUrl:YES]; NSString* expectedResult = @"https://res.cloudinary.com/test123/image/upload/s--v2fTPYTu--/sample.jpg"; // When NSString* actualResult = url; // Then XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration with longUrlSignature = NO and call generate with signUrl = YES, should create a url with SHA1 encrypted apiSecret"); } - (void)test_longUrlSign_true_shouldCreateExpectedUrl { // Given NSString* longUrlSignatureQuery = @"?long_url_signature=true&analytics=false"; NSString* urlCredentials = @"cloudinary://a:b@test123"; NSString* fullUrl = [NSString stringWithFormat:@"%@%@",urlCredentials,longUrlSignatureQuery]; CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudinaryUrl:fullUrl]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[self.sut createUrl] generate:@"sample.jpg" signUrl:YES]; NSString* expectedResult = @"https://res.cloudinary.com/test123/image/upload/s--2hbrSMPOjj5BJ4xV7SgFbRDevFaQNUFf--/sample.jpg"; // When NSString* actualResult = url; // Then XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration with longUrlSignature = YES and call generate with signUrl = YES, should create a url with SHA256 encrypted apiSecret"); } // MARK: - signature algorithm -(void)test_signatureAlgorithm_emptyApiSecret_shouldCreateExpectedSigning { // Given CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:@"" privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:NO signatureAlgorithm:SignatureAlgorithmSha256 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:YES]; NSString* expectedResult = @"DUB-5kBq"; // When NSString* actualResult = [url componentsSeparatedByString:@"--"][1]; // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil"); XCTAssertTrue(actualResult.length <= 8, "encrypted component should not be longer than 8 charecters"); XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and call for signUrl = YES, should ecrypte with SHA256_base64"); } - (void)test_signatureAlgorithm_normalApiSecret_shouldCreateExpectedSigning { // Given CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:@"apiSecret" privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:NO signatureAlgorithm:SignatureAlgorithmSha256 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:YES]; NSString* expectedResult = @"UHH8qJ2e"; // When NSString* actualResult = [url componentsSeparatedByString:@"--"][1]; // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil"); XCTAssertTrue(actualResult.length <= 8, "encrypted component should not be longer than 8 charecters"); XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and call for signUrl = YES, should ecrypte with SHA256_base64"); } - (void)test_signatureAlgorithm_longApiSecret_shouldCreateExpectedSigning { // Given NSString* longString = @"abcdefghijklmnopqrstuvwxyz1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6"; CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:longString privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:NO signatureAlgorithm:SignatureAlgorithmSha256 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:YES]; NSString* expectedResult = @"7k8KYHY2"; // When NSString* actualResult = [url componentsSeparatedByString:@"--"][1]; // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil"); XCTAssertTrue(actualResult.length <= 8, "encrypted component should not be longer than 8 charecters"); XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and call for signUrl = YES, should ecrypte with SHA256_base64"); } - (void)test_signatureAlgorithm_specialApiSecret_shouldCreateExpectedSigning { // Given NSString* specialString = @"🔭!@#$%^&*()_+±§?><`~"; CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:specialString privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:NO signatureAlgorithm:SignatureAlgorithmSha256 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:YES]; NSString* expectedResult = @"g12ptQdG"; // When NSString* actualResult = [url componentsSeparatedByString:@"--"][1]; // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil"); XCTAssertTrue(actualResult.length <= 8, "encrypted component should not be longer than 8 charecters"); XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and call for signUrl = YES, should ecrypte with SHA256_base64"); } - (void)test_signatureAlgorithm_unset_shouldCreateExpectedSigning { // Given CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:@"apiSecret" privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:NO signatureAlgorithm:0 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:YES]; NSString* expectedResult = @"FhXe8ZZ3"; // When NSString* actualResult = [url componentsSeparatedByString:@"--"][1]; // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil"); XCTAssertTrue(actualResult.length <= 8, "encrypted component should not be longer than 8 charecters"); XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration without signatureAlgorithm and call for signUrl = true, should ecrypte with the default SHA1_base64"); } - (void)test_signatureAlgorithm_signUrlFalse_shouldCreateFullUrlWithoutSigning { // Given CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:@"apiSecret" privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:NO signatureAlgorithm:SignatureAlgorithmSha256 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:NO]; NSString* expectedResult = @"http://test123-res.cloudinary.com/image/upload/test.jpg"; // When NSString* actualResult = url; // Then XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and call for signUrl = false, should not encrypt nor add the ApiSecret to the url"); } - (void)test_signatureAlgorithm_unset_shouldCreateExpectedFullUrl { // Given NSString* url = [[self.sut createUrl] generate:@"sample.jpg" signUrl:YES]; NSString* expectedResult = @"https://res.cloudinary.com/test123/image/upload/s--v2fTPYTu--/sample.jpg"; // When NSString* actualResult = url; // Then XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration for default signatureAlgorithm and signUrl = true, should create a url with SHA1 encrypted apiSecret"); } - (void)test_signatureAlgorithm_sha256_shouldCreateExpectedFullUrl { // Given NSString* signatureAlgorithQuery = @"?signature_algorithm=sha256&analytics=false"; NSString* urlCredentials = @"cloudinary://a:b@test123"; NSString* fullUrl = [NSString stringWithFormat:@"%@%@",urlCredentials,signatureAlgorithQuery]; CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudinaryUrl:fullUrl]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[self.sut createUrl] generate:@"sample.jpg" signUrl:YES]; NSString* expectedResult = @"https://res.cloudinary.com/test123/image/upload/s--2hbrSMPO--/sample.jpg"; // When NSString* actualResult = url; // Then XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and signUrl = true, should create a url with SHA256 encrypted apiSecret"); } // MARK: - signing combinations - (void)test_signingCombinations_signTrueLongTrueAlgorithmSha1_shouldUse32CharsEcryptedSha256 { // Given CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:@"apiSecret" privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:YES signatureAlgorithm:SignatureAlgorithmSha1 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:YES]; NSString* expectedResult = @"UHH8qJ2eIEoPHdVQP08BMEN9f4YUDavr"; // When NSString* actualResult = [url componentsSeparatedByString:@"--"][1]; // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil"); XCTAssertTrue(actualResult.length <= 32, "encrypted component should not be longer than 32 charecters"); XCTAssertEqualObjects(actualResult, expectedResult, "longUrlSignature should override signing algorithm and force sha256 with 32 charecters"); } - (void)test_signingCombinations_signTrueLongTrueAlgorithmSha256_shouldUse32CharsEcryptedSha256 { // Given CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:@"apiSecret" privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:YES signatureAlgorithm:SignatureAlgorithmSha256 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:YES]; NSString* expectedResult = @"UHH8qJ2eIEoPHdVQP08BMEN9f4YUDavr"; // When NSString* actualResult = [url componentsSeparatedByString:@"--"][1]; // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil"); XCTAssertTrue(actualResult.length <= 32, "encrypted component should not be longer than 32 charecters"); XCTAssertEqualObjects(actualResult, expectedResult, "longUrlSignature should override signing algorithm and force sha256 with 32 charecters"); } - (void)test_signingCombinations_signFalseLongTrueAlgorithmSha1_shouldNotUseSigning { // Given CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:@"apiSecret" privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:YES signatureAlgorithm:SignatureAlgorithmSha1 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:NO]; NSString* expectedResult = @"http://test123-res.cloudinary.com/image/upload/test.jpg"; // When NSString* actualResult = url; // Then XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and longUrlSignature = true and call for signUrl = false, should not encrypt nor add the ApiSecret to the url"); } - (void)test_signingCombinations_signFalseLongTrueAlgorithmSha256_shouldUseSigning { // Given CLDConfiguration* config = [[CLDConfiguration alloc] initWithCloudName:@"test123" apiKey:@"apiKey" apiSecret:@"apiSecret" privateCdn:YES secure:NO cdnSubdomain:NO secureCdnSubdomain:NO longUrlSignature:YES signatureAlgorithm:SignatureAlgorithmSha256 signatureVersion:2 secureDistribution:nil cname:nil uploadPrefix:nil timeout:nil analytics:NO]; self.sut = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:nil]; NSString* url = [[[self.sut createUrl] setFormat:@"jpg"] generate:@"test" signUrl:NO]; NSString* expectedResult = @"http://test123-res.cloudinary.com/image/upload/test.jpg"; // When NSString* actualResult = url; // Then XCTAssertEqualObjects(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and longUrlSignature = true and call for signUrl = false, should not encrypt nor add the ApiSecret to the url"); } // MARK: - named spaces removal - (void)test_replaceSpaces_named_shouldCreateExpectedUrl { // Given NSString* inputWidth = @"100"; NSString* inputHeight = @"200"; NSString* inputNamed = @"named"; NSString* inputPublicId = @"publicId"; BOOL inputSignUrl = false; NSString* expectedResult = @"https://res.cloudinary.com/test123/image/upload/h_200,t_named,w_100/publicId"; // When CLDTransformation* transformation = [[[[[CLDTransformation alloc] init] setWidth:inputWidth] setNamed:inputNamed] setHeight:inputHeight]; NSString* actualResult = [[[self.sut createUrl] setTransformation:transformation] generate:inputPublicId signUrl:inputSignUrl]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, @"creating url with named in transformation should return the expected result"); } - (void)test_replaceSpaces_namedWithSpaces_shouldReplaceSpaces { // Given NSString* inputWidth = @"100"; NSString* inputHeight = @"200"; NSString* inputSpacedNamed = @"named with spaces"; NSString* inputPublicId = @"publicId"; BOOL inputSignUrl = false; NSString* expectedResult = @"https://res.cloudinary.com/test123/image/upload/h_200,t_named%20with%20spaces,w_100/publicId"; // When CLDTransformation* transformation = [[[[[CLDTransformation alloc] init] setWidth:inputWidth] setNamed:inputSpacedNamed] setHeight:inputHeight]; NSString* actualResult = [[[self.sut createUrl] setTransformation:transformation] generate:inputPublicId signUrl:inputSignUrl]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, @"creating url with named in transformation should return the expected result"); } - (void)test_replaceSpaces_namedArray_shouldCreateExpectedUrl { // Given NSString* inputWidth = @"100"; NSString* inputHeight = @"200"; NSString* inputNamed1 = @"named1"; NSString* inputNamed2 = @"named2"; NSString* inputPublicId = @"publicId"; BOOL inputSignUrl = false; NSString* expectedResult = @"https://res.cloudinary.com/test123/image/upload/h_200,t_named1.named2,w_100/publicId"; // When CLDTransformation* transformation = [[[[[CLDTransformation alloc] init] setWidth:inputWidth] setNamedWithArray:@[inputNamed1,inputNamed2]] setHeight:inputHeight]; NSString* actualResult = [[[self.sut createUrl] setTransformation:transformation] generate:inputPublicId signUrl:inputSignUrl]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, @"creating url with named in transformation should return the expected result"); } - (void)test_replaceSpaces_namedArrayWithSpaces_shouldReplaceSpaces { // Given NSString* inputWidth = @"100"; NSString* inputHeight = @"200"; NSString* inputSpacedNamed1 = @"named with spaces 1"; NSString* inputSpacedNamed2 = @"named with spaces 2"; NSString* inputPublicId = @"publicId"; BOOL inputSignUrl = false; NSString* expectedResult = @"https://res.cloudinary.com/test123/image/upload/h_200,t_named%20with%20spaces%201.named%20with%20spaces%202,w_100/publicId"; // When CLDTransformation* transformation = [[[[[CLDTransformation alloc] init] setWidth:inputWidth] setNamedWithArray:@[inputSpacedNamed1,inputSpacedNamed2]] setHeight:inputHeight]; NSString* actualResult = [[[self.sut createUrl] setTransformation:transformation] generate:inputPublicId signUrl:inputSignUrl]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, @"creating url with named in transformation should return the expected result"); } @end ================================================ FILE: Example/Tests/GenerateUrlTests/UrlTests.swift ================================================ // // UrlTests.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest @testable import Cloudinary class UrlTests: BaseTestCase { let prefix = "https://res.cloudinary.com/test123" var sut: CLDCloudinary? override func setUp() { super.setUp() let config = CLDConfiguration(cloudinaryUrl: "cloudinary://a:b@test123?analytics=false")! sut = CLDCloudinary(configuration: config) } override func tearDown() { super.tearDown() sut = nil } func testParseCloudinaryUrlNoPrivateCdn() { let config = CLDConfiguration(cloudinaryUrl: "cloudinary://abc:def@ghi") XCTAssertEqual(config?.apiKey, "abc") XCTAssertEqual(config?.apiSecret, "def") XCTAssertEqual(config?.cloudName, "ghi") XCTAssertEqual(config?.privateCdn, false) } func testParseCloudinaryUrlWithPrivateCdn() { let config = CLDConfiguration(cloudinaryUrl: "cloudinary://abc:def@ghi/jkl") XCTAssertEqual(config?.apiKey, "abc") XCTAssertEqual(config?.apiSecret, "def") XCTAssertEqual(config?.cloudName, "ghi") XCTAssertEqual(config?.privateCdn, true) XCTAssertEqual(config?.secureDistribution, "jkl") } func testCloudName() { let url = sut?.createUrl().generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/test") } func testCloudinaryUrlValidScheme() { let isValid = CLDConfiguration.validateUrl(url: "cloudinary://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test") XCTAssertTrue(isValid); } func testCloudinaryUrlInvalidScheme() { let isValid = CLDConfiguration.validateUrl(url: "https://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test") XCTAssertFalse(isValid); } func testCloudinaryUrlEmptyScheme() { let isValid = CLDConfiguration.validateUrl(url: "") XCTAssertFalse(isValid); } func testInitConfiguration(){ let config = CLDConfiguration(cloudinaryUrl: "https://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test") XCTAssertEqual(config, nil) let config2 = CLDConfiguration(cloudinaryUrl: "cloudinary://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test") XCTAssertNotNil(config2) } func testSecure() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", secure: true, analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/test") } func testSecureDistribution() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", secure: true, secureDistribution: "something.else.com", analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().generate("test") XCTAssertEqual(url, "https://something.else.com/test123/image/upload/test") } func testSecureAkamai() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true, secure: true, analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().generate("test") XCTAssertEqual(url, "https://test123-res.cloudinary.com/image/upload/test") } func testSecureNonAkamai() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true, secure: true, secureDistribution: "something.cloudfront.net", analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().generate("test") XCTAssertEqual(url, "https://something.cloudfront.net/image/upload/test") } func testHttpPrivateCdn() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true, analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().generate("test") XCTAssertEqual(url, "http://test123-res.cloudinary.com/image/upload/test") } func testCdnSubDomain() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", cdnSubdomain: true, analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().generate("test") XCTAssertEqual(url, "http://res-2.cloudinary.com/test123/image/upload/test") } func testSecureCdnSubDomainFalse() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", secure: true, cdnSubdomain: true, analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/test") } func testSecureCdnSubDomainTrue() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true, secure: true, cdnSubdomain: true, secureCdnSubdomain: true, analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().generate("test") XCTAssertEqual(url, "https://test123-res-2.cloudinary.com/image/upload/test") } func testAnalyticsTrue() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true, secure: true, cdnSubdomain: true, secureCdnSubdomain: true, analytics: true) config.analyticsObject.setSDKVersion(version: "3.3.0") config.analyticsObject.setTechVersion(version: "5.0") config.analyticsObject.setOsVersion(version: "17.0") sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().generate("test") XCTAssertEqual(url, "https://test123-res-2.cloudinary.com/image/upload/test?_a=DAEAEvAFBRA0") } func testFormat() { let url = sut?.createUrl().setFormat("jpg").generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/test.jpg") } func testCrop() { let trans = CLDTransformation().setWidth(100).setHeight(101) var url = sut?.createUrl().setTransformation(trans).generate("test") url = sut?.createUrl().setTransformation(CLDTransformation().setWidth(100).setHeight(101).setCrop(.crop)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_crop,h_101,w_100/test") } func testVariousOptions() { let url = sut?.createUrl().setTransformation(CLDTransformation().setX(1).setY(2).setRadius(3).setGravity(.center).setQuality("0.4").setPrefix("a")).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/g_center,p_a,q_0.4,r_3,x_1,y_2/test") let urlRadius = sut?.createUrl().setTransformation(CLDTransformation().setX(1).setY(2).setRadius([10,20,"30","$v"]).setGravity(.center).setQuality("0.4").setPrefix("a")).generate("test") XCTAssertEqual(urlRadius, "\(prefix)/image/upload/g_center,p_a,q_0.4,r_10:20:30:$v,x_1,y_2/test") let urlRadius2 = sut?.createUrl().setTransformation(CLDTransformation().setX(1).setY(2).setRadius([20,0,"3"]).setGravity(.center).setQuality("0.4").setPrefix("a")).generate("test") XCTAssertEqual(urlRadius2, "\(prefix)/image/upload/g_center,p_a,q_0.4,r_20:0:3,x_1,y_2/test") } func testQuality() { let urlAuto = sut?.createUrl().setTransformation(CLDTransformation().setX(1).setY(2).setRadius(3).setGravity(.center).setQuality(.auto()).setPrefix("a")).generate("test") XCTAssertEqual(urlAuto, "\(prefix)/image/upload/g_center,p_a,q_auto,r_3,x_1,y_2/test") let urlEco = sut?.createUrl().setTransformation(CLDTransformation().setX(1).setY(2).setRadius(3).setGravity(.center).setQuality(.auto(.eco)).setPrefix("a")).generate("test") XCTAssertEqual(urlEco, "\(prefix)/image/upload/g_center,p_a,q_auto:eco,r_3,x_1,y_2/test") let urlFixed = sut?.createUrl().setTransformation(CLDTransformation().setX(1).setY(2).setRadius(3).setGravity(.center).setQuality(.fixed(50)).setPrefix("a")).generate("test") XCTAssertEqual(urlFixed, "\(prefix)/image/upload/g_center,p_a,q_50,r_3,x_1,y_2/test") let urlJpegMini = sut?.createUrl().setTransformation(CLDTransformation().setX(1).setY(2).setRadius(3).setGravity(.center).setQuality(.jpegMini()).setPrefix("a")).generate("test") XCTAssertEqual(urlJpegMini, "\(prefix)/image/upload/g_center,p_a,q_jpegmini,r_3,x_1,y_2/test") } func testTransformationSimple() { let url = sut?.createUrl().setTransformation(CLDTransformation().setNamed(["blip"])).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/t_blip/test") } func testTransformationArray() { let url = sut?.createUrl().setTransformation(CLDTransformation().setNamed(["blip", "blop"])).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/t_blip.blop/test") } func testBaseTransformations() { let url = sut?.createUrl().setTransformation(CLDTransformation().setX(100).setY(100).setCrop(.fill).chain().setCrop(.crop).setWidth(100)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_fill,x_100,y_100/c_crop,w_100/test") } func testContextMetadataToUserVariables() { let url = sut?.createUrl().setTransformation(CLDTransformation() .setVariable(CLDVariable(name: "$xpos", value: "ctx:!x_pos!_to_f")) .setVariable(CLDVariable(name: "$ypos", value: "ctx:!y_pos!_to_f")) .setCrop("crop") .setX("$xpos * w") .setY("$ypos * h")).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/$xpos_ctx:!x_pos!_to_f,$ypos_ctx:!y_pos!_to_f,c_crop,x_$xpos_mul_w,y_$ypos_mul_h/test") } func testBaseTransformationArray() { let url = sut?.createUrl().setTransformation(CLDTransformation().setX(100).setY(100).setWidth(200).setCrop(.fill).chain().setRadius(10).chain().setCrop(.crop).setWidth(100)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_fill,w_200,x_100,y_100/r_10/c_crop,w_100/test") } func test_CLDTransformation_setCrop_validUrl() { var url : String? url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.fill)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_fill/test") url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.crop)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_crop/test") url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.scale)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_scale/test") url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.fit)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_fit/test") url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.limit)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_limit/test") url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.mFit)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_mfit/test") url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.lFill)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_lfill/test") url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.pad)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_pad/test") url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.lPad)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_lpad/test") url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.mPad)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_mpad/test") url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.thumb)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_thumb/test") url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.imaggaCrop)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_imagga_crop/test") url = sut?.createUrl().setTransformation(CLDTransformation().setCrop(.imaggaScale)).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_imagga_scale/test") } func testNoEmptyTransformation() { let url = sut?.createUrl().setTransformation(CLDTransformation().setX(100).setY(100).setCrop(.fill).chain()).generate("test") XCTAssertEqual(url, "\(prefix)/image/upload/c_fill,x_100,y_100/test") } func testType() { let url = sut?.createUrl().setType(.facebook).generate("test") XCTAssertEqual(url, "\(prefix)/image/facebook/test") } func testResourceType() { let url = sut?.createUrl().setResourceType(.raw).generate("test") XCTAssertEqual(url, "\(prefix)/raw/upload/test") } func testFetch() { let url = sut?.createUrl().setType(.fetch).generate("http://blah.com/hello?a=b") XCTAssertEqual(url, "\(prefix)/image/fetch/http://blah.com/hello%3Fa%3Db") } func testCname() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", cname: "hello.com", analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().generate("test") XCTAssertEqual(url, "http://hello.com/test123/image/upload/test") } func testCnameSubdomain() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", cdnSubdomain: true, cname: "hello.com", analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().generate("test") XCTAssertEqual(url, "http://a2.hello.com/test123/image/upload/test") } func testUrlSuffixShared() { XCTAssertNil(sut?.createUrl().setSuffix("hello").generate("test")) } func testUrlSuffixNonUpload() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true) sut = CLDCloudinary(configuration: config) XCTAssertNil(sut?.createUrl().setType(.facebook).setSuffix("hello").generate("test")) } func testUrlSuffixDisallowedChars() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true) sut = CLDCloudinary(configuration: config) XCTAssertNil(sut?.createUrl().setSuffix("hello/world").generate("test")) XCTAssertNil(sut?.createUrl().setSuffix("hello.world").generate("test")) } func testUrlSuffixPrivateCdn() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true, analytics: false) sut = CLDCloudinary(configuration: config) XCTAssertEqual(sut?.createUrl().setSuffix("hello").generate("test"), "http://test123-res.cloudinary.com/images/test/hello") XCTAssertEqual(sut?.createUrl().setSuffix("hello").setTransformation(CLDTransformation().setAngle(0)).generate("test"), "http://test123-res.cloudinary.com/images/a_0/test/hello") } func testUrlSuffixFormat() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true, analytics: false) sut = CLDCloudinary(configuration: config) XCTAssertEqual(sut?.createUrl().setSuffix("hello").setFormat("jpg").generate("test"), "http://test123-res.cloudinary.com/images/test/hello.jpg") } func testUrlSuffixSign() { var url1 = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) var sig1 = url1?.components(separatedBy: "--")[1] var config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true) sut = CLDCloudinary(configuration: config) var url2 = sut?.createUrl().setSuffix("hello").setFormat("jpg").generate("test", signUrl: true) var sig2 = url2?.components(separatedBy: "--")[1] XCTAssertEqual(sig1, sig2) url1 = sut?.createUrl().setFormat("jpg").setTransformation(CLDTransformation().setAngle(0)).generate("test", signUrl: true) sig1 = url1?.components(separatedBy: "--")[1] config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true) sut = CLDCloudinary(configuration: config) url2 = sut?.createUrl().setSuffix("hello").setFormat("jpg").setTransformation(CLDTransformation().setAngle(0)).generate("test", signUrl: true) sig2 = url2?.components(separatedBy: "--")[1] XCTAssertEqual(sig1, sig2) } func testUrlSuffixRaw() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true, analytics: false) sut = CLDCloudinary(configuration: config) XCTAssertEqual(sut?.createUrl().setSuffix("hello").setResourceType(.raw).generate("test"), "http://test123-res.cloudinary.com/files/test/hello") } func testUrlSuffixPrivate() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true, analytics: false) sut = CLDCloudinary(configuration: config) XCTAssertEqual(sut?.createUrl().setSuffix("hello").setResourceType(.image).setType(.private).generate("test"), "http://test123-res.cloudinary.com/private_images/test/hello") } // MARK: - long url signature func test_longUrlSign_emptyApiSecret_shouldCreateExpectedSigning() { // Given let config = CLDConfiguration(cloudName: "test123", apiKey: "apiKey", apiSecret: "", privateCdn: true, longUrlSignature: true) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) let expectedResult = "DUB-5kBqEhbyNmZ0oan_cTYdW-9HAh-O" // When let actualResult = url!.components(separatedBy: "--")[1] // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil") XCTAssertTrue(actualResult.count <= 32, "encrypted component should not be longer than 32 charecters") XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for longUrlSignature and call for signUrl = true, should encrypt the ApiSecret with SHA256_base64") } func test_longUrlSign_normalApiSecret_shouldCreateExpectedSigning() { // Given let config = CLDConfiguration(cloudName: "test123", apiKey: "apiKey", apiSecret: "apiSecret", privateCdn: true, longUrlSignature: true) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) let expectedResult = "UHH8qJ2eIEoPHdVQP08BMEN9f4YUDavr" // When let actualResult = url!.components(separatedBy: "--")[1] // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil") XCTAssertTrue(actualResult.count <= 32, "encrypted component should not be longer than 32 charecters") XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for longUrlSignature and call for signUrl = true, should encrypt the ApiSecret with SHA256_base64") } func test_longUrlSign_longApiSecret_shouldCreateExpectedSigning() { // Given let longString = "abcdefghijklmnopqrstuvwxyz1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6" let config = CLDConfiguration(cloudName: "test123", apiSecret: longString, privateCdn: true, longUrlSignature: true) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) let expectedResult = "7k8KYHY20iQ6sNTJIWb05ti7bYo1HG3R" // When let actualResult = url!.components(separatedBy: "--")[1] // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil") XCTAssertTrue(actualResult.count <= 32, "encrypted component should not be longer than 32 charecters") XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for longUrlSignature and call for signUrl = true, should encrypt the ApiSecret with SHA256_base64") } func test_longUrlSign_specialApiSecret_shouldCreateExpectedSigning() { // Given let longString = "🔭!@#$%^&*()_+±§?><`~" let config = CLDConfiguration(cloudName: "test123", apiSecret: longString, privateCdn: true, longUrlSignature: true) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) let expectedResult = "g12ptQdGPID3Un4aOxZSuiEithIdT2Wm" // When let actualResult = url!.components(separatedBy: "--")[1] // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil") XCTAssertTrue(actualResult.count <= 32, "encrypted component should not be longer than 32 charecters") XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for longUrlSignature and call for signUrl = true, should encrypt the ApiSecret with SHA256_base64") } func test_longUrlSign_unset_shouldCreateExpectedSigning() { // Given let config = CLDConfiguration(cloudName: "test123", apiKey: "apiKey", apiSecret: "apiSecret", privateCdn: true) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) let expectedResult = "FhXe8ZZ3" // When let actualResult = url!.components(separatedBy: "--")[1] // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil") XCTAssertTrue(actualResult.count <= 8, "short encrypted component should not be longer than 8 charecters") XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for longUrlSignature = false and call for signUrl = true, should encrypt the ApiSecret with SHA1_base64") } func test_longUrlSign_signUrlFalse_shouldNotUseSigning() { // Given let config = CLDConfiguration(cloudName: "test123", apiKey: "apiKey", apiSecret: "apiSecret", privateCdn: true, longUrlSignature: true, analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: false) let expectedResult = "http://test123-res.cloudinary.com/image/upload/test.jpg" // When let actualResult = url! // Then XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for longUrlSignature = true and call for signUrl = false, should not encrypt and add the ApiSecret to the url") } func test_longUrlSign_unset_shouldCreateExpectedUrl() { // Given let url = sut?.createUrl().generate("sample.jpg", signUrl: true) let expectedResult = "https://res.cloudinary.com/test123/image/upload/s--v2fTPYTu--/sample.jpg" // When let actualResult = url! // Then XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for longUrlSignature = false and signUrl = true, should create a url with SHA1 encrypted apiSecret") } func test_longUrlSign_true_shouldCreateExpectedUrl() { // Given let longUrlSignatureQuery = ("?analytics=false&\(CLDConfiguration.ConfigParam.LongUrlSignature.description)=true") let urlCredentials = "cloudinary://a:b@test123" let fullUrl = urlCredentials + longUrlSignatureQuery let config = CLDConfiguration(cloudinaryUrl: fullUrl) sut = CLDCloudinary(configuration: config!) let url = sut?.createUrl().generate("sample.jpg", signUrl: true) let expectedResult = "https://res.cloudinary.com/test123/image/upload/s--2hbrSMPOjj5BJ4xV7SgFbRDevFaQNUFf--/sample.jpg" // When let actualResult = url! // Then XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for longUrlSignature = true and signUrl = true, should create a url with SHA256 encrypted apiSecret") } // MARK: - signature algorithm func test_signatureAlgorithm_emptyApiSecret_shouldCreateExpectedSigning() { // Given let config = CLDConfiguration(cloudName: "test123", apiKey: "apiKey", apiSecret: "", privateCdn: true, signatureAlgorithm: .sha256) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) let expectedResult = "DUB-5kBq" // When let actualResult = url!.components(separatedBy: "--")[1] // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil") XCTAssertTrue(actualResult.count <= 8, "encrypted component should not be longer than 8 charecters") XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and call for signUrl = true, should ecrypte with SHA256_base64") } func test_signatureAlgorithm_normalApiSecret_shouldCreateExpectedSigning() { // Given let config = CLDConfiguration(cloudName: "test123", apiKey: "apiKey", apiSecret: "apiSecret", privateCdn: true, signatureAlgorithm: .sha256) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) let expectedResult = "UHH8qJ2e" // When let actualResult = url!.components(separatedBy: "--")[1] // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil") XCTAssertTrue(actualResult.count <= 8, "encrypted component should not be longer than 8 charecters") XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and call for signUrl = true, should ecrypte with SHA256_base64") } func test_signatureAlgorithm_longApiSecret_shouldCreateExpectedSigning() { // Given let longString = "abcdefghijklmnopqrstuvwxyz1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6" let config = CLDConfiguration(cloudName: "test123", apiSecret: longString, privateCdn: true, signatureAlgorithm: .sha256) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) let expectedResult = "7k8KYHY2" // When let actualResult = url!.components(separatedBy: "--")[1] // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil") XCTAssertTrue(actualResult.count <= 8, "encrypted component should not be longer than 8 charecters") XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and call for signUrl = true, should ecrypte with SHA256_base64") } func test_signatureAlgorithm_specialApiSecret_shouldCreateExpectedSigning() { // Given let longString = "🔭!@#$%^&*()_+±§?><`~" let config = CLDConfiguration(cloudName: "test123", apiSecret: longString, privateCdn: true, signatureAlgorithm: .sha256) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) let expectedResult = "g12ptQdG" // When let actualResult = url!.components(separatedBy: "--")[1] // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil") XCTAssertTrue(actualResult.count <= 8, "encrypted component should not be longer than 8 charecters") XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and call for signUrl = true, should ecrypte with SHA256_base64") } func test_signatureAlgorithm_unset_shouldCreateExpectedSigning() { // Given let config = CLDConfiguration(cloudName: "test123", apiKey: "apiKey", apiSecret: "apiSecret", privateCdn: true) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) let expectedResult = "FhXe8ZZ3" // When let actualResult = url!.components(separatedBy: "--")[1] // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil") XCTAssertTrue(actualResult.count <= 8, "encrypted component should not be longer than 8 charecters") XCTAssertEqual(actualResult, expectedResult, "Setting the configuration without signatureAlgorithm and call for signUrl = true, should ecrypte with the default SHA1_base64") } func test_signatureAlgorithm_signUrlFalse_shouldCreateFullUrlWithoutSigning() { // Given let config = CLDConfiguration(cloudName: "test123", apiKey: "apiKey", apiSecret: "apiSecret", privateCdn: true, signatureAlgorithm: .sha256, analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: false) let expectedResult = "http://test123-res.cloudinary.com/image/upload/test.jpg" // When let actualResult = url! // Then XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and call for signUrl = false, should not encrypt nor add the ApiSecret to the url") } func test_signatureAlgorithm_unset_shouldCreateExpectedFullUrl() { // Given let url = sut?.createUrl().generate("sample.jpg", signUrl: true) let expectedResult = "https://res.cloudinary.com/test123/image/upload/s--v2fTPYTu--/sample.jpg" // When let actualResult = url! // Then XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for default signatureAlgorithm and signUrl = true, should create a url with SHA1 encrypted apiSecret") } func test_signatureAlgorithm_sha256_shouldCreateExpectedFullUrl() { // Given let signatureAlgorithmQuery = ("?analytics=false&\(CLDConfiguration.ConfigParam.SignatureAlgorithm.description)=sha256") let urlCredentials = "cloudinary://a:b@test123" let fullUrl = urlCredentials + signatureAlgorithmQuery let config = CLDConfiguration(cloudinaryUrl: fullUrl) sut = CLDCloudinary(configuration: config!) let url = sut?.createUrl().generate("sample.jpg", signUrl: true) let expectedResult = "https://res.cloudinary.com/test123/image/upload/s--2hbrSMPO--/sample.jpg" // When let actualResult = url! // Then XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and signUrl = true, should create a url with SHA256 encrypted apiSecret") } // MARK: - signing combinations func test_signingCombinations_signTrueLongTrueAlgorithmSha1_shouldUse32CharsEcryptedSha256() { // Given let config = CLDConfiguration(cloudName: "test123", apiKey: "apiKey", apiSecret: "apiSecret", privateCdn: true, longUrlSignature: true, signatureAlgorithm: .sha1) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) let expectedResult = "UHH8qJ2eIEoPHdVQP08BMEN9f4YUDavr" // When let actualResult = url!.components(separatedBy: "--")[1] // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil") XCTAssertTrue(actualResult.count <= 32, "encrypted component should not be longer than 32 charecters") XCTAssertEqual(actualResult, expectedResult, "longUrlSignature should override signing algorithm and force sha256 with 32 charecters") } func test_signingCombinations_signTrueLongTrueAlgorithmSha256_shouldUse32CharsEcryptedSha256() { // Given let config = CLDConfiguration(cloudName: "test123", apiKey: "apiKey", apiSecret: "apiSecret", privateCdn: true, longUrlSignature: true, signatureAlgorithm: .sha1) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: true) let expectedResult = "UHH8qJ2eIEoPHdVQP08BMEN9f4YUDavr" // When let actualResult = url!.components(separatedBy: "--")[1] // Then XCTAssertNotNil(actualResult, "encrypted component should not be nil") XCTAssertTrue(actualResult.count <= 32, "encrypted component should not be longer than 32 charecters") XCTAssertEqual(actualResult, expectedResult, "longUrlSignature should override signing algorithm and force sha256 with 32 charecters") } func test_signingCombinations_signFalseLongTrueAlgorithmSha1_shouldNotUseSigning() { // Given let config = CLDConfiguration(cloudName: "test123", apiKey: "apiKey", apiSecret: "apiSecret", privateCdn: true, longUrlSignature: true, signatureAlgorithm: .sha1, analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: false) let expectedResult = "http://test123-res.cloudinary.com/image/upload/test.jpg" // When let actualResult = url! // Then XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and longUrlSignature = true and call for signUrl = false, should not encrypt nor add the ApiSecret to the url") } func test_signingCombinations_signFalseLongTrueAlgorithmSha256_shouldUseSigning() { // Given let config = CLDConfiguration(cloudName: "test123", apiKey: "apiKey", apiSecret: "apiSecret", privateCdn: true, longUrlSignature: true, signatureAlgorithm: .sha256, analytics: false) sut = CLDCloudinary(configuration: config) let url = sut?.createUrl().setFormat("jpg").generate("test", signUrl: false) let expectedResult = "http://test123-res.cloudinary.com/image/upload/test.jpg" // When let actualResult = url! // Then XCTAssertEqual(actualResult, expectedResult, "Setting the configuration for signatureAlgorithm to sha256 and longUrlSignature = true and call for signUrl = false, should not encrypt nor add the ApiSecret to the url") } // MARK: - root path func testUseRootPathShared() { XCTAssertEqual(sut?.createUrl().setUseRootPath(true).generate("test"), "\(prefix)/test") XCTAssertEqual(sut?.createUrl().setUseRootPath(true).setTransformation(CLDTransformation().setAngle(0)).generate("test"), "\(prefix)/a_0/test") } func testUseRootPathNonImageUpload() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true) sut = CLDCloudinary(configuration: config) XCTAssertNil(sut?.createUrl().setUseRootPath(true).setType(.facebook).generate("test")) XCTAssertNil(sut?.createUrl().setUseRootPath(true).setResourceType(.raw).generate("test")) } func testUseRootPathPrivateCdn() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true, analytics: false) sut = CLDCloudinary(configuration: config) XCTAssertEqual(sut?.createUrl().setUseRootPath(true).generate("test"), "http://test123-res.cloudinary.com/test") XCTAssertEqual(sut?.createUrl().setUseRootPath(true).setTransformation(CLDTransformation().setAngle(0)).generate("test"), "http://test123-res.cloudinary.com/a_0/test") } func testUseRootPathUrlSuffixPrivateCdn() { let config = CLDConfiguration(cloudName: "test123", apiKey: "a", apiSecret: "b", privateCdn: true, analytics: false) sut = CLDCloudinary(configuration: config) XCTAssertEqual(sut?.createUrl().setUseRootPath(true).setSuffix("hello").generate("test"), "http://test123-res.cloudinary.com/test/hello") } func testHttpEscape() { XCTAssertEqual(sut?.createUrl().setType("youtube").generate("http://www.youtube.com/watch?v=d9NF2edxy-M"), "\(prefix)/image/youtube/http://www.youtube.com/watch%3Fv%3Dd9NF2edxy-M") } func testDoubleSlash() { XCTAssertEqual(sut?.createUrl().setType("youtube").generate("http://cloudinary.com//images//logo.png"), "\(prefix)/image/youtube/http://cloudinary.com/images/logo.png") } func testBackground() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setBackground("red")).generate("test"), "\(prefix)/image/upload/b_red/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setBackground("#112233")).generate("test"), "\(prefix)/image/upload/b_rgb:112233/test") } func testKeyframeInterval() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setKeyframeInterval(interval: 10)).generate("test"), "\(prefix)/image/upload/ki_10.0/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setKeyframeInterval(interval: 0.05)).generate("test"), "\(prefix)/image/upload/ki_0.05/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setKeyframeInterval(interval: 3.45)).generate("test"), "\(prefix)/image/upload/ki_3.45/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setKeyframeInterval(interval: 300)).generate("test"), "\(prefix)/image/upload/ki_300.0/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setKeyframeInterval("10")).generate("test"), "\(prefix)/image/upload/ki_10/test") } func testDefaultImage() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setDefaultImage("default")).generate("test"), "\(prefix)/image/upload/d_default/test") } func testAngle() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setAngle(12)).generate("test"), "\(prefix)/image/upload/a_12/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setAngle(["exif", "12"])).generate("test"), "\(prefix)/image/upload/a_exif.12/test") } func testOverlay() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlay("text:hello")).generate("test"), "\(prefix)/image/upload/l_text:hello/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlay("text:hello").setWidth(100).setHeight(100)).generate("test"), "\(prefix)/image/upload/h_100,l_text:hello,w_100/test") } func testUnderlay() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setUnderlay("text:hello")).generate("test"), "\(prefix)/image/upload/u_text:hello/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setUnderlay("text:hello").setWidth(100).setHeight(100)).generate("test"), "\(prefix)/image/upload/h_100,u_text:hello,w_100/test") } func testFetchFormat() { XCTAssertEqual(sut?.createUrl().setType(.fetch).setFormat("jpg").generate("http://cloudinary.com/images/logo.png"), "\(prefix)/image/fetch/f_jpg/http://cloudinary.com/images/logo.png") } func testEffect() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.sepia)).generate("test"), "\(prefix)/image/upload/e_sepia/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.hue)).generate("test"), "\(prefix)/image/upload/e_hue/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.red)).generate("test"), "\(prefix)/image/upload/e_red/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.green)).generate("test"), "\(prefix)/image/upload/e_green/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.blue)).generate("test"), "\(prefix)/image/upload/e_blue/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.negate)).generate("test"), "\(prefix)/image/upload/e_negate/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.brightness)).generate("test"), "\(prefix)/image/upload/e_brightness/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.sepia)).generate("test"), "\(prefix)/image/upload/e_sepia/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.grayscale)).generate("test"), "\(prefix)/image/upload/e_grayscale/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.blackwhite)).generate("test"), "\(prefix)/image/upload/e_blackwhite/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.saturation)).generate("test"), "\(prefix)/image/upload/e_saturation/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.colorize)).generate("test"), "\(prefix)/image/upload/e_colorize/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.contrast)).generate("test"), "\(prefix)/image/upload/e_contrast/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.autoContrast)).generate("test"), "\(prefix)/image/upload/e_auto_contrast/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.vibrance)).generate("test"), "\(prefix)/image/upload/e_vibrance/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.autoColor)).generate("test"), "\(prefix)/image/upload/e_auto_color/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.improve)).generate("test"), "\(prefix)/image/upload/e_improve/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.autoBrightness)).generate("test"), "\(prefix)/image/upload/e_auto_brightness/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.fillLight)).generate("test"), "\(prefix)/image/upload/e_fill_light/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.viesusCorrect)).generate("test"), "\(prefix)/image/upload/e_viesus_correct/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.gamma)).generate("test"), "\(prefix)/image/upload/e_gamma/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.screen)).generate("test"), "\(prefix)/image/upload/e_screen/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.multiply)).generate("test"), "\(prefix)/image/upload/e_multiply/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.overlay)).generate("test"), "\(prefix)/image/upload/e_overlay/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.makeTransparent)).generate("test"), "\(prefix)/image/upload/e_make_transparent/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.trim)).generate("test"), "\(prefix)/image/upload/e_trim/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.shadow)).generate("test"), "\(prefix)/image/upload/e_shadow/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.distort)).generate("test"), "\(prefix)/image/upload/e_distort/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.shear)).generate("test"), "\(prefix)/image/upload/e_shear/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.displace)).generate("test"), "\(prefix)/image/upload/e_displace/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.oilPaint)).generate("test"), "\(prefix)/image/upload/e_oil_paint/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.redeye)).generate("test"), "\(prefix)/image/upload/e_redeye/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.advRedeye)).generate("test"), "\(prefix)/image/upload/e_adv_redeye/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.vignette)).generate("test"), "\(prefix)/image/upload/e_vignette/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.gradientFade)).generate("test"), "\(prefix)/image/upload/e_gradient_fade/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.pixelate)).generate("test"), "\(prefix)/image/upload/e_pixelate/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.pixelateRegion)).generate("test"), "\(prefix)/image/upload/e_pixelate_region/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.pixelateFaces)).generate("test"), "\(prefix)/image/upload/e_pixelate_faces/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.blur)).generate("test"), "\(prefix)/image/upload/e_blur/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.blurRegion)).generate("test"), "\(prefix)/image/upload/e_blur_region/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.blurFaces)).generate("test"), "\(prefix)/image/upload/e_blur_faces/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.sharpen)).generate("test"), "\(prefix)/image/upload/e_sharpen/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.unsharpMask)).generate("test"), "\(prefix)/image/upload/e_unsharp_mask/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.orderedDither)).generate("test"), "\(prefix)/image/upload/e_ordered_dither/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.art)).generate("test"), "\(prefix)/image/upload/e_art/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.assistColorblind)).generate("test"), "\(prefix)/image/upload/e_assist_colorblind/test") XCTAssertEqual(sut?.createUrl().setResourceType(.video).setTransformation(CLDTransformation().setEffect(.preview)).generate("test"), "\(prefix)/video/upload/e_preview/test") XCTAssertEqual(sut?.createUrl().setResourceType(.video).setTransformation(CLDTransformation().setEffect(.preview, param: "duration_2")).generate("test"), "\(prefix)/video/upload/e_preview:duration_2/test") } func testEffectWithParam() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.sepia, param: "10")).generate("test"), "\(prefix)/image/upload/e_sepia:10/test") } func testArtisticEffect(){ XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.alDente)).generate("test"), "\(prefix)/image/upload/e_art:al_dente/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.athena)).generate("test"), "\(prefix)/image/upload/e_art:athena/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.audrey)).generate("test"), "\(prefix)/image/upload/e_art:audrey/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.aurora)).generate("test"), "\(prefix)/image/upload/e_art:aurora/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.daguerre)).generate("test"), "\(prefix)/image/upload/e_art:daguerre/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.eucalyptus)).generate("test"), "\(prefix)/image/upload/e_art:eucalyptus/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.fes)).generate("test"), "\(prefix)/image/upload/e_art:fes/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.frost)).generate("test"), "\(prefix)/image/upload/e_art:frost/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.hairspray)).generate("test"), "\(prefix)/image/upload/e_art:hairspray/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.hokusai)).generate("test"), "\(prefix)/image/upload/e_art:hokusai/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.incognito)).generate("test"), "\(prefix)/image/upload/e_art:incognito/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.linen)).generate("test"), "\(prefix)/image/upload/e_art:linen/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.peacock)).generate("test"), "\(prefix)/image/upload/e_art:peacock/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.primavera)).generate("test"), "\(prefix)/image/upload/e_art:primavera/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.quartz)).generate("test"), "\(prefix)/image/upload/e_art:quartz/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.redRock)).generate("test"), "\(prefix)/image/upload/e_art:red_rock/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.refresh)).generate("test"), "\(prefix)/image/upload/e_art:refresh/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.sizzle)).generate("test"), "\(prefix)/image/upload/e_art:sizzle/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.sonnet)).generate("test"), "\(prefix)/image/upload/e_art:sonnet/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.ukulele)).generate("test"), "\(prefix)/image/upload/e_art:ukulele/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setEffect(.zorro)).generate("test"), "\(prefix)/image/upload/e_art:zorro/test") } func testDensity() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setDensity(150)).generate("test"), "\(prefix)/image/upload/dn_150/test") } func testPage() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setPage(5)).generate("test"), "\(prefix)/image/upload/pg_5/test") } func testBorder() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setBorder(5, color: "black")).generate("test"), "\(prefix)/image/upload/bo_5px_solid_black/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setBorder(5, color: "#ffaabbdd")).generate("test"), "\(prefix)/image/upload/bo_5px_solid_rgb:ffaabbdd/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setBorder("1px_solid_blue")).generate("test"), "\(prefix)/image/upload/bo_1px_solid_blue/test") } func testFlags() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setFlags(["abc"])).generate("test"), "\(prefix)/image/upload/fl_abc/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setFlags(["abc", "def"])).generate("test"), "\(prefix)/image/upload/fl_abc.def/test") } func testDprFloat() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setDpr(2.0)).generate("test"), "\(prefix)/image/upload/dpr_2.0/test") } func testDprAuto() { let url = sut?.createUrl().setTransformation(CLDTransformation().setDprAuto()).generate("test") var dprValue = "" if let finalUrl = url, let range = finalUrl.range(of: "dpr_") { let startIndex = range.upperBound dprValue = String(finalUrl[startIndex..<(finalUrl.index(startIndex, offsetBy: 1))]) } if !dprValue.isEmpty { XCTAssert(Int(dprValue) != nil, "DPR value should be transformed to Int value") } else { XCTFail("should find DPR Value") } } func testAspectRatio() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setAspectRatio(2.0)).generate("test"), "\(prefix)/image/upload/ar_2.0/test") } func testSignature() { let sig = cloudinarySignParamsUsingSecret(["a": "b", "c": "d", "e": ""], cloudinaryApiSecret: "abcd") XCTAssertEqual(sig, "ef1f04e0c1af08208a3dd28483107bc7f4a61209") } func testFolders() { XCTAssertEqual(sut?.createUrl().generate("folder/test"), "\(prefix)/image/upload/v1/folder/test") XCTAssertEqual(sut?.createUrl().setVersion("123").generate("folder/test"), "\(prefix)/image/upload/v123/folder/test") } func testFoldersWithForceVersion(){ // should not add version if the user turned off forceVersion var result = sut?.createUrl().setForceVersion(false).generate("folder/test") XCTAssertEqual("\(prefix)/image/upload/folder/test", result) // should still show explicit version if passed by the user result = sut?.createUrl().setForceVersion(false).setVersion("1234").generate("folder/test") XCTAssertEqual("\(prefix)/image/upload/v1234/folder/test", result) // should add version if no value specified for forceVersion: result = sut?.createUrl().generate("folder/test") XCTAssertEqual("\(prefix)/image/upload/v1/folder/test", result) // should add version if forceVersion is true result = sut?.createUrl().setForceVersion(true).generate("folder/test"); XCTAssertEqual("\(prefix)/image/upload/v1/folder/test", result) // should not use v1 if explicit version is passed result = sut?.createUrl().setForceVersion(true).setVersion("1234").generate("folder/test"); XCTAssertEqual("\(prefix)/image/upload/v1234/folder/test", result) } func testFoldersWithVersion() { XCTAssertEqual(sut?.createUrl().generate("v1234/test"), "\(prefix)/image/upload/v1234/test") } func testShorten() { XCTAssertEqual(sut?.createUrl().setShortenUrl(true).generate("test"), "\(prefix)/iu/test") } func testSignUrls() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setWidth(10).setHeight(20).setCrop(.crop)).setVersion("1234").generate("image.jpg", signUrl: true), "\(prefix)/image/upload/s--Ai4Znfl3--/c_crop,h_20,w_10/v1234/image.jpg") } func testEscapePublicId() { let tests = ["a b": "a%20b", "a+b": "a%2Bb", "a%20b": "a%20b", "a-b": "a-b", "a??b": "a%3F%3Fb"] for key in tests.keys { XCTAssertEqual(sut?.createUrl().generate(key), "\(prefix)/image/upload/\(tests[key]!)") } } func testPreloadedImage() { XCTAssertEqual(sut?.createUrl().generate("raw/private/v1234567/document.docx"), "\(prefix)/raw/private/v1234567/document.docx") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setWidth(1.1).setCrop(.scale)).generate("image/private/v1234567/img.jpg"), "\(prefix)/image/private/c_scale,w_1.1/v1234567/img.jpg") } func testGravity() { testGravityUrl(CLDTransformation.CLDGravity.center, "c_crop,g_center,w_100") testGravityUrl(CLDTransformation.CLDGravity.auto, "c_crop,g_auto,w_100") testGravityUrl(CLDTransformation.CLDGravity.face, "c_crop,g_face,w_100") testGravityUrl(CLDTransformation.CLDGravity.faceCenter, "c_crop,g_face:center,w_100") testGravityUrl(CLDTransformation.CLDGravity.faces, "c_crop,g_faces,w_100") testGravityUrl(CLDTransformation.CLDGravity.facesCenter, "c_crop,g_faces:center,w_100") testGravityUrl(CLDTransformation.CLDGravity.advFace, "c_crop,g_adv_face,w_100") testGravityUrl(CLDTransformation.CLDGravity.advFaces, "c_crop,g_adv_faces,w_100") testGravityUrl(CLDTransformation.CLDGravity.advEyes, "c_crop,g_adv_eyes,w_100") testGravityUrl(CLDTransformation.CLDGravity.north, "c_crop,g_north,w_100") testGravityUrl(CLDTransformation.CLDGravity.northWest, "c_crop,g_north_west,w_100") testGravityUrl(CLDTransformation.CLDGravity.northEast, "c_crop,g_north_east,w_100") testGravityUrl(CLDTransformation.CLDGravity.south, "c_crop,g_south,w_100") testGravityUrl(CLDTransformation.CLDGravity.southWest, "c_crop,g_south_west,w_100") testGravityUrl(CLDTransformation.CLDGravity.southEast, "c_crop,g_south_east,w_100") testGravityUrl(CLDTransformation.CLDGravity.west, "c_crop,g_west,w_100") testGravityUrl(CLDTransformation.CLDGravity.east, "c_crop,g_east,w_100") testGravityUrl(CLDTransformation.CLDGravity.xyCenter, "c_crop,g_xy_center,w_100") testGravityUrl(CLDTransformation.CLDGravity.custom, "c_crop,g_custom,w_100") testGravityUrl(CLDTransformation.CLDGravity.customFace, "c_crop,g_custom:face,w_100") testGravityUrl(CLDTransformation.CLDGravity.customFaces, "c_crop,g_custom:faces,w_100") testGravityUrl(CLDTransformation.CLDGravity.customAdvFace, "c_crop,g_custom:adv_face,w_100") testGravityUrl(CLDTransformation.CLDGravity.customAdvFaces, "c_crop,g_custom:adv_faces,w_100") testGravityUrl(CLDTransformation.CLDGravity.autoOcrText, "c_crop,g_auto:ocr_text,w_100") testGravityUrl(CLDTransformation.CLDGravity.ocrText, "c_crop,g_ocr_text,w_100") testGravityUrl(CLDTransformation.CLDGravity.ocrTextAdvOcr, "c_crop,g_ocr_text:adv_ocr,w_100") } fileprivate func testGravityUrl(_ gravity: CLDTransformation.CLDGravity, _ expected: String) { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setWidth(100).setCrop(CLDTransformation.CLDCrop.crop).setGravity(gravity)).generate("public_id"), "\(prefix)/image/upload/\(expected)/public_id","Creating url with gravity enum should return expected result") } func testVideoCodec() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setVideoCodec("auto")).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/vc_auto/video_id") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setVideoCodecAndProfileAndLevel("h264", videoProfile: "basic", level: "3.1")).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/vc_h264:basic:3.1/video_id") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setVideoCodecAndProfileAndLevel("h264", videoProfile: "basic", level: nil)).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/vc_h264:basic/video_id") } func testVideoCodecBFrameTrueOrNil() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setVideoCodecAndProfileAndLevelAndBFrames("h264", videoProfile: "basic", level: "3.1", bframes: true)).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/vc_h264:basic:3.1/video_id") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setVideoCodecAndProfileAndLevelAndBFrames("h264", videoProfile: "basic", level: "3.1", bframes: nil)).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/vc_h264:basic:3.1/video_id") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setVideoCodecAndProfileAndLevelAndBFrames("h264", videoProfile: "basic", level: "3.1")).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/vc_h264:basic:3.1/video_id") } func testVideoCodecBFrameFalse() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setVideoCodecAndProfileAndLevelAndBFrames("h264", videoProfile: "basic", level: "3.1", bframes: false)).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/vc_h264:basic:3.1:bframes_no/video_id") } func testAudioCodec() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setAudioCodec("acc")).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/ac_acc/video_id") } func testBitRate() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setBitRate("1m")).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/br_1m/video_id") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setBitRate(2048)).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/br_2048/video_id") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setBitRate(kb: 44)).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/br_44k/video_id") } func testAudioFrequency() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setAudioFrequency("44100")).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/af_44100/video_id") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setAudioFrequency(44100)).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/af_44100/video_id") } func testVideoSampling() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setVideoSampling("20")).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/vs_20/video_id") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setVideoSampling(frames: 20)).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/vs_20/video_id") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setVideoSampling(delay: 2.3)).setResourceType(.video).generate("video_id"), "\(prefix)/video/upload/vs_2.3s/video_id") } func testOverlayOptions() { XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDLayer().setPublicId(publicId: "logo"))).generate("test"), "\(prefix)/image/upload/l_logo/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDLayer().setPublicId(publicId: "logo").setType(.private))).generate("test"), "\(prefix)/image/upload/l_private:logo/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDLayer().setPublicId(publicId: "logo").setFormat(format: "png"))).generate("test"), "\(prefix)/image/upload/l_logo.png/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDLayer().setPublicId(publicId: "folder/logo"))).generate("test"), "\(prefix)/image/upload/l_folder:logo/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDLayer().setPublicId(publicId: "cat").setResourceType(.video))).generate("test"), "\(prefix)/image/upload/l_video:cat/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDTextLayer().setText(text: "Hello/World").setFontFamily(fontFamily: "Arial").setFontSize(18))).generate("test"), "\(prefix)/image/upload/l_text:Arial_18:Hello%252FWorld/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDTextLayer().setText(text: "Hello World, Nice to meet you?").setFontFamily(fontFamily: "Arial").setFontSize(18))).generate("test"), "\(prefix)/image/upload/l_text:Arial_18:Hello%20World%252C%20Nice%20to%20meet%20you%3F/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDTextLayer().setText(text: "Hello World, Nice to meet you?").setFontFamily(fontFamily: "Arial").setFontSize(18).setFontStyle(.italic).setFontWeight(.bold).setLetterSpacing(4))).generate("test"), "\(prefix)/image/upload/l_text:Arial_18_bold_italic_letter_spacing_4:Hello%20World%252C%20Nice%20to%20meet%20you%3F/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDSubtitlesLayer().setPublicId(publicId: "sample_sub_en.srt"))).generate("test"), "\(prefix)/image/upload/l_subtitles:sample_sub_en.srt/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDSubtitlesLayer().setFontFamily(fontFamily: "Arial").setFontSize(40).setPublicId(publicId: "sample_sub_he.srt"))).generate("test"), "\(prefix)/image/upload/l_subtitles:Arial_40:sample_sub_he.srt/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDSubtitlesLayer().setFontFamily(fontFamily:"Arial").setFontSize(40).setFontAntialiasing(CLDTextLayer.CLDFontAntialiasing.FAST) .setFontHinting(CLDTextLayer.CLDFontHinting.MEDIUM).setPublicId(publicId: "sample_sub_he.srt"))).generate("test"), "\(prefix)/image/upload/l_subtitles:Arial_40_antialias_fast_hinting_medium:sample_sub_he.srt/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDSubtitlesLayer().setFontFamily(fontFamily:"Arial").setFontSize(40).setFontAntialiasing("fast") .setFontHinting("medium").setPublicId(publicId: "sample_sub_he.srt"))).generate("test"), "\(prefix)/image/upload/l_subtitles:Arial_40_antialias_fast_hinting_medium:sample_sub_he.srt/test") XCTAssertEqual(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDFetchLayer(url: "https://res.cloudinary.com/demo/image/upload/sample"))).generate("test"), "\(prefix)/image/upload/l_fetch:aHR0cHM6Ly9yZXMuY2xvdWRpbmFyeS5jb20vZGVtby9pbWFnZS91cGxvYWQvc2FtcGxl/test") } func testOverlayErrors() { XCTAssertNil(sut?.createUrl().setTransformation(CLDTransformation().setOverlayWithLayer(CLDTextLayer().setText(text: "text").setFontStyle(.italic))).generate("test")) XCTAssertNil(sut?.createUrl().setTransformation(CLDTransformation().setUnderlayWithLayer(CLDLayer().setResourceType(.video))).generate("test")) } // MARK: - fsp func testFps(){ XCTAssertEqual(CLDTransformation().setFps("24-29.97").asString() ,"fps_24-29.97") XCTAssertEqual(CLDTransformation().setFps(24).asString() ,"fps_24") XCTAssertEqual(CLDTransformation().setFps(24.5).asString() ,"fps_24.5") XCTAssertEqual(CLDTransformation().setFps("24").asString() ,"fps_24") XCTAssertEqual(CLDTransformation().setFps("-24").asString() ,"fps_-24") XCTAssertEqual(CLDTransformation().setFps(.range (start: 24, end: 29.97)).asString() ,"fps_24-29.97") XCTAssertEqual(CLDTransformation().setFps(.range (start: "24", end: "29.97")).asString() ,"fps_24-29.97") XCTAssertEqual(CLDTransformation().setFps(.range (start: 24)).asString() ,"fps_24-") XCTAssertEqual(CLDTransformation().setFps(.range (end: 29.97)).asString() ,"fps_-29.97") XCTAssertEqual(CLDTransformation().setFps(.range (start: "24")).asString() ,"fps_24-") XCTAssertEqual(CLDTransformation().setFps(.range (end: "29.97")).asString() ,"fps_-29.97") } func testEagerWithStreamingProfile(){ XCTAssertEqual(CLDEagerTransformation().setFormat("m3u8").setStreamingProfile("full_hd").asString(), "sp_full_hd/m3u8") } func testInitialHeightWidth() { XCTAssertEqual(CLDTransformation().setWidth("iw").setHeight("ih").setCrop(.crop).asString() ,"c_crop,h_ih,w_iw") } func testOffset(){ XCTAssertEqual(CLDTransformation().setStartOffset("auto").asString(), "so_auto") } // MARK: - named spaces removal func test_replaceSpaces_named_shouldCreateExpectedUrl() { // Given let input = "name" let expectedResult = "https://res.cloudinary.com/test123/image/upload/h_101,t_name,w_100/test" // When let transformation = CLDTransformation().setWidth(100).setHeight(101).setNamed(input) let actualResult = sut?.createUrl().setTransformation(transformation).generate("test") // Then XCTAssertEqual(actualResult, expectedResult, "creating url with named in transformation should return the expected result") } func test_replaceSpaces_namedWithSpaces_shouldReplaceSpaces() { // Given let input = "name with spaces" let expectedResult = "https://res.cloudinary.com/test123/image/upload/h_101,t_name%20with%20spaces,w_100/test" // When let transformation = CLDTransformation().setWidth(100).setHeight(101).setNamed(input) let actualResult = sut?.createUrl().setTransformation(transformation).generate("test") // Then XCTAssertEqual(actualResult, expectedResult, "creating url with named in transformation should return the expected result") } func test_replaceSpaces_namedArray_shouldCreateExpectedUrl() { // Given let input1 = "name1" let input2 = "name2" let expectedResult = "https://res.cloudinary.com/test123/image/upload/h_101,t_name1.name2,w_100/test" // When let transformation = CLDTransformation().setWidth(100).setHeight(101).setNamed([input1, input2]) let actualResult = sut?.createUrl().setTransformation(transformation).generate("test") // Then XCTAssertEqual(actualResult, expectedResult, "creating url with named in transformation should return the expected result") } func test_replaceSpaces_namedArrayWithSpaces_shouldReplaceSpaces() { // Given let input1 = "named with spaces 1" let input2 = "named with spaces 2" let expectedResult = "https://res.cloudinary.com/test123/image/upload/h_101,t_named%20with%20spaces%201.named%20with%20spaces%202,w_100/test" // When let transformation = CLDTransformation().setWidth(100).setHeight(101).setNamed([input1, input2]) let actualResult = sut?.createUrl().setTransformation(transformation).generate("test") // Then XCTAssertEqual(actualResult, expectedResult, "creating url with named in transformation should return the expected result") } } ================================================ FILE: Example/Tests/Info.plist ================================================ cldCloudinaryUrl $(CLOUDINARY_URL) CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 UIApplicationSceneManifest UIApplicationSupportsMultipleScenes UISceneConfigurations UIWindowSceneSessionRoleApplication UISceneConfigurationName Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate UISceneStoryboardFile Splash UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~iphone UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIApplicationSupportsIndirectInputEvents UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft ================================================ FILE: Example/Tests/NetworkTests/AccessibilityUploderTests/UploaderAccessibilityTests.m ================================================ // // UploaderAccessibilityTests.m // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "Cloudinary_Tests-Swift.h" #import "NetworkBaseTestObjc.h" @interface UploaderAccessibilityTests: NetworkBaseTestObjc @end @implementation UploaderAccessibilityTests // MARK: - upload result - (void)test_uploadResult_accessibiltyAnalysisUnset_shouldNotReturnAccessibilityInfo { XCTAssertNotNil(self.cloudinary.config.apiSecret, "Must set api secret for this test"); // Given XCTestExpectation *expectation = [self expectationWithDescription:@"upload should succeed"]; TestResourceType resource = borderCollie; NSURL* file = [self getUrlBy:resource]; __block CLDUploadResult* sut; __block NSError* error; CLDUploadRequestParams* params = [[CLDUploadRequestParams alloc] init]; // When [[[self.cloudinary createUploader] signedUploadWithUrl:file params:params progress:nil completionHandler:nil] response:^(CLDUploadResult* resultRes, NSError* errorRes) { sut = resultRes; error = errorRes; [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:self.timeout handler:nil]; // Then XCTAssertNil(error, "error should be nil"); XCTAssertNotNil(sut, "result should not be nil"); XCTAssertNil(sut.accessibilityAnalysis, "accessibility analysis field in upload result without setAccessibilityAnalysis(true) should be nil"); } - (void)test_uploadResult_accessibiltyAnalysisParsing_shouldParseAsExpected { XCTAssertNotNil(self.cloudinary.config.apiSecret, "must set api secret for this test"); // Given XCTestExpectation *expectation = [self expectationWithDescription:@"upload with accessibility should succeed"]; TestResourceType resource = borderCollie; NSURL* file = [self getUrlBy:resource]; __block CLDUploadResult* sut; __block NSError* error; CLDUploadRequestParams* params = [[CLDUploadRequestParams alloc] init]; [params setAccessibilityAnalysis:YES]; // When [[[self.cloudinary createUploader] signedUploadWithUrl:file params:params progress:nil completionHandler:nil] response:^(CLDUploadResult* resultRes, NSError* errorRes) { sut = resultRes; error = errorRes; [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:self.timeout handler:nil]; // Then XCTAssertNil(error, "error should be nil"); XCTAssertNotNil(sut, "result should not be nil"); XCTAssertNotNil(sut.accessibilityAnalysis, "accessibility analysis field in upload result should not be nil"); // accessability analysis XCTAssertNotNil(sut.accessibilityAnalysis.colorblindAccessibilityAnalysis, "accessibility analysis field in upload result should not be nil"); XCTAssertNotNil(sut.accessibilityAnalysis.colorblindAccessibilityAnalysis.mostIndistinctPair , "accessibility analysis field in upload result should not be nil"); XCTAssertNotNil(sut.accessibilityAnalysis.colorblindAccessibilityAnalysis.mostIndistinctPair[0] , "accessibility analysis field in upload result should not be nil"); } // MARK: - explicit - (void)test_explicitResult_accessibiltyAnalysisParsing_shouldParseAsExpected { XCTAssertNotNil(self.cloudinary.config.apiSecret, "must set api secret for this test"); // Given XCTestExpectation *expectation = [self expectationWithDescription:@"upload should succeed"]; TestResourceType resource = borderCollie; NSURL* file = [self getUrlBy:resource]; __block CLDUploadResult* uploadResult; __block NSError* error; CLDUploadRequestParams* params = [[CLDUploadRequestParams alloc] init]; [params setAccessibilityAnalysis:YES]; // When [[[self.cloudinary createUploader] signedUploadWithUrl:file params:params progress:nil completionHandler:nil] response:^(CLDUploadResult* resultRes, NSError* errorRes) { uploadResult = resultRes; error = errorRes; [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:self.timeout handler:nil]; // Then XCTAssertNil(error, "error should be nil"); XCTAssertNotNil(uploadResult, "result should not be nil"); } - (void) callForExplicitWithAccessibility:(NSString*) publicId { if(publicId == nil) { return; } // Given XCTestExpectation *expectation = [self expectationWithDescription:@"explicit call with accessibility should succeed"]; __block CLDUploadResult* sut; __block NSError* error; CLDExplicitRequestParams* params = [[CLDExplicitRequestParams alloc] init]; [params setAccessibilityAnalysis:YES]; // When [[[self.cloudinary createManagementApi] explicitPublicId:publicId stringType:@"upload" params:params completionHandler:nil] response:^(CLDExplicitResult* resultRes, NSError* errorRes) { sut = resultRes; error = errorRes; [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:self.timeout handler:nil]; // Then XCTAssertNil(error, "error should be nil"); XCTAssertNotNil(sut, "result should not be nil"); XCTAssertNotNil(sut.accessibilityAnalysis, "accessibility analysis field in upload result should not be nil"); // accessability analysis XCTAssertNotNil(sut.accessibilityAnalysis.colorblindAccessibilityAnalysis, "accessibility analysis field in upload result should not be nil"); XCTAssertNotNil(sut.accessibilityAnalysis.colorblindAccessibilityAnalysis.mostIndistinctPair , "accessibility analysis field in upload result should not be nil"); XCTAssertNotNil(sut.accessibilityAnalysis.colorblindAccessibilityAnalysis.mostIndistinctPair[0] , "accessibility analysis field in upload result should not be nil"); } @end ================================================ FILE: Example/Tests/NetworkTests/AccessibilityUploderTests/UploaderAccessibilityTests.swift ================================================ // // UploaderAccessibilityTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest @testable import Cloudinary class UploaderAccessibilityTests: NetworkBaseTest { // MARK: - upload func test_uploadResult_accessibiltyAnalysisUnset_shouldNotReturnAccessibilityInfo() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") // Given let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var sut: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() // When cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in sut = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(error, "error should be nil") XCTAssertNotNil(sut, "result should not be nil") XCTAssertNil(sut?.accessibilityAnalysis, "accessibility analysis field in upload result without setAccessibilityAnalysis(true) should be nil") } func test_uploadResult_accessibiltyAnalysisParsing_shouldParseAsExpected() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") // Given let expectation = self.expectation(description: "Upload with accessibility should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var sut: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setAccessibilityAnalysis(true) // When cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in sut = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(error, "error should be nil") XCTAssertNotNil(sut, "result should not be nil") XCTAssertNotNil(sut?.accessibilityAnalysis, "accessibility analysis field in upload result should not be nil") XCTAssertNotNil(sut?.accessibilityAnalysis?.colorblindAccessibilityScore, "accessibility analysis field in upload result should not be nil") // accessability analysis XCTAssertNotNil(sut?.accessibilityAnalysis?.colorblindAccessibilityAnalysis, "accessibility analysis field in upload result should not be nil") XCTAssertNotNil(sut?.accessibilityAnalysis?.colorblindAccessibilityAnalysis?.distinctColors, "accessibility analysis field in upload result should not be nil") XCTAssertNotNil(sut?.accessibilityAnalysis?.colorblindAccessibilityAnalysis?.distinctEdges, "accessibility analysis field in upload result should not be nil") XCTAssertNotNil(sut?.accessibilityAnalysis?.colorblindAccessibilityAnalysis?.mostIndistinctPair , "accessibility analysis field in upload result should not be nil") XCTAssertNotNil(sut?.accessibilityAnalysis?.colorblindAccessibilityAnalysis?.mostIndistinctPair?[0] , "accessibility analysis field in upload result should not be nil") } // MARK: - explicit func test_explicitResult_accessibiltyAnalysisParsing_shouldParseAsExpected() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setAccessibilityAnalysis(true) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) callForExplicitWithAccessibility(publicId: result?.publicId) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") } func callForExplicitWithAccessibility(publicId: String?) { guard let publicId = publicId else { return } // Given let expectation = self.expectation(description: "Explicit call with accessibility should succeed") var sut: CLDExplicitResult? var error: NSError? // When let params = CLDExplicitRequestParams() params.setAccessibilityAnalysis(true) cloudinary!.createManagementApi().explicit(publicId, type: "upload", params: params).response({ (resultRes, errorRes) in sut = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(error, "error should be nil") XCTAssertNotNil(sut, "result should not be nil") XCTAssertNotNil(sut?.accessibilityAnalysis, "accessibility analysis field in upload result should not be nil") XCTAssertNotNil(sut?.accessibilityAnalysis?.colorblindAccessibilityScore, "accessibility analysis field in upload result should not be nil") // accessability analysis XCTAssertNotNil(sut?.accessibilityAnalysis?.colorblindAccessibilityAnalysis, "accessibility analysis field in upload result should not be nil") XCTAssertNotNil(sut?.accessibilityAnalysis?.colorblindAccessibilityAnalysis?.distinctColors, "accessibility analysis field in upload result should not be nil") XCTAssertNotNil(sut?.accessibilityAnalysis?.colorblindAccessibilityAnalysis?.distinctEdges, "accessibility analysis field in upload result should not be nil") XCTAssertNotNil(sut?.accessibilityAnalysis?.colorblindAccessibilityAnalysis?.mostIndistinctPair , "accessibility analysis field in upload result should not be nil") XCTAssertNotNil(sut?.accessibilityAnalysis?.colorblindAccessibilityAnalysis?.mostIndistinctPair?[0] , "accessibility analysis field in upload result should not be nil") } } ================================================ FILE: Example/Tests/NetworkTests/DownloaderAssetTests.swift ================================================ // // DownloaderAssetTests.swift // // Copyright (c) 2021 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest import Cloudinary @testable import Cloudinary // MARK: - assets class DownloaderAssetTests: NetworkBaseTest { var sut: CLDDownloader! override func setUp() { super.setUp() sut = cloudinary!.createDownloader() } override func tearDown() { CLDDownloadCoordinator.urlCache.removeAllCachedResponses() sut = nil super.tearDown() } static var skipableTests = [ ("test_downloadAsset_pdfImage_shouldDownloadAndCacheAsset", test_downloadAsset_pdfImage_shouldDownloadAndCacheAsset), ] override func shouldSkipTest() -> Bool { if super.shouldSkipTest() { return true } if ProcessInfo.processInfo.arguments.contains("TEST_PDF") { return false } if let privateName = testRun?.test.name { if !DownloaderAssetTests.skipableTests.filter({ return privateName.contains($0.0) }).isEmpty { return true } } return false } // MARK - cache asset func test_downloadAsset_video_shouldDownloadAndCacheAsset() { assetCacheTest(shouldCache: true, testResource: .dog2, resourceType: .video) } func test_downloadAsset_pdfImage_shouldDownloadAndCacheAsset() { assetCacheTest(shouldCache: true, testResource: .pdf, resourceType: .image) } func test_downloadAsset_pdfRaw_shouldDownloadAndCacheAsset() { assetCacheTest(shouldCache: true, testResource: .pdf, resourceType: .raw) } func test_downloadAsset_docx_shouldDownloadAndCacheAsset() { assetCacheTest(shouldCache: true, testResource: .docx, resourceType: .raw) } // MARK - download without cache func test_downloadAsset_image_shouldDownloadWithoutCaching() { // images are excluded from asset cache use "fetchImage" instead of "fetchAsset". assetCacheTest(shouldCache: false,testResource: .borderCollie, resourceType: .image) } func test_downloadAsset_video_shouldDownloadWithoutCaching() { assetCacheTest(shouldCache: false, testResource: .dog2, resourceType: .video) } } extension DownloaderAssetTests { func assetCacheTest(shouldCache: Bool, testResource: TestResourceType, resourceType: CLDUrlResourceType) { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") // Given let resource: TestResourceType = testResource var publicId: String? var expectation = self.expectation(description: "Upload should succeed") let params = CLDUploadRequestParams() params.setResourceType(resourceType) // When uploadFile(resource, params: params).response({ (result, error) in publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: longTimeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "Download should succeed") var response: Data? /// download asset by publicId let url = cloudinary!.createUrl().setResourceType(resourceType).generate(pubId) let request = sut.fetchAsset(url!).responseAsset { (responseData, err) in response = responseData expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertEqual(response, resource.data, "uploaded data should be equal to downloaded data") } } // MARK - test status code extension DownloaderAssetTests { func test_downloadAsset_unacceptableStatusCode_shouldReturnEmptyValueAndError() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") // Given let resource: TestResourceType = .pdf var publicId: String? var expectation = self.expectation(description: "Upload should succeed") let params = CLDUploadRequestParams() params.setResourceType(.auto) // When uploadFile(resource, params: params).response({ (result, error) in publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: longTimeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "Download should succeed") var response: Data? var error : NSError? /// download asset by publicId let url = cloudinary!.createUrl().setResourceType(.auto).generate(pubId) sut.fetchAsset(url!).responseAsset { (responseData, responseError) in response = responseData error = responseError expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) // Then if let response = response { XCTAssert(response.isEmpty, "downloaded response should be empty") } else { XCTFail("downloaded response should be empty but valid") } XCTAssertNotNil(error, "downloaded error should indicate unacceptable status code") XCTAssertEqual (error?.code, CLDError.CloudinaryErrorCode.unacceptableStatusCode.rawValue, "downloaded error status code should be 401") } } ================================================ FILE: Example/Tests/NetworkTests/DownloaderTests.swift ================================================ // // DownloaderTests.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest import Cloudinary @testable import Cloudinary class DownloaderTests: NetworkBaseTest { // MARK: - Tests func test_downloadImage_shouldReturnNetworkErrorCode() { var expectation = self.expectation(description: "Should get 404 error") var error: NSError? let firstMockUrl = "https://demo-res.cloudinary.com/image/upload/c_fill,dpr_3.0,f_heic,g_auto,h_100,q_auto,w_100/v1/some_invalid_url" let secondMockUrl = "https://httpbin.org/status/404" cloudinarySecured.createDownloader().fetchImage(firstMockUrl).responseImage({ (_, errorRes) in error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) verify404ErrorCode(in: error) expectation = self.expectation(description: "Should get 404 error") cloudinarySecured.createDownloader().fetchImage(secondMockUrl).responseImage({ (_, errorRes) in error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) verify404ErrorCode(in: error) } private func verify404ErrorCode(in error: Error?) { XCTAssertNotNil(error, "should get an error") XCTAssertNotNil((error! as NSError).userInfo["statusCode"], "should get a statusCode in user info") let statusCode = (error! as NSError).userInfo["statusCode"] as! Int XCTAssertTrue(statusCode == 404, "Mock error should be 404 in this test") let httpStatusCode = HTTPStatusCode(rawValue: statusCode) XCTAssertNotNil(httpStatusCode, "should get a case") XCTAssertTrue(httpStatusCode?.rawValue == 404) } func test_downloadImage_shouldDownloadImage() { XCTAssertNotNil(cloudinarySecured.config.apiSecret, "Must set api secret for this test") var expectation = self.expectation(description: "Upload should succeed") var publicId: String? uploadFile().response({ (result, error) in XCTAssertNil(error) publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "test_downloadImage_shouldDownloadImage Download should succeed") var response: UIImage? var error: NSError? let url = cloudinarySecured!.createUrl().generate(pubId) cloudinarySecured.createDownloader().fetchImage(url!).responseImage({ (responseImage, errorRes) in response = responseImage error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(response, "response should not be nil") XCTAssertNil(error, "error should be nil") } func test_downloadImageWithCache_shouldCacheAndRemoveImage() { cloudinarySecured.enableUrlCache = true downloadImageWithCache_shouldCacheImage(cloudinaryObject: cloudinarySecured) } func test_downloadImageWithoutCache_shouldCacheImage() { downloadImageWithoutCache_shouldNotCacheImage(cloudinaryObject: cloudinarySecured) } func test_downloadImageWithCache_emptyInit_shouldCacheImage() { // Given let config: CLDConfiguration if let url = Bundle(for: type(of: self)).infoDictionary?["cldCloudinaryUrl"] as? String, url.count > 0 { config = CLDConfiguration(cloudinaryUrl: url)! } else { config = CLDConfiguration.initWithEnvParams() ?? CLDConfiguration(cloudinaryUrl: "cloudinary://a:b@test123")! } // When let tempSut = CLDCloudinary(configuration: config, networkAdapter: nil, downloadAdapter: nil, sessionConfiguration: nil, downloadSessionConfiguration: nil) tempSut.enableUrlCache = true downloadImageWithCache_shouldCacheImage(cloudinaryObject: tempSut) } func test_downloadImageWithoutCache_emptyInit_shouldCacheImage() { // Given let config: CLDConfiguration if let url = Bundle(for: type(of: self)).infoDictionary?["cldCloudinaryUrl"] as? String, url.count > 0 { config = CLDConfiguration(cloudinaryUrl: url)! } else { config = CLDConfiguration.initWithEnvParams() ?? CLDConfiguration(cloudinaryUrl: "cloudinary://a:b@test123")! } // When let tempSut = CLDCloudinary(configuration: config, networkAdapter: nil, downloadAdapter: nil, sessionConfiguration: nil, downloadSessionConfiguration: nil) downloadImageWithoutCache_shouldNotCacheImage(cloudinaryObject: tempSut) } } extension DownloaderTests { // MARK: - cache by cloudinary func downloadImageWithCache_shouldCacheImage(cloudinaryObject: CLDCloudinary) { XCTAssertNotNil(cloudinaryObject.config.apiSecret, "Must set api secret for this test") // When var expectation = self.expectation(description: "Upload should succeed") /// upload file to get publicId var publicId: String? uploadFile().response({ (result, error) in XCTAssertNil(error) publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "Download 1 should succeed") var response: UIImage? /// download image by publicId - first time, no cache yet let url = cloudinaryObject.createUrl().generate(pubId) cloudinaryObject.createDownloader().fetchImage(url!).responseImage({ (responseImage, errorRes) in response = responseImage expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) expectation = self.expectation(description: "Download 2 should succeed") var responseCached: UIImage? /// download image by publicId - should get from cache so responses should be equal cloudinaryObject.createDownloader().fetchImage(url!).responseImage({ (responseImage, errorRes) in responseCached = responseImage expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertEqual(response?.pngData()?.count, responseCached?.pngData()?.count, "Images should be equal because it is the image we cached") expectation = self.expectation(description: "Download 3 should succeed") /// remove from cache and re-download - image should be different // cloudinaryObject.removeFromCache(key: url!) cloudinaryObject.createDownloader().fetchImage(url!).responseImage({ (responseImage, errorRes) in responseCached = responseImage expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotEqual(response, responseCached, "Images should be differet because image was removed from cache") } func downloadImageWithoutCache_shouldNotCacheImage(cloudinaryObject: CLDCloudinary) { XCTAssertNotNil(cloudinaryObject.config.apiSecret, "Must set api secret for this test") // Given var publicId: String? var response: UIImage? var error: NSError? var responseCached: UIImage? cloudinaryObject.cacheMaxMemoryTotalCost = 20 cloudinaryObject.cacheMaxDiskCapacity = 20 // When var expectation = self.expectation(description: "Upload should succeed") /// upload file to get publicId uploadFile().response({ (result, error) in XCTAssertNil(error) publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "downloadImageWithoutCache_shouldNotCacheImage Download 1 should succeed") let url = cloudinaryObject.createUrl().generate(pubId) /// download image that will not get cached due to low capacity cloudinaryObject.createDownloader().fetchImage(url!).responseImage({ (responseImage, errorRes) in response = responseImage error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) expectation = self.expectation(description: "downloadImageWithoutCache_shouldNotCacheImage Download 2 should succeed") /// download again (should not get the image from cache due to low capacity) cloudinaryObject.createDownloader().fetchImage(url!).responseImage({ (responseImage, errorRes) in responseCached = responseImage error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotEqual(response, responseCached, "Images should be not same because the size of the cache is too small") XCTAssertNotNil(response, "response should not be nil") XCTAssertNil(error, "error should be nil") } } ================================================ FILE: Example/Tests/NetworkTests/ManagementApiTests.swift ================================================ // // ManagementApiTests.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest @testable import Cloudinary class ManagementApiTests: NetworkBaseTest { // MARK: - rename func testRename() { let expectation = self.expectation(description: "Rename should succeed") var result: CLDRenameResult? var error: Error? uploadFile().response({ (uploadResult, uploadError) in if let publicId = uploadResult?.publicId { let toRename = publicId + "__APPENDED STRING" self.cloudinary!.createManagementApi().rename(publicId, to: toRename).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "response should not be nil") } func testRenameWithParams() { let expectation = self.expectation(description: "Rename should succeed") var result: CLDRenameResult? var error: Error? uploadFile().response({ (uploadResult, uploadError) in if let publicId = uploadResult?.publicId { let toRename = publicId + "__APPENDED STRING" self.cloudinary!.createManagementApi().rename(publicId, to: toRename, overwrite: true, invalidate: true).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "response should not be nil") } func testRenameWithRenameParams() throws { try XCTSkipUnless(NetworkTestUtils.skipFolderDecouplingTest(), "prevents redundant call to Cloudinary PAID Folder Decoupling service. to allow Folder Decoupling service testing - set to true") let expectation = self.expectation(description: "Rename should succeed") var result: CLDRenameResult? var error: Error? uploadFile().response({ (uploadResult, uploadError) in if let publicId = uploadResult?.publicId { let toRename = publicId + "__APPENDED STRING" let params = CLDRenameRequestParams(fromPublicId: publicId, toPublicId: toRename, notificationUrl: "http://www.test.com", context: true, metadata: true) self.cloudinary!.createManagementApi().rename(publicId, to: toRename, overwrite: true, invalidate: true, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "response should not be nil") XCTAssertNotNil(result?.metadataObject, "metadata should not be nil") } // MARK: - explicit func testExplicit() { let expectation = self.expectation(description: "Explicit should succeed") var result: CLDExplicitResult? var error: Error? var publicId: String = "" var version: String = "" var eager: [CLDEagerResult] = [] let trans = CLDTransformation().setCrop(.scale).setWidth(2.0) let resource = TestResourceType.borderCollie uploadFile(resource).response({ (uploadResult, uploadError) in if let pubId = uploadResult?.publicId { publicId = pubId let params = CLDExplicitRequestParams() params.setEager([trans]) self.cloudinary!.createManagementApi().explicit(publicId, type: .upload, params: params, completionHandler: { (resultRes, errorRes) in result = resultRes error = errorRes version = result?.version ?? "" eager = result?.eager ?? [] expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "response should not be nil") let derivedUrl = eager.first?.secureUrl ?? "" cloudinary?.config.analytics = false if let url = cloudinary!.createUrl().setFormat(resource.resourceExtension).setVersion(version).setTransformation(trans).generate(publicId){ XCTAssertEqual(url, derivedUrl) } else{ XCTFail("url should not be nil") } } func testExplicitAsync(){ let expectation = self.expectation(description: "Explicit should succeed") var result: CLDExplicitResult? var error: Error? let trans = CLDTransformation().setCrop(.scale).setWidth(2.0) let resource = TestResourceType.borderCollie uploadFile(resource).response({ (uploadResult, uploadError) in if let pubId = uploadResult?.publicId { let params = CLDExplicitRequestParams() .setEager([trans]) .setAsync(true) self.cloudinary!.createManagementApi().explicit(pubId, type: .upload, params: params, completionHandler: { (resultRes, errorRes) in result = resultRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "response should not be nil") XCTAssertEqual(result?.resultJson["status"] as? String, "pending") } // MARK: - tags func testTags() { var expectation = self.expectation(description: "Adding a tag should succeed") var result: CLDTagResult? var error: Error? var uploadedPublicId: String = "" // first upload uploadFile().response({ (uploadResult, uploadError) in if let pubId = uploadResult?.publicId { uploadedPublicId = pubId // test adding a tag self.cloudinary!.createManagementApi().addTag("tag1", publicIds: [uploadedPublicId]).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.publicIds?.first ?? "", uploadedPublicId) // Reaplace tag result = nil error = nil expectation = self.expectation(description: "Replacing a tag should succeed") let replacedTag = "replaced_tag" cloudinary!.createManagementApi().replaceTag(replacedTag, publicIds: [uploadedPublicId]) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.publicIds?.first ?? "", uploadedPublicId) // Remove tag result = nil error = nil expectation = self.expectation(description: "Removing a tag should succeed") cloudinary!.createManagementApi().removeTag(replacedTag, publicIds: [uploadedPublicId]) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.publicIds?.first ?? "", uploadedPublicId) } func testTagsAsArray() { let tagsArray = ["tag1","tag2","tag3"] var expectation = self.expectation(description: "Adding tags as an array should succeed") var result: CLDTagResult? var error: Error? var uploadedPublicId: String = "" // first upload uploadFile().response({ (uploadResult, uploadError) in if let pubId = uploadResult?.publicId { uploadedPublicId = pubId // test adding a tags self.cloudinary!.createManagementApi().addTag(tagsArray, publicIds: [uploadedPublicId]).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.publicIds?.first ?? "", uploadedPublicId) // Reaplace tag result = nil error = nil expectation = self.expectation(description: "Replacing tags as an array should succeed") let replacedTag = ["replaced_tag", "replaced_tag2"] cloudinary!.createManagementApi().replaceTag(replacedTag, publicIds: [uploadedPublicId]) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.publicIds?.first ?? "", uploadedPublicId) // Remove tags result = nil error = nil expectation = self.expectation(description: "Removing tags as an array should succeed") cloudinary!.createManagementApi().removeTag(tagsArray, publicIds: [uploadedPublicId]) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.publicIds?.first ?? "", uploadedPublicId) } // MARK: - text func testGenerateText() { let expectation = self.expectation(description: "Generate text should succeed") var result: CLDTextResult? var error: Error? let params = CLDTextRequestParams().setFontStyle(.italic).setFontColor("blue").setTextDecoration(.underline) cloudinary!.createManagementApi().text("Hello World", params: params) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") } // MARK: - sprite func testGenerateSprite() { let uploadParams = CLDUploadRequestParams() let tag = "sprite_test_tag" uploadParams.setTags([tag]) let expectation1 = self.expectation(description: "Upload first image") let expectation2 = self.expectation(description: "Upload second image") uploadFile(params: uploadParams).response({ (r, e) in expectation1.fulfill() } ) uploadFile(params: uploadParams).response({ (r, e) in expectation2.fulfill() } ) waitForExpectations(timeout: longTimeout) let expectation = self.expectation(description: "Generating sprite should succeed") var result: CLDSpriteResult? var error: Error? let width = 120, height = 25 let params = CLDSpriteRequestParams().setTransformation(CLDTransformation().setWidth(width).setHeight(height)) cloudinary!.createManagementApi().generateSprite(tag, params: params) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: longTimeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertGreaterThan(result?.imageInfos?.count ?? 0, 1) guard let imageInfo = result?.imageInfos?.first?.1 else { XCTFail("should have at least one image info.") return } XCTAssertEqual(imageInfo.height, height) XCTAssertEqual(imageInfo.width, width) } // MARK: - multi func testMulti() { let uploadParams = CLDUploadRequestParams() let tag = "multi_test_tag" uploadParams.setTags([tag]) let expectation1 = self.expectation(description: "Upload first image") let expectation2 = self.expectation(description: "Upload second image") uploadFile(params: uploadParams).response({ (r, e) in expectation1.fulfill() } ) uploadFile(params: uploadParams).response({ (r, e) in expectation2.fulfill() } ) waitForExpectations(timeout: longTimeout) let expectation = self.expectation(description: "Generating multi should succeed") var result: CLDMultiResult? var error: Error? let params = CLDMultiRequestParams().setTransformation(CLDTransformation().setWidth(120).setHeight(25)) cloudinary!.createManagementApi().multi(tag, params: params) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: longTimeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") let multiUrl = result?.url XCTAssertNotNil(multiUrl) if let multiUrl = multiUrl { let gifRange = multiUrl.range(of: ".gif") XCTAssertNotNil(gifRange) XCTAssertEqual(multiUrl.distance(from: multiUrl.startIndex, to: gifRange!.lowerBound), multiUrl.count - 4) } } // MARK: - delete by token func testDeleteByToken() { var expectation = self.expectation(description: "Upload should succeed") var deleteToken: String? let uploadParams = CLDUploadRequestParams() uploadParams.setReturnDeleteToken(true) uploadFile(params: uploadParams).response({ (result, error) in deleteToken = result?.deleteToken expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let token = deleteToken else { XCTFail("Delete token should not be nil at this point") return } expectation = self.expectation(description: "Delete by token should succeed") var result: CLDDeleteResult? var error: Error? // test the params let params = CLDDeleteByTokenRequestParams(params: ["token" : token]) cloudinary!.createManagementApi().deleteByToken(token, params: params) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.result ?? "", "ok") } // MARK: - destroy func testDestroy() { var expectation = self.expectation(description: "Upload should succeed") var publicId: String? uploadFile().response({ (result, error) in publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "Destroy should succeed") var result: CLDDeleteResult? var error: Error? let params = CLDDestroyRequestParams().setInvalidate(true) cloudinary!.createManagementApi().destroy(pubId, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.result ?? "", "ok") } func testDestroyWithSignature() { var expectation = self.expectation(description: "Upload should succeed") var publicId: String? uploadFile().response({ (result, error) in publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "Destroy should succeed") var result: CLDDeleteResult? var error: Error? let params = CLDDestroyRequestParams() let timestamp = Int(Date().timeIntervalSince1970) var paramsToSign: [String : Any] = [:] paramsToSign["public_id"] = pubId paramsToSign["timestamp"] = String(describing: timestamp) let signatureStr = cloudinarySignParamsUsingSecret(paramsToSign, cloudinaryApiSecret: (cloudinary?.config.apiSecret!)!, signatureVersion: cloudinary?.config.signatureVesion) let signature = CLDSignature(signature: signatureStr, timestamp: NSNumber(integerLiteral: timestamp)) params.setSignature (signature) cloudinaryNoSecret!.createManagementApi().destroy(pubId, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.result ?? "", "ok") } // MARK: - explode func testExplode() { var expectation = self.expectation(description: "Upload should succeed") var publicId: String? uploadFile(.pdf).response({ (result, error) in publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "Explode should succeed") var result: CLDExplodeResult? var error: Error? let params = CLDExplodeRequestParams().setType(.upload) cloudinary!.createManagementApi().explode(pubId, transformation: CLDTransformation().setWidth(306).setHeight(396).setPage("all"), params: params) { (resultRes: CLDExplodeResult?, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.status ?? "", "processing") } // MARK: - insufficient timeout func test_renameWithTimeout_insufficientTimeOut_requestShouldRespectTimeout() { // Given var result: CLDRenameResult? var error: Error? let expectedResult = "-1001" let expectation = self.expectation(description: "rename should fail") // When uploadFile().response({ (uploadResult, uploadError) in if let publicId = uploadResult?.publicId { let toRename = publicId + "__APPENDED STRING" self.cloudinaryInsufficientTimeout!.createManagementApi().rename(publicId, to: toRename).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) // Then var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } func test_explicitWithTimeout_insufficientTimeOut_callShouldRespectTimeout() { // Given var result: CLDExplicitResult? var error: Error? let expectedResult = "-1001" let expectation = self.expectation(description: "explicit should fail") // When uploadFile().response({ (uploadResult, uploadError) in if let publicId = uploadResult?.publicId { self.cloudinaryInsufficientTimeout!.createManagementApi().explicit(publicId, type: .facebook).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) // Then var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } func test_destroyWithTimeout_insufficientTimeOut_callShouldRespectTimeout() { // Given var result: CLDDeleteResult? var error: Error? let expectedResult = "-1001" let expectation = self.expectation(description: "destroy should fail") // When uploadFile().response({ (uploadResult, uploadError) in if let publicId = uploadResult?.publicId { self.cloudinaryInsufficientTimeout!.createManagementApi().destroy(publicId).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) // Then var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } func test_addTagWithTimeout_insufficientTimeOut_callShouldRespectTimeout() { // Given var result: CLDTagResult? var error: Error? let expectedResult = "-1001" let expectation = self.expectation(description: "addTag should fail") // When uploadFile().response({ (uploadResult, uploadError) in if let publicId = uploadResult?.publicId { self.cloudinaryInsufficientTimeout!.createManagementApi().addTag("tag1", publicIds: [publicId]).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) // Then var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } func test_replaceTagWithTimeout_insufficientTimeOut_callShouldRespectTimeout() { // Given var result: CLDTagResult? var error: Error? let expectedResult = "-1001" let expectation = self.expectation(description: "replaceTag should fail") // When uploadFile().response({ (uploadResult, uploadError) in if let publicId = uploadResult?.publicId { self.cloudinaryInsufficientTimeout!.createManagementApi().replaceTag("tag1", publicIds: [publicId]).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) // Then var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } func test_removeTagWithTimeout_insufficientTimeOut_callShouldRespectTimeout() { // Given var result: CLDTagResult? var error: Error? let expectedResult = "-1001" let expectation = self.expectation(description: "removeTag should fail") // When uploadFile().response({ (uploadResult, uploadError) in if let publicId = uploadResult?.publicId { self.cloudinaryInsufficientTimeout!.createManagementApi().removeTag("tag1", publicIds: [publicId]).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) // Then var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } func test_textWithTimeout_insufficientTimeOut_callShouldRespectTimeout() { // Given var result: CLDTextResult? var error: Error? let expectedResult = "-1001" let expectation = self.expectation(description: "text should fail") // When uploadFile().response({ (uploadResult, uploadError) in if (uploadResult?.publicId) != nil { self.cloudinaryInsufficientTimeout!.createManagementApi().text("tag1").response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) // Then var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } func test_spriteWithTimeout_insufficientTimeOut_callShouldRespectTimeout() { // Given var result: CLDSpriteResult? var error: Error? let expectedResult = "-1001" let expectation = self.expectation(description: "sprite should fail") // When uploadFile().response({ (uploadResult, uploadError) in if (uploadResult?.publicId) != nil { self.cloudinaryInsufficientTimeout!.createManagementApi().generateSprite("tag1").response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) // Then var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } func test_multiWithTimeout_insufficientTimeOut_callShouldRespectTimeout() { // Given var result: CLDMultiResult? var error: Error? let expectedResult = "-1001" let expectation = self.expectation(description: "multi should fail") // When uploadFile().response({ (uploadResult, uploadError) in if (uploadResult?.publicId) != nil { self.cloudinaryInsufficientTimeout!.createManagementApi().multi("tag1").response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) // Then var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } func test_deleteByTokenWithTimeout_insufficientTimeOut_callShouldRespectTimeout() { // Given var result: CLDDeleteResult? var error: Error? let expectedResult = "-1001" let expectation = self.expectation(description: "delete by token should fail") // When uploadFile().response({ (uploadResult, uploadError) in if (uploadResult?.publicId) != nil { self.cloudinaryInsufficientTimeout!.createManagementApi().deleteByToken("tag1").response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) // Then var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } func test_explodeWithTimeout_insufficientTimeOut_callShouldRespectTimeout() { // Given var result: CLDExplodeResult? var error: Error? let expectedResult = "-1001" let expectation = self.expectation(description: "explode should fail") // When uploadFile().response({ (uploadResult, uploadError) in if let publicId = uploadResult?.publicId { self.cloudinaryInsufficientTimeout!.createManagementApi().explode(publicId, transformation: CLDTransformation.init()).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) // Then var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } // MARK: - sufficient timeout func test_renameWithTimeout_sufficientTimeOut_callShouldRespectTimeout() { let expectation = self.expectation(description: "Rename should succeed") var result: CLDRenameResult? var error: Error? uploadFile().response({ (uploadResult, uploadError) in if let publicId = uploadResult?.publicId { let toRename = publicId + "__APPENDED STRING" self.cloudinarySufficientTimeout!.createManagementApi().rename(publicId, to: toRename).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "response should not be nil") } func test_explicitWithTimeout_sufficientTimeOut_callShouldRespectTimeout() { let expectation = self.expectation(description: "Explicit should succeed") var result: CLDExplicitResult? var error: Error? var publicId: String = "" var version: String = "" var eager: [CLDEagerResult] = [] let trans = CLDTransformation().setCrop(.scale).setWidth(2.0) let resource = TestResourceType.borderCollie uploadFile(resource).response({ (uploadResult, uploadError) in if let pubId = uploadResult?.publicId { publicId = pubId let params = CLDExplicitRequestParams() params.setEager([trans]) self.cloudinarySufficientTimeout!.createManagementApi().explicit(publicId, type: .upload, params: params, completionHandler: { (resultRes, errorRes) in result = resultRes error = errorRes version = result?.version ?? "" eager = result?.eager ?? [] expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "response should not be nil") let derivedUrl = eager.first?.secureUrl ?? "" if let url = cloudinarySufficientTimeout!.createUrl().setFormat(resource.resourceExtension).setVersion(version).setTransformation(trans).generate(publicId){ XCTAssertEqual(url, derivedUrl) } else{ XCTFail("url should not be nil") } } func test_tagsWithTimeout_sufficientTimeOut_callShouldRespectTimeout() { var expectation = self.expectation(description: "Adding a tag should succeed") var result: CLDTagResult? var error: Error? var uploadedPublicId: String = "" // first upload uploadFile().response({ (uploadResult, uploadError) in if let pubId = uploadResult?.publicId { uploadedPublicId = pubId // test adding a tag self.cloudinarySufficientTimeout!.createManagementApi().addTag("tag1", publicIds: [uploadedPublicId]).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) } else { error = uploadError expectation.fulfill() } }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.publicIds?.first ?? "", uploadedPublicId) // Reaplace tag result = nil error = nil expectation = self.expectation(description: "Replacing a tag should succeed") let replacedTag = "replaced_tag" cloudinarySufficientTimeout!.createManagementApi().replaceTag(replacedTag, publicIds: [uploadedPublicId]) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.publicIds?.first ?? "", uploadedPublicId) // Remove tag result = nil error = nil expectation = self.expectation(description: "Removing a tag should succeed") cloudinarySufficientTimeout!.createManagementApi().removeTag(replacedTag, publicIds: [uploadedPublicId]) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.publicIds?.first ?? "", uploadedPublicId) } func test_generateTextWithTimeout_sufficientTimeOut_callShouldRespectTimeout() { let expectation = self.expectation(description: "Generate text should succeed") var result: CLDTextResult? var error: Error? let params = CLDTextRequestParams().setFontStyle(.italic).setFontColor("blue").setTextDecoration(.underline) cloudinarySufficientTimeout!.createManagementApi().text("Hello World", params: params) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") } func test_generateSpriteWithTimeout_sufficientTimeOut_callShouldRespectTimeout() { let uploadParams = CLDUploadRequestParams() let tag = "sprite_test_tag" uploadParams.setTags([tag]) let expectation1 = self.expectation(description: "Upload first image") let expectation2 = self.expectation(description: "Upload second image") uploadFile(params: uploadParams).response({ (r, e) in expectation1.fulfill() } ) uploadFile(params: uploadParams).response({ (r, e) in expectation2.fulfill() } ) waitForExpectations(timeout: longTimeout) let expectation = self.expectation(description: "Generating sprite should succeed") var result: CLDSpriteResult? var error: Error? let width = 120, height = 25 let params = CLDSpriteRequestParams().setTransformation(CLDTransformation().setWidth(width).setHeight(height)) cloudinarySufficientTimeout!.createManagementApi().generateSprite(tag, params: params) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: longTimeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertGreaterThan(result?.imageInfos?.count ?? 0, 1) guard let imageInfo = result?.imageInfos?.first?.1 else { XCTFail("should have at least one image info.") return } XCTAssertEqual(imageInfo.height, height) XCTAssertEqual(imageInfo.width, width) } func test_multiWithTimeout_sufficientTimeOut_callShouldRespectTimeout() { let uploadParams = CLDUploadRequestParams() let tag = "multi_test_tag" uploadParams.setTags([tag]) let expectation1 = self.expectation(description: "Upload first image") let expectation2 = self.expectation(description: "Upload second image") uploadFile(params: uploadParams).response({ (r, e) in expectation1.fulfill() } ) uploadFile(params: uploadParams).response({ (r, e) in expectation2.fulfill() } ) waitForExpectations(timeout: longTimeout) let expectation = self.expectation(description: "Generating multi should succeed") var result: CLDMultiResult? var error: Error? let params = CLDMultiRequestParams().setTransformation(CLDTransformation().setWidth(120).setHeight(25)) cloudinarySufficientTimeout!.createManagementApi().multi(tag, params: params) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: longTimeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") let multiUrl = result?.url XCTAssertNotNil(multiUrl) if let multiUrl = multiUrl { let gifRange = multiUrl.range(of: ".gif") XCTAssertNotNil(gifRange) XCTAssertEqual(multiUrl.distance(from: multiUrl.startIndex, to: gifRange!.lowerBound), multiUrl.count - 4) } } func test_deleteByTokenWithTimeout_sufficientTimeOut_callShouldRespectTimeout() { var expectation = self.expectation(description: "Upload should succeed") var deleteToken: String? let uploadParams = CLDUploadRequestParams() uploadParams.setReturnDeleteToken(true) uploadFile(params: uploadParams).response({ (result, error) in deleteToken = result?.deleteToken expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let token = deleteToken else { XCTFail("Delete token should not be nil at this point") return } expectation = self.expectation(description: "Delete by token should succeed") var result: CLDDeleteResult? var error: Error? // test the params let params = CLDDeleteByTokenRequestParams(params: ["token" : token]) cloudinarySufficientTimeout!.createManagementApi().deleteByToken(token, params: params) { (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.result ?? "", "ok") } func test_destroyWithTimeout_sufficientTimeOut_callShouldRespectTimeout() { var expectation = self.expectation(description: "Upload should succeed") var publicId: String? uploadFile().response({ (result, error) in publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "Destroy should succeed") var result: CLDDeleteResult? var error: Error? let params = CLDDestroyRequestParams().setInvalidate(true) cloudinarySufficientTimeout!.createManagementApi().destroy(pubId, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.result ?? "", "ok") } func test_explodeWithTimeout_sufficientTimeOut_callShouldRespectTimeout() { var expectation = self.expectation(description: "Upload should succeed") var publicId: String? uploadFile(.pdf).response({ (result, error) in publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "Explode should succeed") var result: CLDExplodeResult? var error: Error? let params = CLDExplodeRequestParams().setType(.upload) cloudinarySufficientTimeout!.createManagementApi().explode(pubId, transformation: CLDTransformation().setWidth(306).setHeight(396).setPage("all"), params: params) { (resultRes: CLDExplodeResult?, errorRes) in result = resultRes error = errorRes expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertEqual(result?.status ?? "", "processing") } } ================================================ FILE: Example/Tests/NetworkTests/NetworkBaseTests/NetworkBaseTest.swift ================================================ // // NetworkBaseTest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import XCTest import Cloudinary import AVKit class NetworkBaseTest: BaseTestCase { let longTimeout: TimeInterval = 60.0 var cloudinary: CLDCloudinary? var cloudinaryInsufficientTimeout: CLDCloudinary? var cloudinarySufficientTimeout: CLDCloudinary? var cloudinaryNoSecret: CLDCloudinary! var cloudinarySecured:CLDCloudinary! // MARK: - Lifcycle override func setUp() { super.setUp() let config: CLDConfiguration if let url = Bundle(for: type(of: self)).infoDictionary?["cldCloudinaryUrl"] as? String, url.count > 0 { config = CLDConfiguration(cloudinaryUrl: url)! } else { config = CLDConfiguration.initWithEnvParams() ?? CLDConfiguration(cloudinaryUrl: "cloudinary://a:b@test123?analytics=false")! config.analytics = false } let configInsufficientTimeout = CLDConfiguration (cloudName: config.cloudName, apiKey: config.apiKey, apiSecret: config.apiSecret, privateCdn: config.privateCdn, secure: config.secure, cdnSubdomain: config.cdnSubdomain, secureCdnSubdomain: config.secureCdnSubdomain, secureDistribution: config.secureDistribution, cname: config.cname, uploadPrefix: config.uploadPrefix, timeout: 0.01) let configSufficientTimeout = CLDConfiguration (cloudName: config.cloudName, apiKey: config.apiKey, apiSecret: config.apiSecret, privateCdn: config.privateCdn, secure: config.secure, cdnSubdomain: config.cdnSubdomain, secureCdnSubdomain: config.secureCdnSubdomain, secureDistribution: config.secureDistribution, cname: config.cname, uploadPrefix: config.uploadPrefix, timeout: 30000, analytics: false) let configNoSecret = CLDConfiguration (cloudName: config.cloudName, apiKey: config.apiKey, apiSecret: nil, privateCdn: config.privateCdn, secure: config.secure, cdnSubdomain: config.cdnSubdomain, secureCdnSubdomain: config.secureCdnSubdomain, secureDistribution: config.secureDistribution, cname: config.cname, uploadPrefix: config.uploadPrefix) let configSecured = CLDConfiguration (cloudName: config.cloudName, apiKey: config.apiKey, apiSecret: config.apiSecret, privateCdn: config.privateCdn, secure: true, cdnSubdomain: config.cdnSubdomain, secureCdnSubdomain: config.secureCdnSubdomain, secureDistribution: config.secureDistribution, cname: config.cname, uploadPrefix: config.uploadPrefix) cloudinary = CLDCloudinary(configuration: config, sessionConfiguration: .default) cloudinaryInsufficientTimeout = CLDCloudinary(configuration: configInsufficientTimeout, sessionConfiguration: .default) cloudinarySufficientTimeout = CLDCloudinary(configuration: configSufficientTimeout, sessionConfiguration: .default) cloudinaryNoSecret = CLDCloudinary(configuration: configNoSecret, sessionConfiguration: .default) cloudinarySecured = CLDCloudinary(configuration: configSecured) } override func tearDown() { super.tearDown() cloudinary = nil cloudinaryInsufficientTimeout = nil cloudinarySufficientTimeout = nil cloudinaryNoSecret = nil cloudinarySecured = nil } // MARK: - Resources enum TestResourceType { case logo case borderCollie case borderCollieCropped case borderCollieRotatedPng case borderCollieRotatedJpg case docx case dog case dog2 case pdf case textImage var fileName: String { switch self { case .borderCollieRotatedPng: if #available(iOS 12.0, *) { return "borderCollieRotatedPng" } else { return "borderCollieRotatedPngUnderIOS12" } case .borderCollieRotatedJpg: if #available(iOS 12.0, *) { return "borderCollieRotatedJpg" } else { return "borderCollieRotatedJpgUnderIOS12" } default: return String(describing: self) } } var resourceExtension: String { switch self { case .logo : fallthrough case .borderCollieRotatedPng : return "png" case .textImage : fallthrough case .borderCollie : fallthrough case .borderCollieCropped : fallthrough case .borderCollieRotatedJpg : return "jpg" case .docx: return "docx" case .dog : fallthrough case .dog2: return "mp4" case .pdf : return "pdf" } } var url: URL { let bundle = Bundle(for: NetworkBaseTest.self) return bundle.url(forResource: fileName, withExtension: resourceExtension)! } var data: Data { let data = try! Data(contentsOf: url, options: .uncached) return data } } // MARK: - Helpers @discardableResult func uploadFile(_ resource: TestResourceType = .borderCollie, params: CLDUploadRequestParams? = nil) -> CLDUploadRequest { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") var params = params params?.setContext(["test1": "test2"]) return cloudinary!.createUploader().signedUpload(data: resource.data, params: params) } func getImage(_ resource: TestResourceType) -> UIImage { if let image = UIImage(contentsOfFile: resource.url.path) { return image } else { return UIImage() } } func getVideo(_ resource: TestResourceType) -> AVPlayerItem { return AVPlayerItem(url: resource.url) } // MARK: - skip addons /** Override this variable to skip addons tests in order to prevent account related failures and to save addons quota. If set - all tests in this class will be skipped, unless an environment variable "CLD_TEST_ADDONS" is set to the addon type OR set to "all". You can set multiple addons separated by comma. If unset - skips nothing. nil by default. */ var testingAddonType: AddonType? { nil } let environmentAddonsKey = "CLD_TEST_ADDONS" override func shouldSkipTest() -> Bool { // only skip if testingAddonType is set if let testingAddonType = testingAddonType { if let testableAddonsList = ProcessInfo.processInfo.environment[environmentAddonsKey] { let addonContainedInEnvironmentList = testableAddonsList.lowercased().contains(testingAddonType.rawValue) let environmentAddonListSetToAll = testableAddonsList.lowercased() == AddonType.all.rawValue return !addonContainedInEnvironmentList && !environmentAddonListSetToAll } // environmentAddonsKey is not set but testingAddonType is set - we should skip this tests. return true } return false } enum AddonType: String { case all = "all" // test all addons case lightroom = "lightroom" // adobe photoshop lightroom (BETA) case facialAttributesDetection = "facialattributesdetection" // advanced facial attributes detection case rekognition = "rekognition" // amazon rekognition AI moderation, amazon rekognition auto tagging, amazon rekognition celebrity detection case aspose = "aspose" // aspose document conversion case bgRemoval = "bgremoval" // cloudinary AI background removal cloudinary AI background removal case objectAwareCropping = "objectawarecropping" // cloudinary object-aware cropping" case google = "google" // google AI video moderation, google AI video transcription, google auto tagging, google automatic video tagging, google translation case imagga = "imagga" // imagga auto tagging, imagga crop and scale case jpegmini = "jpegmini" // JPEGmini image optimization case metaDefender = "metadefender" // metaDefender anti-malware protection case azure = "azure" // microsoft azure video indexer case neuralArtwork = "neuralartwork" // neural artwork style transfer case ocr = "ocr" // OCR text detection and extraction case pixelz = "pixelz" // remove the background case url2png = "url2png" // website screenshots case viesus = "viesus" // automatic image enhancement case webpurify = "webpurify" // webPurify image moderation } } ================================================ FILE: Example/Tests/NetworkTests/NetworkBaseTests/NetworkBaseTestObjc.h ================================================ // // NetworkBaseTestObjc.h // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "ObjcBaseTestCase.h" #import "Cloudinary_Tests-Swift.h" #import typedef enum TestResourceType: NSUInteger { logo, borderCollie, docx, dog, pdf, textImage } TestResourceType; @interface NetworkBaseTestObjc: ObjcBaseTestCase @property (nonatomic, strong, nullable) CLDCloudinary* cloudinary; - (NSString* _Nonnull) getResourceNameBy:(TestResourceType)testResourceType; - (NSURL* _Nonnull) getUrlBy :(TestResourceType)testResourceType; - (NSData* _Nonnull) getDataBy :(TestResourceType)testResourceType; - (UIImage* _Nonnull) getImageBy :(TestResourceType)testResourceType; - (AVPlayerItem* _Nonnull) getVideoBy :(TestResourceType)testResourceType; @end ================================================ FILE: Example/Tests/NetworkTests/NetworkBaseTests/NetworkBaseTestObjc.m ================================================ // // NetworkBaseTestObjc.m // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import #import "Cloudinary_Tests-Swift.h" #import "NetworkBaseTestObjc.h" @implementation NetworkBaseTestObjc // MARK: - setup and teardown - (void)setUp { [super setUp]; self.timeout = 40.0; CLDConfiguration* config; NSString *cloudinaryUrl = [[[NSBundle bundleForClass:[self class]] infoDictionary] objectForKey:@"cldCloudinaryUrl"]; if (cloudinaryUrl.length) { config = [[CLDConfiguration alloc] initWithCloudinaryUrl:cloudinaryUrl]; } else { config = [CLDConfiguration initWithEnvParams]; if (config == nil) { config = [[CLDConfiguration alloc] initWithCloudinaryUrl:@"cloudinary://a:b@test123"]; } } NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; self.cloudinary = [[CLDCloudinary alloc] initWithConfiguration:config networkAdapter:nil sessionConfiguration:sessionConfig]; } - (void)tearDown { [super tearDown]; self.cloudinary = nil; } // MARK: - public methods - (NSURL* _Nonnull)getUrlBy:(TestResourceType)testResourceType { NSBundle *bundle = [NSBundle bundleForClass:[self class]]; return [bundle URLForResource:[self getResourceNameBy:testResourceType] withExtension:[self getResourceExtensionBy:testResourceType]]; } - (NSData* _Nonnull)getDataBy:(TestResourceType)testResourceType { return [NSData dataWithContentsOfURL:[self getUrlBy:testResourceType] options:NSUncachedRead error:nil]; } - (CLDUploadRequest*)uploadFileWithResource:(TestResourceType)testResourceType params:(CLDUploadRequestParams*)params { XCTAssertNotNil(self.cloudinary.config.apiSecret, "Must set api secret for this test"); return [[self.cloudinary createUploader] signedUploadWithData:[self getDataBy:testResourceType] params:params progress:nil completionHandler:nil]; } -(UIImage*)getImageBy:(TestResourceType)testResourceType { NSBundle* bundle = [NSBundle bundleForClass:[self class]]; NSURL* url = [bundle URLForResource:[self getResourceNameBy:testResourceType] withExtension:[self getResourceExtensionBy:testResourceType]]; return [UIImage imageWithContentsOfFile:url.path]; } -(AVPlayerItem*)getVideoBy:(TestResourceType)testResourceType { NSBundle* bundle = [NSBundle bundleForClass:[self class]]; NSURL* url = [bundle URLForResource:[self getResourceNameBy:testResourceType] withExtension:[self getResourceExtensionBy:testResourceType]]; return [AVPlayerItem playerItemWithURL:url]; } // Mark: - private methods - (NSString*)getResourceExtensionBy:(TestResourceType)testResourceType { switch (testResourceType) { case logo: return @"png"; break; case borderCollie: case textImage: return @"jpg"; break; case docx: return @"docx"; break; case dog: return @"mp4"; break; case pdf: return @"pdf"; break; } } - (NSString* _Nonnull)getResourceNameBy:(TestResourceType)testResourceType { switch (testResourceType) { case logo: return @"logo"; break; case borderCollie: return @"borderCollie"; break; case textImage: return @"textImage"; break; case docx: return @"docx"; break; case dog: return @"dog"; break; case pdf: return @"pdf"; break; } } @end ================================================ FILE: Example/Tests/NetworkTests/NetworkTestUtils.swift ================================================ // // NetworkTestUtils.swift // Cloudinary_Tests // // Created by Adi Mizrahi on 29/01/2025. // Copyright © 2025 CocoaPods. All rights reserved. // import Foundation class NetworkTestUtils { private static let environmentFolderDecoupling = "CLD_FOLDER_DECOUPLING" static func skipFolderDecouplingTest() -> Bool { guard let _ = ProcessInfo.processInfo.environment[environmentFolderDecoupling] else { return false } return true } } ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/MockProvider/BaseMockProvider.swift ================================================ // // BaseMockProvider.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // @testable import Cloudinary @objcMembers public class BaseMockProvider: NSObject { // MARK: - upload result static public var uploadResult: CLDUploadResult? { guard let resultDictionary = convertToDictionary(string: jsonString()) else { return nil } return CLDUploadResult(json: resultDictionary) } // MARK: - explicit result static public var explicitResult: CLDExplicitResult? { guard let resultDictionary = convertToDictionary(string: jsonString()) else { return nil } return CLDExplicitResult(json: resultDictionary) } fileprivate static func convertToDictionary(string: String) -> [String: AnyObject]? { if let data = string.data(using: .utf8) { return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: AnyObject] } return nil } /** Override this function with mock information as json string */ class func jsonString() -> String { return "" } } ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/OcrUploaderTests/ExplicitMockOcrTests.m ================================================ // // ObjcExplicitMockOcrTests.m // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "Cloudinary_Tests-Swift.h" #import "ObjcBaseTestCase.h" @interface ObjcExplicitMockOcrTests : ObjcBaseTestCase @property (nonatomic, strong, nullable) CLDExplicitResult* sut; @end @implementation ObjcExplicitMockOcrTests // MARK: - setup and teardown - (void)setUp { [super setUp]; self.sut = OcrMockProvider.explicitResult; } - (void)tearDown { [super tearDown]; self.sut = nil; } // MARK: - explicit result - (void)test_explicitResult_ocrParsing_ShouldParseAsExpected { //Given NSString* expectedStatus = @"complete"; NSString* expectedLocale = @"en"; NSString* expectedTextDescription = @"OCR test image\nSOME FONT\nAnother font\nOne more\nlast one\n"; NSString* expectedFullTextAnnotationText = @"OCR test image\nSOME FONT\nAnother font\nOne more\nlast one\n"; NSString* expectedLanguageCode = @"en"; NSString* expectedBlockType = @"TEXT"; NSString* expectedSymbolText = @"O"; // Then XCTAssertNotNil(self.sut.info.ocr, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.status, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.status, expectedStatus, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0], "mock properties should not be nil"); // text annotations XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].locale, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].locale, expectedLocale, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].textDescription, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].textDescription, expectedTextDescription, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].boundingBlock, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].boundingBlock.vertices, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].boundingBlock.vertices[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.text, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.text, expectedFullTextAnnotationText, "value should be equal to expected value"); // pages XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].property, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].property, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].property.detectedLanguages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].property.detectedLanguages[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].property.detectedLanguages[0].languageCode, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].property.detectedLanguages[0].languageCode, expectedLanguageCode, "value should be equal to expected value"); // blocks XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].blockType, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].blockType, expectedBlockType, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].property, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].property.detectedLanguages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].property.detectedLanguages[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].property.detectedLanguages[0].languageCode, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].property.detectedLanguages[0].languageCode, expectedLanguageCode, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].boundingBox, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].boundingBox.vertices, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].boundingBox.vertices[0], "mock properties should not be nil"); // paragraph XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].property, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].property.detectedLanguages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].property.detectedLanguages[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].property.detectedLanguages[0].languageCode, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].property.detectedLanguages[0].languageCode, expectedLanguageCode, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].boundingBox, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].boundingBox.vertices, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].boundingBox.vertices[0], "mock properties should not be nil"); // words XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].property, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].property.detectedLanguages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].property.detectedLanguages[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].property.detectedLanguages[0].languageCode, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].property.detectedLanguages[0].languageCode, expectedLanguageCode, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].boundingBox, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].boundingBox.vertices, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].boundingBox.vertices[0], "mock properties should not be nil"); // symbols XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].text, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].text, expectedSymbolText , "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].property, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].property.detectedLanguages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].property.detectedLanguages[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].property.detectedLanguages[0].languageCode, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].property.detectedLanguages[0].languageCode, expectedLanguageCode, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].boundingBox, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].boundingBox.vertices, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].boundingBox.vertices[0], "mock properties should not be nil"); } @end ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/OcrUploaderTests/ExplicitMockOcrTests.swift ================================================ // // ExplicitMockOcrTests.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest @testable import Cloudinary // explicitMockOcrTests created to reduce server calls to Cloudinary PAID OCR service class ExplicitMockOcrTests: NetworkBaseTest { var sut : CLDExplicitResult! // MARK: - setup and teardown override func setUp() { super.setUp() sut = OcrMockProvider.explicitResult } override func tearDown() { super.tearDown() sut = nil } // MARK: - explicit result func test_explicitResult_ocrParsing_ShouldParseAsExpected() { //Given let expectedStatus = "complete" let expectedLocale = "en" let expectedTextDescription = "OCR test image\nSOME FONT\nAnother font\nOne more\nlast one\n" let expectedVerticeX = CGFloat(89) let expectedVerticeY = CGFloat(87) let expectedFullTextAnnotationText = "OCR test image\nSOME FONT\nAnother font\nOne more\nlast one\n" let expectedPagesWidth = 1144 let expectedPagesHeight = 1048 let expectedLanguageCode = "en" let expectedConfidence = 1 let expectedBlockType = "TEXT" let expectedBoundingBoxVerticeX = CGFloat(241) let expectedBoundingBoxVerticeY = CGFloat(84) let expectedParagraphBoundingBoxVerticeX = CGFloat(241) let expectedParagraphBoundingBoxVerticeY = CGFloat(84) let expectedWordsBoundingBoxVerticeX = CGFloat(241) let expectedWordsBoundingBoxVerticeY = CGFloat(87) let expectedSymbolText = "O" let expectedSymbolsBoundingBoxVerticeX = CGFloat(241) let expectedSymbolsBoundingBoxVerticeY = CGFloat(88) // Then XCTAssertNotNil(sut.info?.ocr, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.status, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.status, expectedStatus, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0], "mock properties should not be nil") // text annotations XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations!, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].locale, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].locale, expectedLocale, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].textDescription, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].textDescription, expectedTextDescription, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].boundingBlock, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].boundingBlock?.vertices, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].boundingBlock?.vertices![0], "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].boundingBlock?.vertices![0].x, expectedVerticeX, "value should be equal to expected value") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].boundingBlock?.vertices![0].y, expectedVerticeY, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.text, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.text, expectedFullTextAnnotationText, "value should be equal to expected value") // pages XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].width, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].width, expectedPagesWidth, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].height, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].height, expectedPagesHeight, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property?.detectedLanguages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property?.detectedLanguages![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property?.detectedLanguages![0].languageCode, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property?.detectedLanguages![0].languageCode, expectedLanguageCode, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property?.detectedLanguages![0].confidence, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property?.detectedLanguages![0].confidence, expectedConfidence, "value should be equal to expected value") // blocks XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].blockType, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].blockType, expectedBlockType, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property?.detectedLanguages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property?.detectedLanguages![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property?.detectedLanguages![0].languageCode, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property?.detectedLanguages![0].languageCode, expectedLanguageCode, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property?.detectedLanguages![0].confidence, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property?.detectedLanguages![0].confidence, expectedConfidence, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].boundingBox, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].boundingBox?.vertices, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].boundingBox?.vertices![0], "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].boundingBox?.vertices![0].x, expectedBoundingBoxVerticeX, "value should be equal to expected value") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].boundingBox?.vertices![0].y, expectedBoundingBoxVerticeY, "value should be equal to expected value") // paragraph XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property?.detectedLanguages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property?.detectedLanguages![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property?.detectedLanguages![0].languageCode, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property?.detectedLanguages![0].languageCode, expectedLanguageCode, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property?.detectedLanguages![0].confidence, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property?.detectedLanguages![0].confidence, expectedConfidence, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].boundingBox, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].boundingBox?.vertices, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].boundingBox?.vertices![0], "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].boundingBox?.vertices![0].x, expectedParagraphBoundingBoxVerticeX, "value should be equal to expected value") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].boundingBox?.vertices![0].y, expectedParagraphBoundingBoxVerticeY, "value should be equal to expected value") // words XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].property, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].property?.detectedLanguages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].property?.detectedLanguages![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].property?.detectedLanguages![0].languageCode, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].property?.detectedLanguages![0].languageCode, expectedLanguageCode, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].boundingBox, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].boundingBox?.vertices, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].boundingBox?.vertices![0], "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].boundingBox?.vertices![0].x, expectedWordsBoundingBoxVerticeX, "value should be equal to expected value") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].boundingBox?.vertices![0].y, expectedWordsBoundingBoxVerticeY, "value should be equal to expected value") // symbols XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].text, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].text, expectedSymbolText , "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].property, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].property?.detectedLanguages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].property?.detectedLanguages![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].property?.detectedLanguages![0].languageCode, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].property?.detectedLanguages![0].languageCode, expectedLanguageCode, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].boundingBox, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].boundingBox?.vertices, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].boundingBox?.vertices![0], "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].boundingBox?.vertices![0].x, expectedSymbolsBoundingBoxVerticeX, "value should be equal to expected value") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].boundingBox?.vertices![0].y, expectedSymbolsBoundingBoxVerticeY, "value should be equal to expected value") } } ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/OcrUploaderTests/OcrMockProvider.swift ================================================ // // OcrMockProvider.swift // Cloudinary_Tests // // MockProvider.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // public class OcrMockProvider: BaseMockProvider { // MARK: - json result override class func jsonString() -> String { return "{\"asset_id\":\"a63b0808cb58d34f1bb074ec7e291cf5\",\"public_id\":\"hplf6ct1tqorpelxnezi\",\"version\":1591790130,\"version_id\":\"de952b0068804f79515f2b7ef80e0046\",\"signature\":\"3c840854853511ea97b46d0bd4672713acec2e48\",\"width\":1144,\"height\":1048,\"format\":\"jpg\",\"resource_type\":\"image\",\"created_at\":\"2020-06-10T11:55:30Z\",\"tags\":[],\"bytes\":64893,\"type\":\"upload\",\"etag\":\"aa1a546faf1dad85ef49f6f6a2875602\",\"placeholder\":false,\"url\":\"http://res.cloudinary.com/ginidev/image/upload/v1591790130/hplf6ct1tqorpelxnezi.jpg\",\"secure_url\":\"https://res.cloudinary.com/ginidev/image/upload/v1591790130/hplf6ct1tqorpelxnezi.jpg\",\"access_mode\":\"public\",\"info\":{\"ocr\":{\"adv_ocr\":{\"status\":\"complete\",\"data\":[{\"textAnnotations\":[{\"locale\":\"en\",\"description\":\"OCR test image\\nSOME FONT\\nAnother font\\nOne more\\nlast one\\n\",\"boundingPoly\":{\"vertices\":[{\"x\":89,\"y\":87},{\"x\":1075,\"y\":87},{\"x\":1075,\"y\":492},{\"x\":89,\"y\":492}]}},{\"description\":\"OCR\",\"boundingPoly\":{\"vertices\":[{\"x\":241,\"y\":87},{\"x\":362,\"y\":90},{\"x\":361,\"y\":132},{\"x\":240,\"y\":129}]}},{\"description\":\"test\",\"boundingPoly\":{\"vertices\":[{\"x\":379,\"y\":88},{\"x\":468,\"y\":90},{\"x\":467,\"y\":132},{\"x\":378,\"y\":130}]}},{\"description\":\"image\",\"boundingPoly\":{\"vertices\":[{\"x\":487,\"y\":89},{\"x\":634,\"y\":92},{\"x\":633,\"y\":141},{\"x\":486,\"y\":138}]}},{\"description\":\"SOME\",\"boundingPoly\":{\"vertices\":[{\"x\":92,\"y\":248},{\"x\":201,\"y\":249},{\"x\":200,\"y\":312},{\"x\":91,\"y\":311}]}},{\"description\":\"FONT\",\"boundingPoly\":{\"vertices\":[{\"x\":224,\"y\":255},{\"x\":325,\"y\":256},{\"x\":324,\"y\":312},{\"x\":223,\"y\":311}]}},{\"description\":\"Another\",\"boundingPoly\":{\"vertices\":[{\"x\":508,\"y\":260},{\"x\":837,\"y\":260},{\"x\":837,\"y\":311},{\"x\":508,\"y\":311}]}},{\"description\":\"font\",\"boundingPoly\":{\"vertices\":[{\"x\":900,\"y\":260},{\"x\":1075,\"y\":260},{\"x\":1075,\"y\":311},{\"x\":900,\"y\":311}]}},{\"description\":\"One\",\"boundingPoly\":{\"vertices\":[{\"x\":90,\"y\":446},{\"x\":169,\"y\":448},{\"x\":168,\"y\":481},{\"x\":89,\"y\":479}]}},{\"description\":\"more\",\"boundingPoly\":{\"vertices\":[{\"x\":185,\"y\":455},{\"x\":284,\"y\":458},{\"x\":283,\"y\":484},{\"x\":184,\"y\":481}]}},{\"description\":\"last\",\"boundingPoly\":{\"vertices\":[{\"x\":516,\"y\":442},{\"x\":602,\"y\":441},{\"x\":602,\"y\":491},{\"x\":516,\"y\":492}]}},{\"description\":\"one\",\"boundingPoly\":{\"vertices\":[{\"x\":614,\"y\":451},{\"x\":688,\"y\":451},{\"x\":688,\"y\":479},{\"x\":614,\"y\":479}]}}],\"fullTextAnnotation\":{\"pages\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\",\"confidence\":1}]},\"width\":1144,\"height\":1048,\"blocks\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\",\"confidence\":1}]},\"boundingBox\":{\"vertices\":[{\"x\":241,\"y\":84},{\"x\":634,\"y\":92},{\"x\":633,\"y\":141},{\"x\":240,\"y\":133}]},\"paragraphs\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\",\"confidence\":1}]},\"boundingBox\":{\"vertices\":[{\"x\":241,\"y\":84},{\"x\":634,\"y\":92},{\"x\":633,\"y\":141},{\"x\":240,\"y\":133}]},\"words\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":241,\"y\":87},{\"x\":362,\"y\":90},{\"x\":361,\"y\":132},{\"x\":240,\"y\":129}]},\"symbols\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":241,\"y\":88},{\"x\":279,\"y\":89},{\"x\":278,\"y\":130},{\"x\":240,\"y\":129}]},\"text\":\"O\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":285,\"y\":88},{\"x\":320,\"y\":89},{\"x\":319,\"y\":130},{\"x\":284,\"y\":129}]},\"text\":\"C\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}],\"detectedBreak\":{\"type\":\"SPACE\"}},\"boundingBox\":{\"vertices\":[{\"x\":326,\"y\":89},{\"x\":362,\"y\":90},{\"x\":361,\"y\":130},{\"x\":325,\"y\":129}]},\"text\":\"R\"}]},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":379,\"y\":88},{\"x\":468,\"y\":90},{\"x\":467,\"y\":132},{\"x\":378,\"y\":130}]},\"symbols\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":379,\"y\":90},{\"x\":394,\"y\":90},{\"x\":393,\"y\":129},{\"x\":378,\"y\":129}]},\"text\":\"t\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":396,\"y\":100},{\"x\":423,\"y\":101},{\"x\":422,\"y\":131},{\"x\":395,\"y\":130}]},\"text\":\"e\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":427,\"y\":99},{\"x\":451,\"y\":100},{\"x\":450,\"y\":130},{\"x\":426,\"y\":129}]},\"text\":\"s\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}],\"detectedBreak\":{\"type\":\"SPACE\"}},\"boundingBox\":{\"vertices\":[{\"x\":454,\"y\":90},{\"x\":468,\"y\":90},{\"x\":467,\"y\":130},{\"x\":453,\"y\":130}]},\"text\":\"t\"}]},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":487,\"y\":89},{\"x\":634,\"y\":92},{\"x\":633,\"y\":141},{\"x\":486,\"y\":138}]},\"symbols\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":487,\"y\":89},{\"x\":493,\"y\":89},{\"x\":492,\"y\":128},{\"x\":486,\"y\":128}]},\"text\":\"i\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":500,\"y\":99},{\"x\":540,\"y\":100},{\"x\":539,\"y\":130},{\"x\":499,\"y\":129}]},\"text\":\"m\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":545,\"y\":100},{\"x\":572,\"y\":101},{\"x\":571,\"y\":131},{\"x\":544,\"y\":130}]},\"text\":\"a\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":576,\"y\":99},{\"x\":602,\"y\":100},{\"x\":601,\"y\":141},{\"x\":575,\"y\":140}]},\"text\":\"g\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}],\"detectedBreak\":{\"type\":\"LINE_BREAK\"}},\"boundingBox\":{\"vertices\":[{\"x\":608,\"y\":99},{\"x\":634,\"y\":100},{\"x\":633,\"y\":130},{\"x\":607,\"y\":129}]},\"text\":\"e\"}]}]}],\"blockType\":\"TEXT\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\",\"confidence\":1}]},\"boundingBox\":{\"vertices\":[{\"x\":92,\"y\":248},{\"x\":325,\"y\":251},{\"x\":324,\"y\":314},{\"x\":91,\"y\":311}]},\"paragraphs\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\",\"confidence\":1}]},\"boundingBox\":{\"vertices\":[{\"x\":92,\"y\":248},{\"x\":325,\"y\":251},{\"x\":324,\"y\":314},{\"x\":91,\"y\":311}]},\"words\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":92,\"y\":248},{\"x\":201,\"y\":249},{\"x\":200,\"y\":312},{\"x\":91,\"y\":311}]},\"symbols\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":92,\"y\":248},{\"x\":105,\"y\":248},{\"x\":104,\"y\":311},{\"x\":91,\"y\":311}]},\"text\":\"S\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":116,\"y\":256},{\"x\":135,\"y\":256},{\"x\":134,\"y\":309},{\"x\":115,\"y\":309}]},\"text\":\"O\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":146,\"y\":257},{\"x\":177,\"y\":257},{\"x\":176,\"y\":312},{\"x\":145,\"y\":312}]},\"text\":\"M\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}],\"detectedBreak\":{\"type\":\"SPACE\"}},\"boundingBox\":{\"vertices\":[{\"x\":190,\"y\":257},{\"x\":201,\"y\":257},{\"x\":200,\"y\":310},{\"x\":189,\"y\":310}]},\"text\":\"E\"}]},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":224,\"y\":255},{\"x\":325,\"y\":256},{\"x\":324,\"y\":312},{\"x\":223,\"y\":311}]},\"symbols\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":224,\"y\":256},{\"x\":237,\"y\":256},{\"x\":236,\"y\":309},{\"x\":223,\"y\":309}]},\"text\":\"F\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":244,\"y\":256},{\"x\":263,\"y\":256},{\"x\":262,\"y\":311},{\"x\":243,\"y\":311}]},\"text\":\"O\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":274,\"y\":256},{\"x\":299,\"y\":256},{\"x\":298,\"y\":309},{\"x\":273,\"y\":309}]},\"text\":\"N\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}],\"detectedBreak\":{\"type\":\"LINE_BREAK\"}},\"boundingBox\":{\"vertices\":[{\"x\":306,\"y\":257},{\"x\":325,\"y\":257},{\"x\":324,\"y\":308},{\"x\":305,\"y\":308}]},\"text\":\"T\"}]}]}],\"blockType\":\"TEXT\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\",\"confidence\":1}]},\"boundingBox\":{\"vertices\":[{\"x\":508,\"y\":260},{\"x\":1075,\"y\":260},{\"x\":1075,\"y\":311},{\"x\":508,\"y\":311}]},\"paragraphs\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\",\"confidence\":1}]},\"boundingBox\":{\"vertices\":[{\"x\":508,\"y\":260},{\"x\":1075,\"y\":260},{\"x\":1075,\"y\":311},{\"x\":508,\"y\":311}]},\"words\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":508,\"y\":260},{\"x\":837,\"y\":260},{\"x\":837,\"y\":311},{\"x\":508,\"y\":311}]},\"symbols\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":508,\"y\":264},{\"x\":553,\"y\":264},{\"x\":553,\"y\":309},{\"x\":508,\"y\":309}]},\"text\":\"A\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":558,\"y\":274},{\"x\":597,\"y\":274},{\"x\":597,\"y\":309},{\"x\":558,\"y\":309}]},\"text\":\"n\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":608,\"y\":274},{\"x\":643,\"y\":274},{\"x\":643,\"y\":311},{\"x\":608,\"y\":311}]},\"text\":\"o\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":656,\"y\":264},{\"x\":691,\"y\":264},{\"x\":691,\"y\":311},{\"x\":656,\"y\":311}]},\"text\":\"t\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":702,\"y\":260},{\"x\":741,\"y\":260},{\"x\":741,\"y\":309},{\"x\":702,\"y\":309}]},\"text\":\"h\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":752,\"y\":274},{\"x\":787,\"y\":274},{\"x\":787,\"y\":311},{\"x\":752,\"y\":311}]},\"text\":\"e\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}],\"detectedBreak\":{\"type\":\"SPACE\"}},\"boundingBox\":{\"vertices\":[{\"x\":802,\"y\":274},{\"x\":837,\"y\":274},{\"x\":837,\"y\":309},{\"x\":802,\"y\":309}]},\"text\":\"r\"}]},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":900,\"y\":260},{\"x\":1075,\"y\":260},{\"x\":1075,\"y\":311},{\"x\":900,\"y\":311}]},\"symbols\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":900,\"y\":260},{\"x\":933,\"y\":260},{\"x\":933,\"y\":309},{\"x\":900,\"y\":309}]},\"text\":\"f\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":944,\"y\":274},{\"x\":979,\"y\":274},{\"x\":979,\"y\":311},{\"x\":944,\"y\":311}]},\"text\":\"o\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":990,\"y\":274},{\"x\":1029,\"y\":274},{\"x\":1029,\"y\":309},{\"x\":990,\"y\":309}]},\"text\":\"n\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}],\"detectedBreak\":{\"type\":\"LINE_BREAK\"}},\"boundingBox\":{\"vertices\":[{\"x\":1040,\"y\":264},{\"x\":1075,\"y\":264},{\"x\":1075,\"y\":311},{\"x\":1040,\"y\":311}]},\"text\":\"t\"}]}]}],\"blockType\":\"TEXT\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\",\"confidence\":1}]},\"boundingBox\":{\"vertices\":[{\"x\":90,\"y\":446},{\"x\":284,\"y\":452},{\"x\":283,\"y\":485},{\"x\":89,\"y\":479}]},\"paragraphs\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\",\"confidence\":1}]},\"boundingBox\":{\"vertices\":[{\"x\":90,\"y\":446},{\"x\":284,\"y\":452},{\"x\":283,\"y\":485},{\"x\":89,\"y\":479}]},\"words\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":90,\"y\":446},{\"x\":169,\"y\":448},{\"x\":168,\"y\":481},{\"x\":89,\"y\":479}]},\"symbols\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":90,\"y\":446},{\"x\":120,\"y\":447},{\"x\":119,\"y\":480},{\"x\":89,\"y\":479}]},\"text\":\"O\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":125,\"y\":456},{\"x\":144,\"y\":457},{\"x\":143,\"y\":481},{\"x\":124,\"y\":480}]},\"text\":\"n\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}],\"detectedBreak\":{\"type\":\"SPACE\"}},\"boundingBox\":{\"vertices\":[{\"x\":148,\"y\":457},{\"x\":169,\"y\":458},{\"x\":168,\"y\":481},{\"x\":147,\"y\":480}]},\"text\":\"e\"}]},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":185,\"y\":455},{\"x\":284,\"y\":458},{\"x\":283,\"y\":484},{\"x\":184,\"y\":481}]},\"symbols\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":185,\"y\":456},{\"x\":215,\"y\":457},{\"x\":214,\"y\":482},{\"x\":184,\"y\":481}]},\"text\":\"m\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":219,\"y\":457},{\"x\":237,\"y\":458},{\"x\":236,\"y\":481},{\"x\":218,\"y\":480}]},\"text\":\"o\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":243,\"y\":457},{\"x\":259,\"y\":457},{\"x\":258,\"y\":480},{\"x\":242,\"y\":480}]},\"text\":\"r\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}],\"detectedBreak\":{\"type\":\"LINE_BREAK\"}},\"boundingBox\":{\"vertices\":[{\"x\":263,\"y\":457},{\"x\":284,\"y\":458},{\"x\":283,\"y\":481},{\"x\":262,\"y\":480}]},\"text\":\"e\"}]}]}],\"blockType\":\"TEXT\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\",\"confidence\":1}]},\"boundingBox\":{\"vertices\":[{\"x\":516,\"y\":442},{\"x\":688,\"y\":441},{\"x\":688,\"y\":491},{\"x\":516,\"y\":492}]},\"paragraphs\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\",\"confidence\":1}]},\"boundingBox\":{\"vertices\":[{\"x\":516,\"y\":442},{\"x\":688,\"y\":441},{\"x\":688,\"y\":491},{\"x\":516,\"y\":492}]},\"words\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":516,\"y\":442},{\"x\":602,\"y\":441},{\"x\":602,\"y\":491},{\"x\":516,\"y\":492}]},\"symbols\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":516,\"y\":443},{\"x\":563,\"y\":443},{\"x\":563,\"y\":492},{\"x\":516,\"y\":492}]},\"text\":\"l\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":564,\"y\":443},{\"x\":580,\"y\":443},{\"x\":580,\"y\":492},{\"x\":564,\"y\":492}]},\"text\":\"a\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":581,\"y\":443},{\"x\":596,\"y\":443},{\"x\":596,\"y\":492},{\"x\":581,\"y\":492}]},\"text\":\"s\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}],\"detectedBreak\":{\"type\":\"SPACE\"}},\"boundingBox\":{\"vertices\":[{\"x\":597,\"y\":442},{\"x\":602,\"y\":442},{\"x\":602,\"y\":491},{\"x\":597,\"y\":491}]},\"text\":\"t\"}]},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":614,\"y\":451},{\"x\":688,\"y\":451},{\"x\":688,\"y\":479},{\"x\":614,\"y\":479}]},\"symbols\":[{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":614,\"y\":451},{\"x\":663,\"y\":451},{\"x\":663,\"y\":479},{\"x\":614,\"y\":479}]},\"text\":\"o\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}]},\"boundingBox\":{\"vertices\":[{\"x\":664,\"y\":451},{\"x\":671,\"y\":451},{\"x\":671,\"y\":479},{\"x\":664,\"y\":479}]},\"text\":\"n\"},{\"property\":{\"detectedLanguages\":[{\"languageCode\":\"en\"}],\"detectedBreak\":{\"type\":\"LINE_BREAK\"}},\"boundingBox\":{\"vertices\":[{\"x\":672,\"y\":451},{\"x\":688,\"y\":451},{\"x\":688,\"y\":479},{\"x\":672,\"y\":479}]},\"text\":\"e\"}]}]}],\"blockType\":\"TEXT\"}]}],\"text\":\"OCR test image\\nSOME FONT\\nAnother font\\nOne more\\nlast one\\n\"}}]}}},\"original_filename\":\"textImage\"}" } } ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/OcrUploaderTests/UploaderMockOcrTests.m ================================================ // // ObjcUploaderMockOcrTests.m // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "Cloudinary_Tests-Swift.h" #import "ObjcBaseTestCase.h" @interface ObjcUploaderMockOcrTests : ObjcBaseTestCase @property (nonatomic, strong, nullable) CLDUploadResult* sut; @end @implementation ObjcUploaderMockOcrTests // MARK: - setup and teardown - (void)setUp { [super setUp]; self.sut = OcrMockProvider.uploadResult; } - (void)tearDown { [super tearDown]; self.sut = nil; } // MARK: - upload result - (void)test_uploadResult_ocrParsing_ShouldParseAsExpected { //Given NSString* expectedStatus = @"complete"; NSString* expectedLocale = @"en"; NSString* expectedTextDescription = @"OCR test image\nSOME FONT\nAnother font\nOne more\nlast one\n"; NSString* expectedFullTextAnnotationText = @"OCR test image\nSOME FONT\nAnother font\nOne more\nlast one\n"; NSString* expectedLanguageCode = @"en"; NSString* expectedBlockType = @"TEXT"; NSString* expectedSymbolText = @"O"; // Then XCTAssertNotNil(self.sut.info.ocr, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.status, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.status, expectedStatus, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0], "mock properties should not be nil"); // text annotations XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].locale, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].locale, expectedLocale, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].textDescription, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].textDescription, expectedTextDescription, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].boundingBlock, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].boundingBlock.vertices, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].textAnnotations[0].boundingBlock.vertices[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.text, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.text, expectedFullTextAnnotationText, "value should be equal to expected value"); // pages XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].property, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].property, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].property.detectedLanguages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].property.detectedLanguages[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].property.detectedLanguages[0].languageCode, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].property.detectedLanguages[0].languageCode, expectedLanguageCode, "value should be equal to expected value"); // blocks XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].blockType, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].blockType, expectedBlockType, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].property, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].property.detectedLanguages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].property.detectedLanguages[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].property.detectedLanguages[0].languageCode, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].property.detectedLanguages[0].languageCode, expectedLanguageCode, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].boundingBox, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].boundingBox.vertices, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].boundingBox.vertices[0], "mock properties should not be nil"); // paragraph XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].property, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].property.detectedLanguages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].property.detectedLanguages[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].property.detectedLanguages[0].languageCode, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].property.detectedLanguages[0].languageCode, expectedLanguageCode, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].boundingBox, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].boundingBox.vertices, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].boundingBox.vertices[0], "mock properties should not be nil"); // words XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].property, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].property.detectedLanguages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].property.detectedLanguages[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].property.detectedLanguages[0].languageCode, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].property.detectedLanguages[0].languageCode, expectedLanguageCode, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].boundingBox, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].boundingBox.vertices, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].boundingBox.vertices[0], "mock properties should not be nil"); // symbols XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].text, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].text, expectedSymbolText , "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].property, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].property.detectedLanguages, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].property.detectedLanguages[0], "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].property.detectedLanguages[0].languageCode, "mock properties should not be nil"); XCTAssertEqualObjects(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].property.detectedLanguages[0].languageCode, expectedLanguageCode, "value should be equal to expected value"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].boundingBox, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].boundingBox.vertices, "mock properties should not be nil"); XCTAssertNotNil(self.sut.info.ocr.advOcr.data[0].fullTextAnnotation.pages[0].blocks[0].paragraphs[0].words[0].symbols[0].boundingBox.vertices[0], "mock properties should not be nil"); } @end ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/OcrUploaderTests/UploaderMockOcrTests.swift ================================================ // // UploaderMockOcrTests.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest @testable import Cloudinary // UploaderMockOcrTests created to reduce server calls to Cloudinary PAID OCR service class UploaderMockOcrTests: NetworkBaseTest { var sut : CLDUploadResult! // MARK: - setup and teardown override func setUp() { super.setUp() sut = OcrMockProvider.uploadResult } override func tearDown() { super.tearDown() sut = nil } // MARK: - upload result func test_uploadResult_ocrParsing_ShouldParseAsExpected() { //Given let expectedStatus = "complete" let expectedLocale = "en" let expectedTextDescription = "OCR test image\nSOME FONT\nAnother font\nOne more\nlast one\n" let expectedVerticeX = CGFloat(89) let expectedVerticeY = CGFloat(87) let expectedFullTextAnnotationText = "OCR test image\nSOME FONT\nAnother font\nOne more\nlast one\n" let expectedPagesWidth = 1144 let expectedPagesHeight = 1048 let expectedLanguageCode = "en" let expectedConfidence = 1 let expectedBlockType = "TEXT" let expectedBoundingBoxVerticeX = CGFloat(241) let expectedBoundingBoxVerticeY = CGFloat(84) let expectedParagraphBoundingBoxVerticeX = CGFloat(241) let expectedParagraphBoundingBoxVerticeY = CGFloat(84) let expectedWordsBoundingBoxVerticeX = CGFloat(241) let expectedWordsBoundingBoxVerticeY = CGFloat(87) let expectedSymbolText = "O" let expectedSymbolsBoundingBoxVerticeX = CGFloat(241) let expectedSymbolsBoundingBoxVerticeY = CGFloat(88) // Then XCTAssertNotNil(sut.info?.ocr, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.status, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.status, expectedStatus, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0], "mock properties should not be nil") // text annotations XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations!, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].locale, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].locale, expectedLocale, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].textDescription, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].textDescription, expectedTextDescription, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].boundingBlock, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].boundingBlock?.vertices, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].boundingBlock?.vertices![0], "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].boundingBlock?.vertices![0].x, expectedVerticeX, "value should be equal to expected value") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].textAnnotations![0].boundingBlock?.vertices![0].y, expectedVerticeY, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.text, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.text, expectedFullTextAnnotationText, "value should be equal to expected value") // pages XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].width, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].width, expectedPagesWidth, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].height, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].height, expectedPagesHeight, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property?.detectedLanguages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property?.detectedLanguages![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property?.detectedLanguages![0].languageCode, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property?.detectedLanguages![0].languageCode, expectedLanguageCode, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property?.detectedLanguages![0].confidence, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].property?.detectedLanguages![0].confidence, expectedConfidence, "value should be equal to expected value") // blocks XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].blockType, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].blockType, expectedBlockType, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property?.detectedLanguages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property?.detectedLanguages![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property?.detectedLanguages![0].languageCode, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property?.detectedLanguages![0].languageCode, expectedLanguageCode, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property?.detectedLanguages![0].confidence, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].property?.detectedLanguages![0].confidence, expectedConfidence, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].boundingBox, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].boundingBox?.vertices, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].boundingBox?.vertices![0], "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].boundingBox?.vertices![0].x, expectedBoundingBoxVerticeX, "value should be equal to expected value") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].boundingBox?.vertices![0].y, expectedBoundingBoxVerticeY, "value should be equal to expected value") // paragraph XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property?.detectedLanguages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property?.detectedLanguages![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property?.detectedLanguages![0].languageCode, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property?.detectedLanguages![0].languageCode, expectedLanguageCode, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property?.detectedLanguages![0].confidence, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].property?.detectedLanguages![0].confidence, expectedConfidence, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].boundingBox, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].boundingBox?.vertices, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].boundingBox?.vertices![0], "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].boundingBox?.vertices![0].x, expectedParagraphBoundingBoxVerticeX, "value should be equal to expected value") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].boundingBox?.vertices![0].y, expectedParagraphBoundingBoxVerticeY, "value should be equal to expected value") // words XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].property, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].property?.detectedLanguages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].property?.detectedLanguages![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].property?.detectedLanguages![0].languageCode, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].property?.detectedLanguages![0].languageCode, expectedLanguageCode, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].boundingBox, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].boundingBox?.vertices, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].boundingBox?.vertices![0], "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].boundingBox?.vertices![0].x, expectedWordsBoundingBoxVerticeX, "value should be equal to expected value") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].boundingBox?.vertices![0].y, expectedWordsBoundingBoxVerticeY, "value should be equal to expected value") // symbols XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].text, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].text, expectedSymbolText , "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].property, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].property?.detectedLanguages, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].property?.detectedLanguages![0], "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].property?.detectedLanguages![0].languageCode, "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].property?.detectedLanguages![0].languageCode, expectedLanguageCode, "value should be equal to expected value") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].boundingBox, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].boundingBox?.vertices, "mock properties should not be nil") XCTAssertNotNil(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].boundingBox?.vertices![0], "mock properties should not be nil") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].boundingBox?.vertices![0].x, expectedSymbolsBoundingBoxVerticeX, "value should be equal to expected value") XCTAssertEqual(sut.info?.ocr?.advOcr?.data![0].fullTextAnnotation?.pages![0].blocks![0].paragraphs![0].words![0].symbols![0].boundingBox?.vertices![0].y, expectedSymbolsBoundingBoxVerticeY, "value should be equal to expected value") } } ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/OcrUploaderTests/UploaderOcrTests.swift ================================================ // // UploaderOcrTests.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest @testable import Cloudinary class UploaderOcrTests: NetworkBaseTest { // prevents redundant call to Cloudinary PAID OCR service. to allow OCR service testing - set environment variable "CLD_TEST_ADDONS" = "ocr". override var testingAddonType: AddonType? { .ocr } // MARK: - upload func test_upload_ocr_uploadShouldSucceed() throws { // Given XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .textImage let file = resource.url var result: CLDUploadResult? var error: NSError? // When let params = CLDUploadRequestParams() params.setOcr(true) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result, "result should not be nil") XCTAssertNotNil(result?.info?.ocr, "ocr param should not be nil") } // MARK: - explicit func test_explicit_ocr_callShouldSucceed() throws { // Given XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .textImage let file = resource.url var result: CLDUploadResult? var error: NSError? // When let params = CLDUploadRequestParams() params.setOcr(true) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) self.callForExplicit(publicId: result?.publicId) // Then XCTAssertNil(error, "upload error should be nil") XCTAssertNotNil(result, "upload result should not be nil") } func callForExplicit(publicId: String?) { guard let publicId = publicId else { return } // Given let expectation = self.expectation(description: "Explicit call with ocr should succeed") var result: CLDExplicitResult? var error: NSError? // When let params = CLDExplicitRequestParams() params.setOcr(true) cloudinary!.createManagementApi().explicit(publicId, type: "upload", params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(error, "explicit error should be nil") XCTAssertNotNil(result, "explicit result should not be nil") XCTAssertNotNil(result?.info?.ocr, "ocr param should not be nil") } } ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/PreprocessUploaderTests/PreprocessUploaderTests.swift ================================================ // // PreprocessUploaderTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest @testable import Cloudinary class PreprocessUploaderTests: NetworkBaseTest { // MARK: - crop upload func test_cropPreprocess_upload_croppedImageShouldBeUploaded() { XCTAssertNotNil(cloudinary!.config.apiSecret, "must set api secret for this test") // Given let expectation = self.expectation(description: "upload should succeed") let file = TestResourceType.borderCollie.url let cropWidth = 47 let cropHeight = 43 let xPoint = 200 let yPoint = 100 var result: CLDUploadResult? var error: NSError? let preprocessChain = CLDImagePreprocessChain().addStep(CLDPreprocessHelpers.crop(cropRect: CGRect(x: xPoint, y: yPoint, width: cropWidth, height: cropHeight))) // When cloudinary!.createUploader().signedUpload(url: file, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertEqual(result?.width , cropWidth, "returned image_width should be cropped") XCTAssertEqual(result?.height, cropHeight, "returned image_height should be cropped") } func test_cropPreprocess_uploadWidthOutOfBounds_croppedImageUploadShouldFail() { XCTAssertNotNil(cloudinary!.config.apiSecret, "must set api secret for this test") // Given let expectation = self.expectation(description: "upload should succeed") let file = TestResourceType.borderCollie.url let cropWidth = 1147 // borderCollie image size 960 * 960 let cropHeight = 43 let xPoint = 200 let yPoint = 100 var result: CLDUploadResult? var error: NSError? let preprocessChain = CLDImagePreprocessChain().addStep(CLDPreprocessHelpers.crop(cropRect: CGRect(x: xPoint, y: yPoint, width: cropWidth, height: cropHeight))) // When cloudinary!.createUploader().signedUpload(url: file, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "result should be nil") } func test_cropPreprocess_uploadHeightOutOfBounds_croppedImageUploadShouldFail() { XCTAssertNotNil(cloudinary!.config.apiSecret, "must set api secret for this test") // Given let expectation = self.expectation(description: "upload should succeed") let file = TestResourceType.borderCollie.url let cropWidth = 47 let cropHeight = 1143 // borderCollie image size 960 * 960 let xPoint = 200 let yPoint = 100 var result: CLDUploadResult? var error: NSError? let preprocessChain = CLDImagePreprocessChain().addStep(CLDPreprocessHelpers.crop(cropRect: CGRect(x: xPoint, y: yPoint, width: cropWidth, height: cropHeight))) // When cloudinary!.createUploader().signedUpload(url: file, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "result should be nil") } func test_cropPreprocess_uploadXOutOfBounds_croppedImageUploadShouldFail() { XCTAssertNotNil(cloudinary!.config.apiSecret, "must set api secret for this test") // Given let expectation = self.expectation(description: "upload should succeed") let file = TestResourceType.borderCollie.url let cropWidth = 47 let cropHeight = 43 let xPoint = -1 let yPoint = 100 var result: CLDUploadResult? var error: NSError? let preprocessChain = CLDImagePreprocessChain().addStep(CLDPreprocessHelpers.crop(cropRect: CGRect(x: xPoint, y: yPoint, width: cropWidth, height: cropHeight))) // When cloudinary!.createUploader().signedUpload(url: file, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "result should be nil") } func test_cropPreprocess_uploadYOutOfBounds_croppedImageUploadShouldFail() { XCTAssertNotNil(cloudinary!.config.apiSecret, "must set api secret for this test") // Given let expectation = self.expectation(description: "upload should succeed") let file = TestResourceType.borderCollie.url let cropWidth = 47 let cropHeight = 43 let xPoint = 200 let yPoint = -1 var result: CLDUploadResult? var error: NSError? let preprocessChain = CLDImagePreprocessChain().addStep(CLDPreprocessHelpers.crop(cropRect: CGRect(x: xPoint, y: yPoint, width: cropWidth, height: cropHeight))) // When cloudinary!.createUploader().signedUpload(url: file, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "result should be nil") } // MARK: - crop download func test_uploadPreprocess_cropDownloading_UploadedImageDataShouldBeEqualToCroppedImage() { XCTAssertNotNil(cloudinary!.config.apiSecret, "must set api secret for this test") // Given let expectation = self.expectation(description: "upload should succeed") let file = TestResourceType.borderCollie.url let cropWidth = 252 let cropHeight = 252 let xPoint = 280 let yPoint = 180 var result : CLDUploadResult? var error : NSError? var publicId: String? let preprocessChain = CLDImagePreprocessChain().addStep(CLDPreprocessHelpers.crop(cropRect: CGRect(x: xPoint, y: yPoint, width: cropWidth, height: cropHeight))).setEncoder(CLDPreprocessHelpers.customImageEncoder(format: .JPEG, quality: 90)) // When cloudinary!.createUploader().signedUpload(url: file, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") guard let pubId = publicId else { XCTFail("publicId should not be nil at this point") return } cropDownloading_uploadedImageDataShouldBeEqualToCroppedImage(publicId: pubId, testResourceType: .borderCollieCropped) } func cropDownloading_uploadedImageDataShouldBeEqualToCroppedImage(publicId: String, testResourceType: TestResourceType) { let expectation = self.expectation(description: "download should succeed") // Given let localCroppedImageData = UIImage(data: testResourceType.data)?.pngData() var response: UIImage? var error : NSError? // When let url = cloudinary!.createUrl().generate(publicId) cloudinary!.createDownloader().fetchImage(url!).responseImage({ (responseImage, errorRes) in response = responseImage error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) let downloadedImageData = response?.pngData() // Then XCTAssertNotNil(response, "response should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertEqual(downloadedImageData, localCroppedImageData, "image downloaded after uploaded with crop preprocess, should be equal to expected cropped image") } // MARK: - rotate upload func test_rotatePreprocess_upload45_rotatedImageShouldBeUploaded() { XCTAssertNotNil(cloudinary!.config.apiSecret, "must set api secret for this test") // Given let expectation = self.expectation(description: "upload should succeed") let file = TestResourceType.borderCollie.url let inputDegree: Float = 45 var result: CLDUploadResult? var error: NSError? let expectedSize = CGSize(width: 1357, height: 1357) let preprocessChain = CLDImagePreprocessChain().addStep(CLDPreprocessHelpers.rotate(degrees: inputDegree)) // When cloudinary!.createUploader().signedUpload(url: file, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertEqual(result?.width , Int(expectedSize.width), "returned image_width should be the rotated image's width") XCTAssertEqual(result?.height, Int(expectedSize.height), "returned image_height should be the rotated image's height") } func test_rotatePreprocess_upload90_rotatedImageShouldBeUploaded() { XCTAssertNotNil(cloudinary!.config.apiSecret, "must set api secret for this test") // Given let expectation = self.expectation(description: "upload should succeed") let file = TestResourceType.logo.url let inputDegree: Float = 90 var result: CLDUploadResult? var error: NSError? let expectedSize = CGSize(width: 51, height: 241) let preprocessChain = CLDImagePreprocessChain().addStep(CLDPreprocessHelpers.rotate(degrees: inputDegree)) // When cloudinary!.createUploader().signedUpload(url: file, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertEqual(result?.width , Int(expectedSize.width), "returned image_width should be the rotated image's width") XCTAssertEqual(result?.height, Int(expectedSize.height), "returned image_height should be the rotated image's height") } // MARK: - rotate download func test_rotatePreprocess_rotateDownloadingPng_UploadedImageDataShouldBeEqualToRotatedImage() { XCTAssertNotNil(cloudinary!.config.apiSecret, "must set api secret for this test") // Given let expectation = self.expectation(description: "upload should succeed") let file = TestResourceType.borderCollie.url let inputDegree: Float = 45 var result: CLDUploadResult? var error: NSError? var publicId: String? let preprocessChain = CLDImagePreprocessChain().addStep(CLDPreprocessHelpers.rotate(degrees: inputDegree)) // When cloudinary!.createUploader().signedUpload(url: file, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") guard let pubId = publicId else { XCTFail("publicId should not be nil at this point") return } rotateDownloading_uploadedImageDataShouldBeEqualToRotatedImage(publicId: pubId, testResourceType: .borderCollieRotatedPng) } func test_rotatePreprocess_rotateDownloadingJpg_UploadedImageDataShouldBeEqualToRotatedImage() { XCTAssertNotNil(cloudinary!.config.apiSecret, "must set api secret for this test") // Given let expectation = self.expectation(description: "upload should succeed") let file = TestResourceType.borderCollie.url let inputDegree: Float = 45 var result: CLDUploadResult? var error: NSError? var publicId: String? let preprocessChain = CLDImagePreprocessChain().addStep(CLDPreprocessHelpers.rotate(degrees: inputDegree)).setEncoder(CLDPreprocessHelpers.customImageEncoder(format: .JPEG, quality: 100)) // When cloudinary!.createUploader().signedUpload(url: file, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") guard let pubId = publicId else { XCTFail("publicId should not be nil at this point") return } rotateDownloading_uploadedImageDataShouldBeEqualToRotatedImage(publicId: pubId, testResourceType: .borderCollieRotatedJpg) } func rotateDownloading_uploadedImageDataShouldBeEqualToRotatedImage(publicId: String, testResourceType: TestResourceType) { let expectation = self.expectation(description: "download should succeed") // Given let localRotatedImageData = UIImage(data: testResourceType.data) var response: UIImage? var error : NSError? // When let url = cloudinary!.createUrl().generate(publicId) cloudinary!.createDownloader().fetchImage(url!).responseImage({ (responseImage, errorRes) in response = responseImage error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) let downloadedImageData = response?.pngData() // Then XCTAssertNotNil(response, "response should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertEqual(response?.size, localRotatedImageData?.size, "image downloaded after uploaded with rotate preprocess, should be equal to expected rotated image") } } ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/QualityAnalysisUploaderTests/MockProviderQualityAnalysis.swift ================================================ // // MockProviderQualityAnalysis.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // public class MockProviderQualityAnalysis: BaseMockProvider { // MARK: - json result override class func jsonString() -> String { return "{\"asset_id\":\"c9c2e940e1e5db90269f97af090b57fe\",\"public_id\":\"x7ldagsqc4wyyidbjahc\",\"version\":1602752171,\"version_id\":\"5f0199de7decc33210c6553c30e2ff57\",\"signature\":\"5d828d573c800ad8477b57f049add2b37eeff7cf\",\"width\":1144,\"height\":1048,\"format\":\"jpg\",\"resource_type\":\"image\",\"created_at\":\"2020-10-15T08:56:11Z\",\"tags\":[],\"pages\":1,\"bytes\":64893,\"type\":\"upload\",\"etag\":\"aa1a546faf1dad85ef49f6f6a2875602\",\"placeholder\":false,\"url\":\"http://res.cloudinary.com/ginidev/image/upload/v1602752171/x7ldagsqc4wyyidbjahc.jpg\",\"secure_url\":\"https://res.cloudinary.com/ginidev/image/upload/v1602752171/x7ldagsqc4wyyidbjahc.jpg\",\"access_mode\":\"public\",\"quality_analysis\":{\"jpeg_quality\":0.89,\"jpeg_chroma\":0.25,\"focus\":1.0,\"noise\":1.0,\"contrast\":0.99,\"exposure\":1.0,\"saturation\":1.0,\"lighting\":1.0,\"pixel_score\":0.9,\"color_score\":1.0,\"dct\":0.91,\"blockiness\":1.0,\"chroma_subsampling\":0.0,\"resolution\":0.58},\"quality_score\":0.76,\"original_filename\":\"textImage\"}" } } ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/QualityAnalysisUploaderTests/ObjcQualityAnalysisExplicitResultParserTests.m ================================================ // // QualityAnalysisExplicitResultParserTests.m // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "Cloudinary_Tests-Swift.h" @interface ObjcQualityAnalysisExplicitResultParserTests : XCTestCase @property (nonatomic, strong, nullable) CLDExplicitResult* sut; @end @implementation ObjcQualityAnalysisExplicitResultParserTests // MARK: - setup and teardown - (void)setUp { [super setUp]; self.sut = [MockProviderQualityAnalysis explicitResult]; } - (void)tearDown { [super tearDown]; self.sut = nil; } // MARK: - explicit result - (void)test_explicitResult_qualityAnalysisParsing_ShouldParseAsExpected { // Given NSNumber* expectedBlockiness = @1.0; NSNumber* expectedChromaSubsampling = @0.0; NSNumber* expectedResolution = @0.58; NSNumber* expectedNoise = @1.0; NSNumber* expectedColorScore = @1.0; NSNumber* expectedJpegChroma = @0.25; NSNumber* expectedDct = @0.91; NSNumber* expectedJpegQuality = @0.89; NSNumber* expectedFocus = @1.0; NSNumber* expectedSaturation = @1.0; NSNumber* expectedContrast = @0.99; NSNumber* expectedExposure = @1.0; NSNumber* expectedLighting = @1.0; NSNumber* expectedPixelScore = @0.9; // Then XCTAssertNotNil(self.sut, "explicit self.sut should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.blockiness, expectedBlockiness, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.chromaSubsampling, expectedChromaSubsampling, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.resolution, expectedResolution, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.noise, expectedNoise, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.colorScore, expectedColorScore, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.jpegChroma, expectedJpegChroma, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.dct, expectedDct, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.jpegQuality, expectedJpegQuality, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.focus, expectedFocus, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.saturation, expectedSaturation, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.contrast, expectedContrast, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.exposure, expectedExposure, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.lighting, expectedLighting, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.pixelScore, expectedPixelScore, "value should be equal to expected value"); } @end ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/QualityAnalysisUploaderTests/ObjcQualityAnalysisUploadResultParserTests.m ================================================ // // ObjcQualityAnalysisUploadResultParserTests.m // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "Cloudinary_Tests-Swift.h" @interface ObjcQualityAnalysisUploadResultParserTests : XCTestCase @property (nonatomic, strong, nullable) CLDUploadResult* sut; @end @implementation ObjcQualityAnalysisUploadResultParserTests // MARK: - setup and teardown - (void)setUp { [super setUp]; self.sut = [MockProviderQualityAnalysis uploadResult]; } - (void)tearDown { [super tearDown]; self.sut = nil; } // MARK: - upload result - (void)test_uploadResult_qualityAnalysisParsing_ShouldParseAsExpected { // Given NSNumber* expectedBlockiness = @1.0; NSNumber* expectedChromaSubsampling = @0.0; NSNumber* expectedResolution = @0.58; NSNumber* expectedNoise = @1.0; NSNumber* expectedColorScore = @1.0; NSNumber* expectedJpegChroma = @0.25; NSNumber* expectedDct = @0.91; NSNumber* expectedJpegQuality = @0.89; NSNumber* expectedFocus = @1.0; NSNumber* expectedSaturation = @1.0; NSNumber* expectedContrast = @0.99; NSNumber* expectedExposure = @1.0; NSNumber* expectedLighting = @1.0; NSNumber* expectedPixelScore = @0.9; // Then XCTAssertNotNil(self.sut, "upload self.sut should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.blockiness, expectedBlockiness, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.chromaSubsampling, expectedChromaSubsampling, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.resolution, expectedResolution, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.noise, expectedNoise, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.colorScore, expectedColorScore, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.jpegChroma, expectedJpegChroma, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.dct, expectedDct, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.jpegQuality, expectedJpegQuality, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.focus, expectedFocus, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.saturation, expectedSaturation, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.contrast, expectedContrast, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.exposure, expectedExposure, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.lighting, expectedLighting, "value should be equal to expected value"); XCTAssertEqualObjects(self.sut.qualityAnalysisResult.pixelScore, expectedPixelScore, "value should be equal to expected value"); } @end ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/QualityAnalysisUploaderTests/ObjcUploaderQualityAnalysisTests.m ================================================ // // ObjcUploaderQualityAnalysisTests.m // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "Cloudinary_Tests-Swift.h" #import "NetworkBaseTestObjc.h" @interface ObjcUploaderQualityAnalysisTests: NetworkBaseTestObjc @property (nonatomic, strong, nullable) __block CLDUploadResult* sut; @end @implementation ObjcUploaderQualityAnalysisTests // prevents redundant call to Cloudinary PAID Quality analysis service. to allow Quality analysis service testing - set to true. -(BOOL)allowQualityAnalysisCalls { return [[[NSProcessInfo processInfo] arguments] containsObject:@"TEST_QUALITY_ANALYSIS"]; } // MARK: - upload result - (void)test_upload_qualityAnalysisFalse_uploadShouldSucceedWithoutReturningQualityAnalysis { XCTAssertNotNil(self.cloudinary.config.apiSecret, "Must set api secret for this test"); // Given XCTestExpectation *expectation = [self expectationWithDescription:@"upload should succeed"]; TestResourceType resource = borderCollie; NSURL* file = [self getUrlBy:resource]; __block NSError* error; CLDUploadRequestParams* params = [[CLDUploadRequestParams alloc] init]; [params setQualityAnalysis:NO]; // When [[[self.cloudinary createUploader] signedUploadWithUrl:file params:params progress:nil completionHandler:nil] response:^(CLDUploadResult* resultRes, NSError* errorRes) { self.sut = resultRes; error = errorRes; [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:self.timeout handler:nil]; // Then XCTAssertNil(error, "error should be nil"); XCTAssertNotNil(self.sut, "result should not be nil"); XCTAssertNil (self.sut.qualityAnalysisResult, "quality analysis should be nil"); } - (void)test_upload_qualityAnalysisUnset_uploadShouldSucceedWithoutReturningQualityAnalysis { XCTAssertNotNil(self.cloudinary.config.apiSecret, "Must set api secret for this test"); // Given XCTestExpectation *expectation = [self expectationWithDescription:@"upload should succeed"]; TestResourceType resource = borderCollie; NSURL* file = [self getUrlBy:resource]; __block NSError* error; CLDUploadRequestParams* params = [[CLDUploadRequestParams alloc] init]; // When [[[self.cloudinary createUploader] signedUploadWithUrl:file params:params progress:nil completionHandler:nil] response:^(CLDUploadResult* resultRes, NSError* errorRes) { self.sut = resultRes; error = errorRes; [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:self.timeout handler:nil]; // Then XCTAssertNil(error, "error should be nil"); XCTAssertNotNil(self.sut, "result should not be nil"); XCTAssertNil (self.sut.qualityAnalysisResult, "quality analysis should be nil, defualt value should be false"); } - (void)test_upload_qualityAnalysis_uploadShouldReturnQualityAnalysis { XCTSkipUnless([self allowQualityAnalysisCalls], "prevents redundant call to Cloudinary PAID Quality Analysis service. to allow Quality Analysis service testing - set to true"); XCTAssertNotNil(self.cloudinary.config.apiSecret, "must set api secret for this test"); // Given XCTestExpectation *expectation = [self expectationWithDescription:@"upload with quality should succeed"]; TestResourceType resource = borderCollie; NSURL* file = [self getUrlBy:resource]; __block NSError* error; CLDUploadRequestParams* params = [[CLDUploadRequestParams alloc] init]; [params setQualityAnalysis:YES]; // When [[[self.cloudinary createUploader] signedUploadWithUrl:file params:params progress:nil completionHandler:nil] response:^(CLDUploadResult* resultRes, NSError* errorRes) { self.sut = resultRes; error = errorRes; [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:self.timeout handler:nil]; // Then XCTAssertNil(error, "error should be nil"); XCTAssertNotNil(self.sut, "result should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.blockiness, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.chromaSubsampling, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.resolution, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.noise, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.colorScore, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.jpegChroma, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.dct, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.jpegQuality, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.focus, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.saturation, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.contrast, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.exposure, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.lighting, "qualityAnalysis param should not be nil"); XCTAssertNotNil(self.sut.qualityAnalysisResult.pixelScore, "qualityAnalysis param should not be nil"); } // MARK: - explicit - (void)test_explicit_qualityAnalysis_callShouldReturnQualityAnalysis { XCTSkipUnless([self allowQualityAnalysisCalls], "prevents redundant call to Cloudinary PAID Quality Analysis service. to allow Quality Analysis service testing - set to true"); XCTAssertNotNil(self.cloudinary.config.apiSecret, "must set api secret for this test"); // Given XCTestExpectation *expectation = [self expectationWithDescription:@"upload should succeed"]; TestResourceType resource = borderCollie; NSURL* file = [self getUrlBy:resource]; __block CLDUploadResult* uploadResult; __block NSError* error; CLDUploadRequestParams* params = [[CLDUploadRequestParams alloc] init]; [params setQualityAnalysis:YES]; // When [[[self.cloudinary createUploader] signedUploadWithUrl:file params:params progress:nil completionHandler:nil] response:^(CLDUploadResult* resultRes, NSError* errorRes) { uploadResult = resultRes; error = errorRes; [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:self.timeout handler:nil]; // Then XCTAssertNil(error, "error should be nil"); XCTAssertNotNil(uploadResult, "result should not be nil"); } - (void)callForExplicitWithPublicId:(NSString*) publicId { if(publicId == nil) { return; } // Given XCTestExpectation *expectation = [self expectationWithDescription:@"explicit call with quality should succeed"]; __block CLDExplicitResult* result; __block NSError* error; CLDExplicitRequestParams* params = [[CLDExplicitRequestParams alloc] init]; [params setQualityAnalysis:YES]; // When [[[self.cloudinary createManagementApi] explicitPublicId:publicId stringType:@"upload" params:params completionHandler:nil] response:^(CLDExplicitResult* resultRes, NSError* errorRes) { result = resultRes; error = errorRes; [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:self.timeout handler:nil]; // Then XCTAssertNil(error, "error should be nil"); XCTAssertNotNil(result, "result should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.blockiness, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.chromaSubsampling, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.resolution, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.noise, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.colorScore, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.jpegChroma, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.dct, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.jpegQuality, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.focus, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.saturation, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.contrast, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.exposure, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.lighting, "qualityAnalysis param should not be nil"); XCTAssertNotNil(result.qualityAnalysisResult.pixelScore, "qualityAnalysis param should not be nil"); } @end ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/QualityAnalysisUploaderTests/QualityAnalysisExplicitResultParserTests.swift ================================================ // // QualityAnalysisExplicitResultParserTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest @testable import Cloudinary // QualityAnalysisExplicitResultParserTests created to reduce server calls to Cloudinary PAID Quality analysis service class QualityAnalysisExplicitResultParserTests: NetworkBaseTest { var sut : CLDExplicitResult! // MARK: - setup and teardown override func setUp() { super.setUp() sut = MockProviderQualityAnalysis.explicitResult } override func tearDown() { super.tearDown() sut = nil } // MARK: - explicit result func test_explicitResult_qualityAnalysisParsing_ShouldParseAsExpected() { // Given let expectedBlockiness = NSNumber(value: 1.0) let expectedChromaSubsampling = NSNumber(value: 0.0) let expectedResolution = NSNumber(value: 0.58) let expectedNoise = NSNumber(value: 1.0) let expectedColorScore = NSNumber(value: 1.0) let expectedJpegChroma = NSNumber(value: 0.25) let expectedDct = NSNumber(value: 0.91) let expectedJpegQuality = NSNumber(value: 0.89) let expectedFocus = NSNumber(value: 1.0) let expectedSaturation = NSNumber(value: 1.0) let expectedContrast = NSNumber(value: 0.99) let expectedExposure = NSNumber(value: 1.0) let expectedLighting = NSNumber(value: 1.0) let expectedPixelScore = NSNumber(value: 0.9) // Then XCTAssertNotNil(sut, "explicit sut should not be nil") XCTAssertNotNil(sut.qualityAnalysisResult, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.blockiness, expectedBlockiness, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.chromaSubsampling, expectedChromaSubsampling, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.resolution, expectedResolution, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.noise, expectedNoise, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.colorScore, expectedColorScore, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.jpegChroma, expectedJpegChroma, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.dct, expectedDct, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.jpegQuality, expectedJpegQuality, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.focus, expectedFocus, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.saturation, expectedSaturation, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.contrast, expectedContrast, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.exposure, expectedExposure, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.lighting, expectedLighting, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.pixelScore, expectedPixelScore, "value should be equal to expected value") } } ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/QualityAnalysisUploaderTests/QualityAnalysisUploadResultParserTests.swift ================================================ // // QualityAnalysisUploadResultParserTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest @testable import Cloudinary // QualityAnalysisUploadResultParserTests created to reduce server calls to Cloudinary PAID Quality analysis service class QualityAnalysisUploadResultParserTests: NetworkBaseTest { var sut : CLDUploadResult! // MARK: - setup and teardown override func setUp() { super.setUp() sut = MockProviderQualityAnalysis.uploadResult } override func tearDown() { super.tearDown() sut = nil } // MARK: - upload result func test_uploadResult_qualityAnalysisParsing_ShouldParseAsExpected() { // Given let expectedBlockiness = NSNumber(value: 1.0) let expectedChromaSubsampling = NSNumber(value: 0.0) let expectedResolution = NSNumber(value: 0.58) let expectedNoise = NSNumber(value: 1.0) let expectedColorScore = NSNumber(value: 1.0) let expectedJpegChroma = NSNumber(value: 0.25) let expectedDct = NSNumber(value: 0.91) let expectedJpegQuality = NSNumber(value: 0.89) let expectedFocus = NSNumber(value: 1.0) let expectedSaturation = NSNumber(value: 1.0) let expectedContrast = NSNumber(value: 0.99) let expectedExposure = NSNumber(value: 1.0) let expectedLighting = NSNumber(value: 1.0) let expectedPixelScore = NSNumber(value: 0.9) // Then XCTAssertNotNil(sut, "upload sut should not be nil") XCTAssertNotNil(sut.qualityAnalysisResult, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.blockiness, expectedBlockiness, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.chromaSubsampling, expectedChromaSubsampling, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.resolution, expectedResolution, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.noise, expectedNoise, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.colorScore, expectedColorScore, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.jpegChroma, expectedJpegChroma, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.dct, expectedDct, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.jpegQuality, expectedJpegQuality, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.focus, expectedFocus, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.saturation, expectedSaturation, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.contrast, expectedContrast, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.exposure, expectedExposure, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.lighting, expectedLighting, "value should be equal to expected value") XCTAssertEqual(sut.qualityAnalysisResult?.pixelScore, expectedPixelScore, "value should be equal to expected value") } } ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/QualityAnalysisUploaderTests/UploaderQualityAnalysisTests.swift ================================================ // // UploaderQualityAnalysisTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest @testable import Cloudinary class UploaderQualityAnalysisTests: NetworkBaseTest { var sut: CLDUploadResult? // prevents redundant call to Cloudinary PAID Quality analysis service. to allow Quality analysis service testing - set to true. var allowQualityAnalysisCalls: Bool { return ProcessInfo.processInfo.arguments.contains("TEST_QUALITY_ANALYSIS") } // MARK: - upload func test_upload_qualityAnalysisFalse_uploadShouldSucceedWithoutReturningQualityAnalysis() { // Given XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .textImage let file = resource.url var error: NSError? // When let params = CLDUploadRequestParams() params.setQualityAnalysis(false) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in self.sut = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(error, "error should be nil") XCTAssertNotNil(sut, "result should not be nil") XCTAssertNil (sut?.qualityAnalysisResult, "qualityAnalysis param should be nil") } func test_upload_qualityAnalysisUnset_uploadShouldSucceedWithoutReturningQualityAnalysis() { // Given XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .textImage let file = resource.url var error: NSError? // When let params = CLDUploadRequestParams() cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in self.sut = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(error, "error should be nil") XCTAssertNotNil(sut, "result should not be nil") XCTAssertNil (sut?.qualityAnalysisResult, "qualityAnalysis param should be nil, defualt value should be false") } func test_upload_qualityAnalysis_uploadShouldReturnQualityAnalysis() throws { try XCTSkipUnless(allowQualityAnalysisCalls, "prevents redundant call to Cloudinary PAID Quality Analysis service. to allow Quality Analysis service testing - set to true") // Given XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .textImage let file = resource.url var error: NSError? // When let params = CLDUploadRequestParams() params.setQualityAnalysis(true) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in self.sut = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(error, "error should be nil") XCTAssertNotNil(sut, "result should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.blockiness, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.chromaSubsampling, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.resolution, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.noise, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.colorScore, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.jpegChroma, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.dct, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.jpegQuality, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.focus, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.saturation, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.contrast, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.exposure, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.lighting, "qualityAnalysis param should not be nil") XCTAssertNotNil(sut?.qualityAnalysisResult?.pixelScore, "qualityAnalysis param should not be nil") } // MARK: - explicit func test_explicit_qualityAnalysis_callShouldReturnQualityAnalysis() throws { try XCTSkipUnless(allowQualityAnalysisCalls, "prevents redundant call to Cloudinary PAID Quality Analysis service. to allow Quality Analysis service testing - set to true") // Given XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .textImage let file = resource.url var error: NSError? // When let params = CLDUploadRequestParams() params.setQualityAnalysis(true) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in self.sut = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) self.callForExplicit(publicId: sut?.publicId) // Then XCTAssertNil(error, "upload error should be nil") XCTAssertNotNil(sut, "upload result should not be nil") } func callForExplicit(publicId: String?) { guard let publicId = publicId else { return } // Given let expectation = self.expectation(description: "Explicit call with qualityAnalysis should succeed") var explicitSut: CLDExplicitResult? var error : NSError? // When let params = CLDExplicitRequestParams() params.setQualityAnalysis(true) cloudinary!.createManagementApi().explicit(publicId, type: "upload", params: params).response({ (resultRes, errorRes) in explicitSut = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNil(error, "explicit error should be nil") XCTAssertNotNil(explicitSut, "explicit result should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.blockiness, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.chromaSubsampling, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.resolution, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.noise, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.colorScore, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.jpegChroma, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.dct, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.jpegQuality, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.focus, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.saturation, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.contrast, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.exposure, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.lighting, "qualityAnalysis param should not be nil") XCTAssertNotNil(explicitSut?.qualityAnalysisResult?.pixelScore, "qualityAnalysis param should not be nil") } } ================================================ FILE: Example/Tests/NetworkTests/UploaderTests/UploaderTests.swift ================================================ // // UploaderTests.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest @testable import Cloudinary class UploaderTests: NetworkBaseTest { // MARK: - Tests func testUploadImageData() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let data = TestResourceType.borderCollie.data var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setColors(true) cloudinary!.createUploader().signedUpload(data: data, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") } func testUploadMediaMetadata() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let data = TestResourceType.borderCollie.data var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setMediaMetadata(true) cloudinary!.createUploader().signedUpload(data: data, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNotNil(result?.getParam(.metadata), "metadata should not be nil") XCTAssertNil(error, "error should be nil") } func testUploadRespectTimeoutFromConfiguration() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectedResult = "-1001" let expectation = self.expectation(description: "upload should fail") let data = TestResourceType.borderCollie.data var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setColors(true) cloudinaryInsufficientTimeout!.createUploader().signedUpload(data: data, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } func testUploadRespectTimeoutFromParameters() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectedResult = "-1001" let expectation = self.expectation(description: "upload should fail") let data = TestResourceType.borderCollie.data var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setColors(true) params.setTimeout(from: cloudinaryInsufficientTimeout!.config) cloudinaryInsufficientTimeout!.createUploader().signedUpload(data: data, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) var actualResult = String() if let error = error { actualResult = String((error as NSError).code) } XCTAssertNotNil(error, "error should not be nil") XCTAssertNil(result, "response should be nil") XCTAssertEqual(actualResult, expectedResult, "error should occur due to timeout") } func testUploadPNGImageData() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let data = TestResourceType.logo.data // Load the image into memory to decompress it, this will give us the real size of the image. let imageSize = UIImage(data: data)?.pngData()?.count var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setResourceType(.image) let chain = CLDImagePreprocessChain().setEncoder(CLDPreprocessHelpers.customImageEncoder(format: EncodingFormat.PNG, quality: 100)) cloudinary!.createUploader().signedUpload(data: data, params: params, preprocessChain: chain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertEqual(imageSize, Int(result!.length!)) } func testUploadPNGImageDataFromMemory() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let pngImageDataLoadedToMemory = UIImage(data: TestResourceType.logo.data)!.pngData()! var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setResourceType(.image) let chain = CLDImagePreprocessChain().setEncoder(CLDPreprocessHelpers.customImageEncoder(format: EncodingFormat.PNG, quality: 100)) cloudinary!.createUploader().signedUpload(data: pngImageDataLoadedToMemory, params: params, preprocessChain: chain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertEqual(pngImageDataLoadedToMemory.count, Int(result!.length!)) } func testUploadPNGImageFile() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let url = TestResourceType.logo.url // Load the image into memory to decompress it, this will give us the real size of the image. let imageSize = UIImage(contentsOfFile: url.relativePath)?.pngData()?.count var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setResourceType(.image) let chain = CLDImagePreprocessChain().setEncoder(CLDPreprocessHelpers.customImageEncoder(format: EncodingFormat.PNG, quality: 100)) cloudinary!.createUploader().signedUpload(url: url, params: params, preprocessChain: chain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertEqual(imageSize, Int(result!.length!)) } func testUploadPNGImageFileWithPreprocess() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let file = TestResourceType.logo.url var result: CLDUploadResult? var error: NSError? let preprocessChain = CLDImagePreprocessChain() .addStep(CLDPreprocessHelpers.limit(width: 50, height: 50)) .setEncoder(CLDPreprocessHelpers.customImageEncoder(format: EncodingFormat.PNG, quality: 100)) let params = CLDUploadRequestParams() params.setColors(true) cloudinary!.createUploader().signedUpload(url: file, params: params, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result!.width, "width should not be nil") XCTAssertNotNil(result!.height, "height should not be nil") // The image "logo" is not a square, setting maximum width and height to 50 will match its aspect ratio but still should be less then or equal to 50. XCTAssertTrue(result!.width! <= 50, "Width must be less then 50") XCTAssertTrue(result!.height! <= 50, "Height must be less then 50") } func testUploadImageFile() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let file = TestResourceType.borderCollie.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setColors(true) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") } func testUploadFolderDecoupling() throws { try XCTSkipUnless(NetworkTestUtils.skipFolderDecouplingTest(), "prevents redundant call to Cloudinary PAID Folder Decoupling service. to allow Folder Decoupling service testing - set to true") XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed and return the params sent") let file = TestResourceType.borderCollie.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setPublicIdPrefix("public_id_prefix") params.setAssetFolder("asset_folder") params.setDisplayName("display_name") params.setUseFilenameAsDisplayName(true) params.setFolder("folder/test") cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result?.assetFolder, "result should not be nil") XCTAssertNotNil(result?.displayName, "result should not be nil") XCTAssertNil(error, "error should be nil") } func testUploadImageFileWithPreprocess() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let file = TestResourceType.borderCollie.url var result: CLDUploadResult? var error: NSError? let preprocessChain = CLDImagePreprocessChain().addStep(CLDPreprocessHelpers.limit(width: 500, height: 500)) let params = CLDUploadRequestParams() params.setColors(true) cloudinary!.createUploader().signedUpload(url: file, params: params, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertEqual(result?.width, 500) XCTAssertEqual(result?.height, 500) } func testUploadWithPreprocessValidator() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should fail") let file = TestResourceType.logo.url var result: CLDUploadResult? var error: NSError? let preprocessChain = CLDImagePreprocessChain().addStep(CLDPreprocessHelpers.dimensionsValidator(minWidth: 500, maxWidth: 1500, minHeight: 500, maxHeight: 1500)) let params = CLDUploadRequestParams() params.setColors(true) cloudinary!.createUploader().signedUpload(url: file, params: params, preprocessChain: preprocessChain).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(result, "result should be nil") XCTAssertNotNil(error, "error should not be nil") } func testUploadLargeErrors() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") var expectation = self.expectation(description: "Chunk size under 5MB should fail") let file = TestResourceType.dog.url var requestError: NSError? let params = CLDUploadRequestParams() params.setResourceType(CLDUrlResourceType.video) // chunk size is too small: cloudinary!.createUploader().signedUploadLarge(url: file, params: params, chunkSize: 3 * 1024 * 1024, progress: nil).response({ (resultRes, errorRes) in requestError = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) expectation = self.expectation(description: "Uploading non-existing file should fail") cloudinary!.createUploader().signedUploadLarge(url: URL(fileURLWithPath: "non/existing/file"), params: params, chunkSize: 10 * 1024 * 1024, progress: nil).response({ (resultRes, errorRes) in requestError = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(requestError, "Error should not be nil") } func testUploadVideoData() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let data = TestResourceType.dog.data var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams().setColors(true) params.setResourceType(.video) cloudinary!.createUploader().signedUpload(data: data, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: 60.0, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") } func testUploadAccessControlParams() { let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .iso8601) formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z" let aclToken = CLDAccessControlRule.token() let start = formatter.date(from: "2019-02-22 16:20:57 +0200")! let end = formatter.date(from: "2019-03-22 00:00:00 +0200")! let acl = CLDAccessControlRule.anonymous(start: start, end: end) let aclString = "{\"access_type\":\"anonymous\",\"start\":\"2019-02-22 16:20:57 +0200\",\"end\":\"2019-03-22 00:00 +0200\"}" var params = CLDUploadRequestParams().setAccessControl([aclToken]) XCTAssertEqual(params.accessControl, "[{\"access_type\":\"token\"}]") params = CLDUploadRequestParams().setAccessControl([acl]) XCTAssertNotNil(params.accessControl) params = CLDUploadRequestParams().setAccessControl(aclString) XCTAssertEqual(params.accessControl!, aclString) } func testUploadWithAccessControl() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .iso8601) formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z" let start = formatter.date(from: "2019-02-22 16:20:57 +0200")! let end = formatter.date(from: "2019-03-22 00:00:00 +0200")! let anonStartEnd = CLDAccessControlRule.anonymous(start: start, end: end) let anonStart = CLDAccessControlRule.anonymous(start: start) let anonEnd = CLDAccessControlRule.anonymous(end: end) let token = CLDAccessControlRule.token() uploadAndCompare(accessControl: [anonStartEnd]) uploadAndCompare(accessControl: [anonStart]) uploadAndCompare(accessControl: [anonEnd]) uploadAndCompare(accessControl: [token]) uploadAndCompare(accessControl: [anonStart, token]) uploadAndCompare(accessControl: [anonStartEnd, token]) uploadAndCompare(accessControl: [token, anonEnd]) } fileprivate func uploadAndCompare(accessControl: [CLDAccessControlRule]) { let file = TestResourceType.borderCollie.url var result: CLDUploadResult? var error: NSError? let expectation = self.expectation(description: "Upload should succeed") let params = CLDUploadRequestParams().setAccessControl(accessControl) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: 30.0, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") let returnedAcl = (result?.accessControl)! for index in 0.. Bool { var matches = 0 if let publicId = publicId { let regex = try! NSRegularExpression(pattern: "\(filename)_[a-z0-9]{6}", options: NSRegularExpression.Options(rawValue: 0)) matches = regex.numberOfMatches(in: publicId, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, publicId.count)) } return matches == 1 } func testUniqueFilename() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let filename = resource.fileName let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setUseFilename(true).setUniqueFilename(false) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertEqual(result?.publicId ?? "", filename) } func testUploadAsync() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams().setAsync(true) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") let status = result?.resultJson["status"] as? String XCTAssertEqual(status, "pending") } func testUploadLargeWithSignature(){ let expectation = self.expectation(description: "Signed upload large should succeed") let timestamp = Int(Date().timeIntervalSince1970) let signature = cloudinarySignParamsUsingSecret(["timestamp" : String(describing: timestamp)], cloudinaryApiSecret: (cloudinary?.config.apiSecret)!) let params = CLDUploadRequestParams().setResourceType(CLDUrlResourceType.video).setSignature(CLDSignature(signature: signature, timestamp: NSNumber(value: timestamp))) let resource: TestResourceType = .dog let file = resource.url var result: CLDUploadResult? var error: NSError? cloudinaryNoSecret.createUploader().signedUploadLarge(url: file, params: params as! CLDUploadRequestParams, chunkSize: 6 * 1024 * 1024) .response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") } func testEager() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() let trans = CLDEagerTransformation().setCrop(.crop).setWidth(2.0).setFormat("png") let trans2 = CLDEagerTransformation().setCrop(.crop).setWidth(2.0).setFormat("gif") params.setEager([trans, trans2]) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertTrue((result?.eager?[0].url?.hasSuffix("png")) ?? false) XCTAssertTrue((result?.eager?[1].url?.hasSuffix("gif")) ?? false) } func testHeaders() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setHeaders(["Link": "1"]) params.setContext(["caption": "My Logo"]) cloudinary?.setExtraHeaderes(["Test": "Test"]) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") } func testExtraHeaders() { let extraHeaders = ["Test": "Test"] let testCloudinary = CLDCloudinary(configuration: cloudinary!.config) testCloudinary.setExtraHeaderes(extraHeaders) let networkCoordinator = TestableCloudinary.getNetworkCoordinator(from: testCloudinary) as! CLDNetworkCoordinator XCTAssertEqual(networkCoordinator.getExtraHeaders(), extraHeaders) } func testFaceCoordinates() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() let coordinate = CLDCoordinate(rect: CGRect(x: 10, y: 10, width: 100, height: 15000)) params.setFaceCoordinates([coordinate]) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") let resultCoords = (result!.coordinates!.faces as! NSArray)[0] as! NSArray XCTAssertEqual(resultCoords[0] as! Float, 10) XCTAssertEqual(resultCoords[1] as! Float, 10) XCTAssertEqual(resultCoords[2] as! Float, 100) XCTAssertEqual(resultCoords[3] as! Float, Float(result!.height!)) } func testCustomCoordinates() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() let coordinate = CLDCoordinate(rect: CGRect(x: 10, y: 10, width: 100, height: 100)) params.setCustomCoordinates([coordinate]) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") } func testResponsiveBreakpoints() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() let breakpoints = CLDResponsiveBreakpoints(createDerived: true).setBytesStep(1000).setFormat("webp").setTransformations(CLDTransformation().setAngle(10)) params.setResponsiveBreakpoints([breakpoints]) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result?.responsiveBreakpoints?[0].breakpoints?[0].url, "responsive breakpoints url cannot be nil") XCTAssertTrue(result!.responsiveBreakpoints![0].breakpoints![0].url!.hasSuffix("webp"), "responsive breakpoints url has to end with the right format (webp)") XCTAssertNotNil(result?.responsiveBreakpoints?[0].transformation) XCTAssertTrue(result!.responsiveBreakpoints![0].transformation!.contains("a_10")) } func testContext() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") var expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() let customContext = ["caption": "some cäption", "alt": "alternativè"] params.setContext(customContext) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes if let error = error { print(error) } expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") if let context = result!.context { if let custom = context["custom"] { XCTAssertTrue(NSDictionary(dictionary: custom).isEqual(to: customContext)) } else { XCTFail("Context should have a 'custom' key") } } else { XCTFail("Result should include a 'context' key") } expectation = self.expectation(description: "Explicit should succeed") let differentContext = ["caption": "different = caption", "alt2": "alt|alternative alternative"] let exParams: CLDExplicitRequestParams? = CLDExplicitRequestParams().setType(.upload).setContext(differentContext) cloudinary!.createManagementApi().explicit(result!.publicId!, type: .upload, params: exParams).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") if let context = result!.context { if let custom = context["custom"] { XCTAssertTrue(NSDictionary(dictionary: custom).isEqual(to: differentContext)) } else { XCTFail("Context should have a 'custom' key") } } else { XCTFail("Result should include a 'context' key") } } func testQualityAnalysis() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setQualityAnalysis(true) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") XCTAssertNotNil(result?.qualityAnalysisResult, "quality analysis field in upload result should not be nil") } func test_upload_NoApiKeyFail() { // Given XCTAssertNotNil(cloudinary!.config.apiKey, "Must set api key for this test") let expectation = self.expectation(description: "Upload should fail for not specifing an api key in config and not passing it as argument") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let configWithEmptyApiKey = CLDConfiguration(cloudName: cloudinary!.config.cloudName, apiKey: "", apiSecret: cloudinary!.config.apiSecret, secure: true) let cloudinaryWithNoKey = CLDCloudinary(configuration: configWithEmptyApiKey, sessionConfiguration: .default) // When cloudinaryWithNoKey.createUploader().signedUpload(url: file, params: CLDUploadRequestParams()).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Expect XCTAssertNil(result, "result should be nil") XCTAssertNotNil(error, "error should not be nil") XCTAssertEqual(400, error!.code) XCTAssertEqual("Missing required parameter - api_key", error!.userInfo["message"] as! String, "Api key should be unassigned") } func test_upload_ApiKeyAsArgument() { // Given XCTAssertNotNil(cloudinary!.config.apiKey, "Must set api key for this test") let expectation = self.expectation(description: "Upload should succeed with the key passed as an argument") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let cloudName = cloudinary!.config.cloudName! let apiKey = "" let apiSecret = cloudinary!.config.apiSecret XCTAssertNotNil(cloudName, "cloudName Should not be nil") XCTAssertNotNil(apiSecret, "apiSecret Should not be nil") let configWithEmptyApiKey = CLDConfiguration(cloudName: cloudName, apiKey: apiKey, apiSecret: apiSecret, secure: true) let cloudinaryWithNoKey = CLDCloudinary(configuration: configWithEmptyApiKey, sessionConfiguration: .default) XCTAssertEqual(cloudinaryWithNoKey.config.apiKey, "", "Should be empty") // When let params = CLDUploadRequestParams() params.setApiKey(cloudinary!.config.apiKey!) cloudinaryWithNoKey.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Expect XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") } func test_upload_ApiKeyWithConfig() { // Given XCTAssertNotNil(cloudinary!.config.apiKey, "Must set api key for this test") let expectation = self.expectation(description: "Upload should succeed with the key in config") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? // When cloudinary!.createUploader().signedUpload(url: file, params: CLDUploadRequestParams()).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) // Expect XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") } // MARK: - eval func test_eval_JavaScriptAddTag_shouldAddTag() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url let tagInput = "blurry" let input = "upload_options.tags = '\(tagInput)'" var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setEval(input) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") if let tags = result?.tags { XCTAssertTrue(tags.contains(tagInput), "eval field in this scenario should add tag via javascript code, then in the upload result the tag should appear") } else { XCTFail("eval field in this scenario should add tag via javascript code, then in the upload result the tag should appear") } } func test_eval_JavaScriptAddTagWithCondition_shouldAddTag() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url let tagInput = "blurry" let input = "if (resource_info.quality_analysis.focus > 0.5) { upload_options.tags = '\(tagInput)' }" var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setEval(input) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") if let tags = result?.tags { XCTAssertTrue(tags.contains(tagInput), "eval field in this scenario should add tag via javascript code, then in the upload result the tag should appear") } else { XCTFail("eval field in this scenario should add tag via javascript code, then in the upload result the tag should appear") } } func testManualModeration() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setModeration(.manual) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") if let moderationArr = result?.moderation as? NSArray { if let mod = moderationArr.firstObject as? NSDictionary, let status = mod["status"] as? String, let kind = mod["kind"] as? String { XCTAssertEqual(status, "pending") XCTAssertEqual(kind, "manual") } else { XCTFail("Moderation dictionary should hold its status and kind.") } } else { XCTFail("Manual moderation response should be a dictionary holding a moderation dictionary.") } } func testBackgroundRemoval() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setBackgroundRemoval("illegal") params.setResourceType(.image) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(result, "result should be nil") XCTAssertNotNil(error, "error should not be nil") if let userInfo = error?.userInfo, let errMessage = userInfo["message"] as? String { XCTAssertNotNil(errMessage.range(of: "Background removal is invalid")) XCTAssertEqual(userInfo["statusCode"] as? Int, 400) } else { XCTFail("Error should hold a message in its user info.") } } func testFilenameOverride() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed and uploaded filename should change to 'overridden'") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setFilenameOverride("overridden") params.setUseFilename(true) params.setResourceType(.image) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNil(error, "error should be nil") if let originalFilename = result?.originalFilename { XCTAssertEqual(originalFilename, "overridden", "Filename mismatch, replaced name should be 'overridden'") } else { XCTFail("Upload param 'original_filename' is missing") } } func testRawConversion() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .docx let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setRawConvert("illegal") params.setResourceType(.raw) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(result, "result should be nil") XCTAssertNotNil(error, "error should not be nil") if let userInfo = error?.userInfo, let errMessage = userInfo["message"] as? String { XCTAssertNotNil(errMessage.range(of: "Raw convert is invalid")) XCTAssertEqual(userInfo["statusCode"] as? Int, 400) } else { XCTFail("Error should hold a message in its user info.") } } func testCategorization() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setCategorization("illegal") cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(result, "result should be nil") XCTAssertNotNil(error, "error should not be nil") if let userInfo = error?.userInfo, let errMessage = userInfo["message"] as? String { XCTAssertNotNil(errMessage.range(of: "Categorization item illegal is not valid")) XCTAssertEqual(userInfo["statusCode"] as? Int, 400) } else { XCTFail("Error should hold a message in its user info.") } } func testDetection() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setDetection("illegal") cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNil(result, "result should be nil") XCTAssertNotNil(error, "error should not be nil") if let errMessage = error?.userInfo["message"] as? String { XCTAssertNotNil(errMessage.range(of: "Detection invalid")) } else { XCTFail("Error should hold a message in its user info.") } } func testCldIsRemoteUrl(){ let remoteUrls = [ "ftp://ftp.cloudinary.com/images/old_logo.png", "http://cloudinary.com/images/old_logo.png", "https://cloudinary.com/images/old_logo.png", "s3://s3-us-west-2.amazonaws.com/cloudinary/images/old_logo.png", "gs://cloudinary/images/old_logo.png", "data:image/gif;charset=utf-8;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", "data:image/gif;param1=value1;param2=value2;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", "data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg", "data:image/gif;charset=utf8;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" ] remoteUrls.forEach({XCTAssertTrue($0.cldIsRemoteUrl())}) let notRemoteUrls = [ "not a remote url", "http/almost", "" ] notRemoteUrls.forEach({XCTAssertFalse($0.cldIsRemoteUrl())}) } func testQualityOverride(){ XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let result = uploadResource() validateQualityOverride(publicId: result.publicId!, quality: "80", shouldSucceed: true) validateQualityOverride(publicId: result.publicId!, quality: "80:420", shouldSucceed: true) validateQualityOverride(publicId: result.publicId!, quality: "80:421", shouldSucceed: false) validateQualityOverride(publicId: result.publicId!, quality: "auto:best", shouldSucceed: true) validateQualityOverride(publicId: result.publicId!, quality: "auto:advanced", shouldSucceed: false) validateQualityOverride(publicId: result.publicId!, quality: "none", shouldSucceed: true) } //MARK: - Helpers func uploadResource() -> CLDUploadResult { let expectation = self.expectation(description: "Upload should succeed") let resource: TestResourceType = .borderCollie let file = resource.url var result: CLDUploadResult? var error: NSError? cloudinary!.createUploader().signedUpload(url: file, params: CLDUploadRequestParams()) .response({ (resultRes, errorRes) in result = resultRes error = errorRes if let error = error { print(error) } expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") return result! } func testUploadLarge() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload large should succeed") let file = TestResourceType.dog.url let filename = TestResourceType.dog.fileName var requestResult: CLDUploadResult? var requestError: NSError? let params = CLDUploadRequestParams() params.setResourceType(CLDUrlResourceType.video) params.setUseFilename(true) cloudinary!.createUploader().signedUploadLarge(url: file, params: params, chunkSize: 5 * 1024 * 1024).response({ (result, error) in requestResult = result requestError = error expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(requestResult, "result should not be nil") XCTAssertNil(requestError, "error should be nil") XCTAssertTrue(isUsedFilename(filename: filename, publicId: requestResult?.publicId)) } func testAutoChaptering() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed and return playback url") let file = TestResourceType.dog.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setColors(true) params.setResourceType(.video) params.setAutoChpatering(true) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNotNil(result?.playbackUrl, "playback url should not be nil") XCTAssertNil(error, "error should be nil") } func testAutoTranscription() { XCTAssertNotNil(cloudinary!.config.apiSecret, "Must set api secret for this test") let expectation = self.expectation(description: "Upload should succeed and return playback url") let file = TestResourceType.dog.url var result: CLDUploadResult? var error: NSError? let params = CLDUploadRequestParams() params.setColors(true) params.setResourceType(.video) params.setAutoTranscription(true) cloudinary!.createUploader().signedUpload(url: file, params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) XCTAssertNotNil(result, "result should not be nil") XCTAssertNotNil(result?.playbackUrl, "playback url should not be nil") XCTAssertNil(error, "error should be nil") } func validateQualityOverride(publicId: String, quality: String, shouldSucceed: Bool){ let qualityOverrideExpectation = self.expectation(description: "Explicit call with quality override should succeed") var result: CLDUploadResult? var error: NSError? let params = CLDExplicitRequestParams().setQualityOverride(quality) cloudinary!.createManagementApi().explicit(publicId, type: "upload", params: params).response({ (resultRes, errorRes) in result = resultRes error = errorRes if let error = error { print(error) } qualityOverrideExpectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) let notNil = shouldSucceed ? result : error XCTAssertNotNil(notNil, "quality \(quality) should \(shouldSucceed ? "succeed" : "fail")") } func createUploadPresetIfNeeded(_ uploadPresetUrl: String, presetName: String) { let semaphore = DispatchSemaphore(value: 0) if let url = URL(string: "\(uploadPresetUrl)/\(presetName)") { let session = URLSession.shared session.dataTask(with: url, completionHandler: { (returnData, response, error) -> () in if let returnData = returnData { if let strData = String(data: returnData, encoding: String.Encoding.utf8) { if strData.range(of: "Can't find upload preset named") != nil { if let url = URL(string: uploadPresetUrl) { var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") let params = "name=\(presetName)&unsigned=true&tags=ios_sdk_test" let httpData = params.data(using: String.Encoding.utf8) request.httpBody = httpData session.dataTask(with: request, completionHandler: { (returnData, response, error) -> () in if let returnData = returnData, let strData = String(data: returnData, encoding: String.Encoding.utf8) { print(strData) } semaphore.signal() }).resume() } else { XCTFail("preset request failed") semaphore.signal() } } else { semaphore.signal() } } else { XCTFail("preset request failed") semaphore.signal() } } else { XCTFail("preset request failed") semaphore.signal() } }).resume() } else { XCTFail("preset request failed") semaphore.signal() } _ = semaphore.wait(timeout: DispatchTime.distantFuture) } } ================================================ FILE: Example/Tests/ParamTests/UploadRequestParamsTests.swift ================================================ // // UploadRequestParamsTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class UploadRequestParamsTests: BaseTestCase { var sut : CLDUploadRequestParams! // MARK: - setup and teardown override func setUp() { super.setUp() sut = CLDUploadRequestParams() } override func tearDown() { sut = nil super.tearDown() } // MARK: - accessibility analysis func test_getAccessibilityAnalysis_unset_shouldNotStoreValue() { // Then XCTAssertNil(sut.accessibilityAnalysis, "unset property should not be stored in params") } func test_setAccessibilityAnalysis_true_shouldStoreTrueValue() { // When sut.setAccessibilityAnalysis(true) // Then XCTAssertTrue(sut.accessibilityAnalysis == true, "setting property to true should store true in params") } func test_setAccessibilityAnalysis_trueThenFalse_shouldUpdateValue() { // When sut.setAccessibilityAnalysis(true) sut.setAccessibilityAnalysis(false) // Then XCTAssertTrue(sut.accessibilityAnalysis == false, "updating property to false value should store false in params") } // MARK: - ocr func test_getOcr_unset_shouldNotStoreValue() { // Then XCTAssertFalse(sut.ocr, "unset property should not be false") XCTAssertFalse(sut.params.keys.contains("ocr"), "unset property should not be stored in params") } func test_setOcr_true_shouldStoreTrueValue() { // When sut.setOcr(true) // Then XCTAssertTrue(sut.ocr == true, "setting property to true should store true in params") XCTAssertTrue(sut.params.keys.contains("ocr"), "setting property to true should store true in params") } func test_setOcr_trueThenFalse_shouldRemoveValue() { // When sut.setOcr(true) sut.setOcr(false) // Then XCTAssertFalse(sut.ocr, "updating ocr property to false value should remove store property from params") XCTAssertFalse(sut.params.keys.contains("ocr"), "updating ocr property to false value should remove store property from params") } // MARK: - eval func test_getEval_unset_shouldNotStoreValue() { XCTAssertNil(sut.eval, "unset property should not be stored in params") } func test_setEval_String_shouldStoreValue() { // Given let input = "evalString" // When sut.setEval(input) // Then XCTAssertEqual(sut.eval, input, "set property should be stored in params") } func test_backgroundRemoval_String_shouldStoreValue() { // Given let input = "backgroundString" // When sut.setBackgroundRemoval(input) // Then XCTAssertEqual(sut.backgroundRemoval, input, "set property should be stored in params") } func test_setOcr_updateValue_shouldUpdateValue() { // Given let initialInput = "evalString" let updatedInput = "updatedString" // When sut.setEval(initialInput) sut.setEval(updatedInput) // Then XCTAssertEqual(sut.eval, updatedInput, "Init without longUrlSignature should store the default false value") } // MARK: - quality analysis func test_getQualityAnalysis_unset_shouldNotStoreValue() { XCTAssertNil(sut.qualityAnalysis, "unset property should not be stored in params") } func test_setQualityAnalysis_String_shouldStoreValue() { // Given let input = true // When sut.setQualityAnalysis(input) // Then XCTAssertEqual(sut.qualityAnalysis, input, "set property should be stored in params") } func test_setQualityAnalysis_updateValue_shouldUpdateValue() { // Given let initialInput = true let updatedInput = false // When sut.setQualityAnalysis(initialInput) sut.setQualityAnalysis(updatedInput) // Then XCTAssertEqual(sut.qualityAnalysis, updatedInput, "updated property should be stored in params") } } ================================================ FILE: Example/Tests/Preprocess/PreprocessTests.swift ================================================ // // PreprocessTests.swift // CloudinaryTests // // Created by Nitzan Jaitman on 04/01/2018. // Copyright © 2018 Cloudinary. All rights reserved. // import XCTest import UIKit @testable import Cloudinary class PreprocessTests: BaseTestCase { var sut: UIImage! // MARK: - setup and tearDown override func setUp() { super.setUp() } override func tearDown() { super.tearDown() sut = nil } // MARK: - get image fileprivate func getImage()->UIImage { let bundle = Bundle(for: PreprocessTests.self) let url = bundle.url(forResource: "borderCollie", withExtension: "jpg")! return CLDPreprocessHelpers.resizeImage(image: UIImage(contentsOfFile: url.path)!, requiredSize: CGSize(width: 300, height:300)) } fileprivate func getFullSizeImage(_ resourceType: NetworkBaseTest.TestResourceType = .borderCollie) -> UIImage { let bundle = Bundle(for: PreprocessTests.self) let url = bundle.url(forResource: resourceType.fileName, withExtension: resourceType.resourceExtension)! return UIImage(contentsOfFile: url.path)! } // MARK: - limit func testLimit() { let image = getImage() var modified = try! CLDPreprocessHelpers.limit(width: 5000, height: 5000)(image) XCTAssertEqual(image.size, modified.size) modified = try! CLDPreprocessHelpers.limit(width: 5000, height: 10)(image) XCTAssertEqual(modified.size, CGSize(width: 10, height:10)) modified = try! CLDPreprocessHelpers.limit(width: 10, height: 5000)(image) XCTAssertEqual(modified.size, CGSize(width: 10, height:10)) modified = try! CLDPreprocessHelpers.limit(width: 200, height: 300)(image) XCTAssertEqual(modified.size, CGSize(width: 200, height:200)) modified = try! CLDPreprocessHelpers.limit(width: 300, height: 200)(image) XCTAssertEqual(modified.size, CGSize(width: 200, height:200)) modified = try! CLDPreprocessHelpers.limit(width: 400, height: 200)(image) XCTAssertEqual(modified.size, CGSize(width: 200, height:200)) modified = try! CLDPreprocessHelpers.limit(width: 200, height: 400)(image) XCTAssertEqual(modified.size, CGSize(width: 200, height:200)) modified = try! CLDPreprocessHelpers.limit(width: 500, height: 300)(image) XCTAssertEqual(modified.size, CGSize(width: 300, height:300)) modified = try! CLDPreprocessHelpers.limit(width: 300, height: 500)(image) XCTAssertEqual(modified.size, CGSize(width: 300, height:300)) } // MARK: - crop func test_crop_small_shouldCropToSize() { // Given let image = getFullSizeImage() let imageNewRect = CGRect(x: 0, y: 0, width: 5, height: 5) // When sut = try! CLDPreprocessHelpers.crop(cropRect: imageNewRect)(image) // Then XCTAssertEqual(sut.size ,imageNewRect.size, "image should be cropped to the requested size") } func test_crop_big_shouldCropToSize() { // Given let image = getFullSizeImage() let imageNewRect = CGRect(x: 0, y: 0, width: 900, height: 900) // When sut = try! CLDPreprocessHelpers.crop(cropRect: imageNewRect)(image) // Then XCTAssertEqual(sut.size, imageNewRect.size, "image should be cropped to the requested size") } func test_crop_widthOutOfBounds_shouldCropToSize() { // Given let image = getFullSizeImage() let imageNewRect = CGRect(x: 0, y: 0, width: 1900, height: 40) // borderCollie image size 960 * 960 // Then XCTAssertThrowsError(try CLDPreprocessHelpers.crop(cropRect: imageNewRect)(image), "trying to crop an image out of bounds should throw an error") } func test_crop_heightOutOfBounds_shouldCropToSize() { // Given let image = getFullSizeImage() let imageNewRect = CGRect(x: 0, y: 0, width: 40, height: 1900) // borderCollie image size 960 * 960 // Then XCTAssertThrowsError(try CLDPreprocessHelpers.crop(cropRect: imageNewRect)(image), "trying to crop an image out of bounds should throw an error") } func test_crop_XOutOfBounds_shouldCropToSize() { // Given let image = getFullSizeImage() let imageNewRect = CGRect(x: 950, y: 0, width: 40, height: 40) // borderCollie image size 960 * 960 // Then XCTAssertThrowsError(try CLDPreprocessHelpers.crop(cropRect: imageNewRect)(image), "trying to crop an image out of bounds should throw an error") } func test_crop_YOutOfBounds_shouldCropToSize() { // Given let image = getFullSizeImage() let imageNewRect = CGRect(x: 0, y: 950, width: 40, height: 40) // borderCollie image size 960 * 960 // Then XCTAssertThrowsError(try CLDPreprocessHelpers.crop(cropRect: imageNewRect)(image), "trying to crop an image out of bounds should throw an error") } // MARK: - rotate func test_rotate_45_shouldRotateToAngle() { // Given let image = getFullSizeImage() let imageExpectedSize = CGSize(width: 1357, height: 1357) // When sut = try! CLDPreprocessHelpers.rotate(degrees: 45)(image) // Then XCTAssertEqual(sut.size ,imageExpectedSize, "image should be rotated to the requested angle (this should change the image size)") } func test_rotate_90_shouldRotateToAngle() { // Given let image = getFullSizeImage(.logo) let imageExpectedSize = CGSize(width: image.size.height, height: image.size.width) // When sut = try! CLDPreprocessHelpers.rotate(degrees: 90)(image) // Then XCTAssertEqual(sut.size ,imageExpectedSize, "image should be rotated to the requested angle (this should change the image size)") } func test_rotate_180_shouldRotateToAngle() { // Given let image = getFullSizeImage() let imageExpectedSize = image.size // When sut = try! CLDPreprocessHelpers.rotate(degrees: 180)(image) // Then XCTAssertEqual(sut.size ,imageExpectedSize, "image should be rotated to the requested angle (this should change the image size)") } func test_rotate_810_shouldRotateToAngle() { // Given let image = getFullSizeImage() let imageExpectedSize = CGSize(width: image.size.height, height: image.size.width) // When sut = try! CLDPreprocessHelpers.rotate(degrees: 810)(image) // Then XCTAssertEqual(sut.size ,imageExpectedSize, "image should be rotated to the requested angle (this should change the image size)") } func test_rotate_45png_shouldRotateToAngle() { // Given let image = getFullSizeImage(.logo) let imageExpectedSize = CGSize(width: 206, height: 206) // When sut = try! CLDPreprocessHelpers.rotate(degrees: 45)(image) // Then XCTAssertEqual(sut.size ,imageExpectedSize, "image should be rotated to the requested angle (this should change the image size)") } func test_rotate_CIImage_shouldRotateToAngle() { // Given let ciimage = CIImage(image: getFullSizeImage(.logo))! let uiimageFromCIImage = UIImage(ciImage: ciimage) let imageExpectedSize = CGSize(width: 206, height: 206) // When sut = try! CLDPreprocessHelpers.rotate(degrees: 45)(uiimageFromCIImage) // Then XCTAssertNil(uiimageFromCIImage.cgImage, "image.cgImage should be nil") XCTAssertEqual(sut.size ,imageExpectedSize, "image should be rotated to the requested angle (this should change the image size)") } func test_rotate_shouldEqualPrePreparedImage() { // Given let image = getFullSizeImage() let prePreparedImage = getFullSizeImage(.borderCollieRotatedPng) // When sut = try! CLDPreprocessHelpers.rotate(degrees: 45)(image) // Then XCTAssertEqual(sut.size, prePreparedImage.size, "rotated image should be equal to pre prepared image") } // MARK: - dimension validator func testDimensionValidator() { let image = getImage() var modified = try? CLDPreprocessHelpers.dimensionsValidator(minWidth: 300, maxWidth: 300, minHeight: 300, maxHeight: 300)(image) XCTAssertNotNil(modified) modified = try? CLDPreprocessHelpers.dimensionsValidator(minWidth: 300, maxWidth: 3000, minHeight: 300, maxHeight: 3000)(image) XCTAssertNotNil(modified) modified = try? CLDPreprocessHelpers.dimensionsValidator(minWidth: 10, maxWidth: 3000, minHeight: 10, maxHeight: 3000)(image) XCTAssertNotNil(modified) modified = try? CLDPreprocessHelpers.dimensionsValidator(minWidth: 10, maxWidth: 300, minHeight: 10, maxHeight: 300)(image) XCTAssertNotNil(modified) modified = try? CLDPreprocessHelpers.dimensionsValidator(minWidth: 400, maxWidth: 3000, minHeight: 30, maxHeight: 3000)(image) XCTAssertNil(modified) modified = try? CLDPreprocessHelpers.dimensionsValidator(minWidth: 300, maxWidth: 300, minHeight: 400, maxHeight: 3000)(image) XCTAssertNil(modified) modified = try? CLDPreprocessHelpers.dimensionsValidator(minWidth: 300, maxWidth: 300, minHeight: 100, maxHeight: 200)(image) XCTAssertNil(modified) modified = try? CLDPreprocessHelpers.dimensionsValidator(minWidth: 100, maxWidth: 200, minHeight: 300, maxHeight: 300)(image) XCTAssertNil(modified) } } ================================================ FILE: Example/Tests/Preprocess/VideoPreprocessTests.swift ================================================ import XCTest import AVFoundation @testable import Cloudinary class VideoPreprocessTests: XCTestCase { var videoURL: URL! var invalidVideoURL: URL! var notAVideoURL: URL! // MARK: - Setup and TearDown override func setUp() { super.setUp() let bundle = Bundle(for: VideoPreprocessTests.self) videoURL = bundle.url(forResource: "dog", withExtension: "mp4") invalidVideoURL = URL(fileURLWithPath: "/invalid/path/to/video.mp4") notAVideoURL = bundle.url(forResource: "empty_string", withExtension: "txt") } override func tearDown() { super.tearDown() videoURL = nil invalidVideoURL = nil notAVideoURL = nil } // MARK: - Helper Methods fileprivate func getVideoTranscode(from url: URL) -> CLDVideoTranscode { return CLDVideoTranscode(sourceURL: url) } // MARK: - Tests func testSetOutputFormat() { var video = getVideoTranscode(from: videoURL) video = try! CLDVideoPreprocessHelpers.setOutputFormat(format: .mp4)(video) XCTAssertEqual(video.outputFormat, .mp4) video = try! CLDVideoPreprocessHelpers.setOutputFormat(format: .mov)(video) XCTAssertEqual(video.outputFormat, .mov) } func testSetOutputDimensions() { var video = getVideoTranscode(from: videoURL) let dimensions = CGSize(width: 1280, height: 720) video = try! CLDVideoPreprocessHelpers.setOutputDimensions(dimensions: dimensions)(video) XCTAssertEqual(video.outputDimensions, dimensions) } func testSetCompressionPreset() { var video = getVideoTranscode(from: videoURL) let preset = AVAssetExportPresetHighestQuality video = try! CLDVideoPreprocessHelpers.setCompressionPreset(preset: preset)(video) XCTAssertEqual(video.compressionPreset, preset) } func testDimensionsValidator() { var video = getVideoTranscode(from: videoURL) let dimensions = CGSize(width: 1280, height: 720) video = try! CLDVideoPreprocessHelpers.setOutputDimensions(dimensions: dimensions)(video) var modifiedVideo = try? CLDVideoPreprocessHelpers.dimensionsValidator(minWidth: 100, maxWidth: 2000, minHeight: 100, maxHeight: 2000)(video) XCTAssertNotNil(modifiedVideo) modifiedVideo = try? CLDVideoPreprocessHelpers.dimensionsValidator(minWidth: 1300, maxWidth: 2000, minHeight: 100, maxHeight: 2000)(video) XCTAssertNil(modifiedVideo) } func testInvalidURL() { let video = getVideoTranscode(from: invalidVideoURL) XCTAssertThrowsError(try CLDVideoPreprocessHelpers.setOutputFormat(format: .mp4)(video)) { error in XCTAssertEqual((error as NSError).domain, "CLDVideoPreprocessHelpers") } } func testNotAVideo() { let video = getVideoTranscode(from: notAVideoURL) XCTAssertThrowsError(try CLDVideoPreprocessHelpers.setOutputFormat(format: .mp4)(video)) { error in XCTAssertEqual((error as NSError).domain, "CLDVideoPreprocessHelpers") } } func testMissingVideoTrack() { let video = getVideoTranscode(from: videoURL) XCTAssertTrue(video.hasVideoTrack, "Original video should have a video track") let noVideoTrackURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("mov") let fileManager = FileManager.default let fileContents = Data() fileManager.createFile(atPath: noVideoTrackURL.path, contents: fileContents) let noVideoTrackTranscode = getVideoTranscode(from: noVideoTrackURL) XCTAssertFalse(noVideoTrackTranscode.hasVideoTrack, "The mock video should not have a video track") do { _ = try CLDVideoPreprocessHelpers.setOutputFormat(format: .mp4)(noVideoTrackTranscode) XCTFail("Expected to throw an error for missing video track") } catch { XCTAssertEqual((error as NSError).domain, "CLDVideoPreprocessHelpers") } } } ================================================ FILE: Example/Tests/StringUtilsTest.swift ================================================ // // Created by Nitzan Jaitman on 02/09/2018. // Copyright (c) 2018 Cloudinary. All rights reserved. // import Foundation import XCTest @testable import Cloudinary class StringUtilsTests: BaseTestCase { override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } func testBase64Encode (){ let str = "ad?.,x09~!@!" XCTAssertEqual("YWQ/Lix4MDl+IUAh", str.cldBase64Encode()) } func testBase64UrlEncode(){ let str = "ad?.,x09~!@!" XCTAssertEqual("YWQ_Lix4MDl-IUAh", str.cldBase64UrlEncode()) } } ================================================ FILE: Example/Tests/TestableCloudinary.swift ================================================ // // TestableCloudinary.swift // Cloudinary_Tests // // Created by Adi Mizrahi on 30/01/2025. // Copyright © 2025 CocoaPods. All rights reserved. // import Cloudinary class TestableCloudinary { static func getNetworkCoordinator(from cloudinary: CLDCloudinary) -> AnyObject? { let mirror = Mirror(reflecting: cloudinary) for child in mirror.children { if String(describing: type(of: child.value)).contains("CLDNetworkCoordinator") { return child.value as AnyObject } } return nil } } ================================================ FILE: Example/Tests/TransformationTests/CLDConditionExpressionTests/CLDConditionExpressionHelpersTests.swift ================================================ // // CLDConditionExpressionHelpersTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class CLDConditionExpressionHelpersTests: BaseTestCase { var sut : CLDConditionExpression! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - widthWithOperator func test_widthWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "w_lt_100" // When sut = CLDConditionExpression().width(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_widthWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "w_gt_30.3" // When sut = CLDConditionExpression().width(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_widthWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "w_eq_30.3" // When sut = CLDConditionExpression().width(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_widthWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "w_eq_ih_add_20" // When sut = CLDConditionExpression().width(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_widthWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "w_eq_ih_add_20" // When sut = CLDConditionExpression().width(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - heightWithOperator func test_heightWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "h_lt_100" // When sut = CLDConditionExpression().height(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_heightWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "h_gt_30.3" // When sut = CLDConditionExpression().height(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_heightWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "h_eq_30.3" // When sut = CLDConditionExpression().height(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_heightWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "h_eq_ih_add_20" // When sut = CLDConditionExpression().height(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_heightWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "h_eq_ih_add_20" // When sut = CLDConditionExpression().height(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - aspectRatioWithOperator func test_aspectRatioWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "ar_lt_100" // When sut = CLDConditionExpression().aspectRatio(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_aspectRatioWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "ar_gt_30.3" // When sut = CLDConditionExpression().aspectRatio(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_aspectRatioWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "ar_eq_30.3" // When sut = CLDConditionExpression().aspectRatio(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_aspectRatioWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "ar_eq_ih_add_20" // When sut = CLDConditionExpression().aspectRatio(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_aspectRatioWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "ar_eq_ih_add_20" // When sut = CLDConditionExpression().aspectRatio(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - initialWidthWithOperator func test_initialWidthWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "iw_lt_100" // When sut = CLDConditionExpression().initialWidth(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialWidthWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "iw_gt_30.3" // When sut = CLDConditionExpression().initialWidth(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialWidthWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "iw_eq_30.3" // When sut = CLDConditionExpression().initialWidth(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialWidthWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "iw_eq_ih_add_20" // When sut = CLDConditionExpression().initialWidth(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialWidthWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "iw_eq_ih_add_20" // When sut = CLDConditionExpression().initialWidth(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - initialHeightWithOperator func test_initialHeightWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "ih_lt_100" // When sut = CLDConditionExpression().initialHeight(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialHeightWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "ih_gt_30.3" // When sut = CLDConditionExpression().initialHeight(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialHeightWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "ih_eq_30.3" // When sut = CLDConditionExpression().initialHeight(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialHeightWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "ih_eq_ih_add_20" // When sut = CLDConditionExpression().initialHeight(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialHeightWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "ih_eq_ih_add_20" // When sut = CLDConditionExpression().initialHeight(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - initialAspectRatioWithOperator func test_initialAspectRatioWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "iar_lt_100" // When sut = CLDConditionExpression().initialAspectRatio(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialAspectRatioWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "iar_gt_30.3" // When sut = CLDConditionExpression().initialAspectRatio(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialAspectRatioWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "iar_eq_30.3" // When sut = CLDConditionExpression().initialAspectRatio(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialAspectRatioWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "iar_eq_ih_add_20" // When sut = CLDConditionExpression().initialAspectRatio(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialAspectRatioWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "iar_eq_ih_add_20" // When sut = CLDConditionExpression().initialAspectRatio(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - pageCountWithOperator func test_pageCountWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "pc_lt_100" // When sut = CLDConditionExpression().pageCount(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_pageCountWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "pc_gt_30.3" // When sut = CLDConditionExpression().pageCount(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_pageCountWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "pc_eq_30.3" // When sut = CLDConditionExpression().pageCount(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_pageCountWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "pc_eq_ih_add_20" // When sut = CLDConditionExpression().pageCount(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_pageCountWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "pc_eq_ih_add_20" // When sut = CLDConditionExpression().pageCount(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - faceCountWithOperator func test_faceCountWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "fc_lt_100" // When sut = CLDConditionExpression().faceCount(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_faceCountWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "fc_gt_30.3" // When sut = CLDConditionExpression().faceCount(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_faceCountWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "fc_eq_30.3" // When sut = CLDConditionExpression().faceCount(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_faceCountWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "fc_eq_ih_add_20" // When sut = CLDConditionExpression().faceCount(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_faceCountWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "fc_eq_ih_add_20" // When sut = CLDConditionExpression().faceCount(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - tagsWithOperator func test_tagsWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "tags_lt_100" // When sut = CLDConditionExpression().tags(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_tagsWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "tags_gt_30.3" // When sut = CLDConditionExpression().tags(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_tagsWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "tags_eq_30.3" // When sut = CLDConditionExpression().tags(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_tagsWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "tags_eq_ih_add_20" // When sut = CLDConditionExpression().tags(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_tagsWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "tags_eq_ih_add_20" // When sut = CLDConditionExpression().tags(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - pageXOffsetWithOperator func test_pageXOffsetWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "px_lt_100" // When sut = CLDConditionExpression().pageXOffset(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_pageXOffsetWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "px_gt_30.3" // When sut = CLDConditionExpression().pageXOffset(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_pageXOffsetWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "px_eq_30.3" // When sut = CLDConditionExpression().pageXOffset(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_pageXOffsetWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "px_eq_ih_add_20" // When sut = CLDConditionExpression().pageXOffset(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_pageXOffsetWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "px_eq_ih_add_20" // When sut = CLDConditionExpression().pageXOffset(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - pageYOffsetWithOperator func test_pageYOffsetWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "py_lt_100" // When sut = CLDConditionExpression().pageYOffset(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_pageYOffsetWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "py_gt_30.3" // When sut = CLDConditionExpression().pageYOffset(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_pageYOffsetWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "py_eq_30.3" // When sut = CLDConditionExpression().pageYOffset(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_pageYOffsetWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "py_eq_ih_add_20" // When sut = CLDConditionExpression().pageYOffset(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_pageYOffsetWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "py_eq_ih_add_20" // When sut = CLDConditionExpression().pageYOffset(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - illustrationScoreWithOperator func test_illustrationScoreWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "ils_lt_100" // When sut = CLDConditionExpression().illustrationScore(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_illustrationScoreWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "ils_gt_30.3" // When sut = CLDConditionExpression().illustrationScore(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_illustrationScoreWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "ils_eq_30.3" // When sut = CLDConditionExpression().illustrationScore(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_illustrationScoreWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "ils_eq_ih_add_20" // When sut = CLDConditionExpression().illustrationScore(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_illustrationScoreWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "ils_eq_ih_add_20" // When sut = CLDConditionExpression().illustrationScore(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - currentPageIndexWithOperator func test_currentPageIndexWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "cp_lt_100" // When sut = CLDConditionExpression().currentPageIndex(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_currentPageIndexWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "cp_gt_30.3" // When sut = CLDConditionExpression().currentPageIndex(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_currentPageIndexWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "cp_eq_30.3" // When sut = CLDConditionExpression().currentPageIndex(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_currentPageIndexWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "cp_eq_ih_add_20" // When sut = CLDConditionExpression().currentPageIndex(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_currentPageIndexWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "cp_eq_ih_add_20" // When sut = CLDConditionExpression().currentPageIndex(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - durationWithOperator func test_durationWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "du_lt_100" // When sut = CLDConditionExpression().duration(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_durationWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "du_gt_30.3" // When sut = CLDConditionExpression().duration(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_durationWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "du_eq_30.3" // When sut = CLDConditionExpression().duration(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_durationWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "du_eq_ih_add_20" // When sut = CLDConditionExpression().duration(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_durationWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "du_eq_ih_add_20" // When sut = CLDConditionExpression().duration(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - initialDurationWithOperator func test_initialDurationWithOperator_intValue_shouldAppendValidValue() { // Given let stringOperator = "<" let value: Int = 100 let expectedValueResult = "idu_lt_100" // When sut = CLDConditionExpression().initialDuration(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialDurationWithOperator_floatValue_shouldAppendValidValue() { // Given let stringOperator = ">" let value = Float(30.3) let expectedValueResult = "idu_gt_30.3" // When sut = CLDConditionExpression().initialDuration(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialDurationWithOperator_stringValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = "30.3" let expectedValueResult = "idu_eq_30.3" // When sut = CLDConditionExpression().initialDuration(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialDurationWithOperator_expressionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "idu_eq_ih_add_20" // When sut = CLDConditionExpression().initialDuration(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_initialDurationWithOperator_conditionValue_shouldAppendValidValue() { // Given let stringOperator = "=" let value = CLDConditionExpression.initialHeight().add(by: 20) let expectedValueResult = "idu_eq_ih_add_20" // When sut = CLDConditionExpression().initialDuration(stringOperator, value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } } ================================================ FILE: Example/Tests/TransformationTests/CLDConditionExpressionTests/CLDConditionExpressionTests.m ================================================ // // ObjcCLDConditionExpressionTests.m // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "ObjcBaseTestCase.h" @interface ObjcCLDConditionExpressionTests : ObjcBaseTestCase @end @implementation ObjcCLDConditionExpressionTests CLDConditionExpression *conSut; // MARK: - setup and teardown - (void)setUp { [super setUp]; } - (void)tearDown { [super tearDown]; conSut = nil; } // MARK: - complexConditionExpression - (void)test_complexConditionExpression_shouldCreateValidExpression { // Given NSString* expectedResult = @"w_add_30_and_h_gt_30_or_ar_eq_20"; // When conSut = [[[[[[CLDConditionExpression width] addByInt:30] and] height:@">" intValue:30] or] aspectRatio:@"eq" string:@"20"]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } // MARK: - creatingConditionExpression - (void)test_creatingConditionExpression_widthAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"w_add_30"; // When conSut = [CLDConditionExpression width]; [conSut addByInt: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_heightSubtract_shouldCreateValidExpression { // Given float input = 30.3; NSString* expectedResult = @"h_sub_30.3"; // When conSut = [CLDConditionExpression height]; [conSut subtractByFloat: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_initialWidthMultiple_shouldCreateValidExpression { // Given NSString* input = @"30"; NSString* expectedResult = @"iw_mul_30"; // When conSut = [CLDConditionExpression initialWidth]; [conSut multipleByString: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_initialHeightDivide_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"ih_div_30"; // When conSut = [CLDConditionExpression initialHeight]; [conSut divideByInt: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_aspectRatioPower_shouldCreateValidExpression { // Given float input = 30.3; NSString* expectedResult = @"ar_pow_30.3"; // When conSut = [CLDConditionExpression aspectRatio]; [conSut powerByFloat: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_initialAspectRatioAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"iar_add_30"; // When conSut = [CLDConditionExpression initialAspectRatio]; [conSut addByInt: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_pageCountAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"pc_add_30"; // When conSut = [CLDConditionExpression pageCount]; [conSut addByInt: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_faceCountAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"fc_add_30"; // When conSut = [CLDConditionExpression faceCount]; [conSut addByInt: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_tagsAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"tags_add_30"; // When conSut = [CLDConditionExpression tags]; [conSut addByInt: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_pageXOffsetAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"px_add_30"; // When conSut = [CLDConditionExpression pageXOffset]; [conSut addByInt: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_pageYOffsetAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"py_add_30"; // When conSut = [CLDConditionExpression pageYOffset]; [conSut addByInt: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_illustrationScoreAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"ils_add_30"; // When conSut = [CLDConditionExpression illustrationScore]; [conSut addByInt: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_currentPageIndexAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"cp_add_30"; // When conSut = [CLDConditionExpression currentPageIndex]; [conSut addByInt: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_durationAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"du_add_30"; // When conSut = [CLDConditionExpression duration]; [conSut addByInt: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingConditionExpression_initialDurationAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"idu_add_30"; // When conSut = [CLDConditionExpression initialDuration]; [conSut addByInt: input]; NSString* actualResult = [conSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } // MARK: - asParams - (void)test_asParams_initialDurationAdd_shouldCreateValidExpression { // Given int input = 30; NSDictionary* expectedResult = @{@"idu":@"add_30"}; // When conSut = [CLDConditionExpression initialDuration]; [conSut addByInt: input]; NSDictionary* actualResult = [conSut asParams]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue(actualResult.count == 1 && expectedResult.count == 1, "Calling get asString should return the expect string"); XCTAssertTrue([actualResult.allKeys[0] isEqualToString: expectedResult.allKeys[0]], "Calling get asString should return the expect string"); XCTAssertTrue([actualResult.allValues[0] isEqualToString: expectedResult.allValues[0]], "Calling get asString should return the expect string"); } @end ================================================ FILE: Example/Tests/TransformationTests/CLDConditionExpressionTests/CLDConditionExpressionTests.swift ================================================ // // CLDConditionExpressionTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class CLDConditionExpressionTests: BaseTestCase { var sut : CLDConditionExpression! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - test initilization methods - empty func test_init_emptyInputParamaters_shouldStoreEmptyProperties() { // Given let name = String() // When sut = CLDConditionExpression() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil currentKey property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil currentValue property") XCTAssertEqual(sut.currentKey, name, "Name property should be equal to name") XCTAssertEqual(sut.currentValue, String(), "Initilized object should contain an empty string as currentValue property") } // MARK: - test initilization methods - value func test_init_emptyNameParamater_shouldStoreEmptyNameProperty() { // Given let name = String() let value = "alue" // When sut = CLDConditionExpression(value: "\(name) \(value)") // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil currentKey property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil currentValue property") XCTAssertEqual(sut.currentValue, value, "Initilized object should contain an empty string as value property") } func test_init_validStringParamatersAndNoNamePrefix_shouldStoreValidProperties() { // Given let name = "name" let value = "alue" // When sut = CLDConditionExpression(value: "\(name) \(value)") // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, name, "currentKey should be equal to name") XCTAssertEqual(sut.currentValue, value, "Initilized object should contain a string as value property") } func test_init_validStringParamaters_shouldStoreValidProperties() { // Given let name = "w" let value = "* 2" let valueResult = "*_2" // When sut = CLDConditionExpression(value: "\(name) \(value)") // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, name, "currentKey should be equal to name") XCTAssertEqual(sut.currentValue, valueResult, "Initilized object should contain a string as valueResult property") } // MARK: - test class methods func test_width_shouldStoreValidKey() { // Given let expectedResult = "w" // When sut = CLDConditionExpression.width() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_height_shouldStoreValidKey() { // Given let expectedResult = "h" // When sut = CLDConditionExpression.height() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_initialWidth_shouldStoreValidKey() { // Given let expectedResult = "iw" // When sut = CLDConditionExpression.initialWidth() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_initialHeight_shouldStoreValidKey() { // Given let expectedResult = "ih" // When sut = CLDConditionExpression.initialHeight() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_aspectRatio_shouldStoreValidKey() { // Given let expectedResult = "ar" // When sut = CLDConditionExpression.aspectRatio() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_initialAspectRatio_shouldStoreValidKey() { // Given let expectedResult = "iar" // When sut = CLDConditionExpression.initialAspectRatio() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_pageCount_shouldStoreValidKey() { // Given let expectedResult = "pc" // When sut = CLDConditionExpression.pageCount() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_faceCount_shouldStoreValidKey() { // Given let expectedResult = "fc" // When sut = CLDConditionExpression.faceCount() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_tags_shouldStoreValidKey() { // Given let expectedResult = "tags" // When sut = CLDConditionExpression.tags() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_pageXOffset_shouldStoreValidKey() { // Given let expectedResult = "px" // When sut = CLDConditionExpression.pageXOffset() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_pageYOffset_shouldStoreValidKey() { // Given let expectedResult = "py" // When sut = CLDConditionExpression.pageYOffset() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_illustrationScore_shouldStoreValidKey() { // Given let expectedResult = "ils" // When sut = CLDConditionExpression.illustrationScore() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_currentPageIndex_shouldStoreValidKey() { // Given let expectedResult = "cp" // When sut = CLDConditionExpression.currentPageIndex() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } // MARK: - test instance method // MARK: - add func test_addInt_shouldStoreValidFirstValue() { // Given let value = 20 let expectedValueResult = "add_20" // When sut = CLDConditionExpression.width().add(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_addInt_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = 20 let expectedValueResult = "width_add_20" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").add(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_addFloat_shouldStoreValidFirstValue() { // Given let value = Float(30.3) let expectedValueResult = "add_30.3" // When sut = CLDConditionExpression.width().add(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_addFloat_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expectedValueResult = "width_add_30.3" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").add(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - subtract func test_subtractInt_shouldStoreValidFirstValue() { // Given let value = 20 let expectedValueResult = "sub_20" // When sut = CLDConditionExpression.width().subtract(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_subtractInt_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = 20 let expectedValueResult = "width_sub_20" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").subtract(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_subtractFloat_shouldStoreValidFirstValue() { // Given let value = Float(30.3) let expectedValueResult = "sub_30.3" // When sut = CLDConditionExpression.width().subtract(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_subtractFloat_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expectedValueResult = "width_sub_30.3" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").subtract(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - multiple func test_multipleInt_shouldStoreValidFirstValue() { // Given let value = 20 let expectedValueResult = "mul_20" // When sut = CLDConditionExpression.width().multiple(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_multipleInt_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = 20 let expectedValueResult = "width_mul_20" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").multiple(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_multipleFloat_shouldStoreValidFirstValue() { // Given let value = Float(30.3) let expectedValueResult = "mul_30.3" // When sut = CLDConditionExpression.width().multiple(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_multipleFloat_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expectedValueResult = "width_mul_30.3" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").multiple(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - divide func test_divideInt_shouldStoreValidFirstValue() { // Given let value = 20 let expectedValueResult = "div_20" // When sut = CLDConditionExpression.width().divide(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_divideInt_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = 20 let expectedValueResult = "width_div_20" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_divideFloat_shouldStoreValidFirstValue() { // Given let value = Float(30.3) let expectedValueResult = "div_30.3" // When sut = CLDConditionExpression.width().divide(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_divideFloat_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expectedValueResult = "width_div_30.3" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - power func test_powerInt_shouldStoreValidFirstValue() { // Given let value = 20 let expectedValueResult = "pow_20" // When sut = CLDConditionExpression.width().power(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_powerInt_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = 20 let expectedValueResult = "width_pow_20" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").power(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_powerFloat_shouldStoreValidFirstValue() { // Given let value = Float(30.3) let expectedValueResult = "pow_30.3" // When sut = CLDConditionExpression.width().power(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_powerFloat_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expectedValueResult = "width_pow_30.3" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").power(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_powerFloat_shouldAppendValidValueToVariable() { // $big_$small_pow_1.5/c_fill,w_$big,h_$small_add_20 // Given let initialValue = "$big $small ^ 1.5" let expectedValueResult = "$big_$small_pow_1.5" // When sut = CLDConditionExpression(value: initialValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_powerString_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = "30.3" let expectedValueResult = "width_pow_30.3" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").power(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - equal func test_equalFloat_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let equalValue = Float(10.1) let expectedValueResult = "width_div_30.3_eq_10.1" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).equal(to: equalValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_equalString_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let equalValue = "initialHeight" let expectedValueResult = "width_div_30.3_eq_initialHeight" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).equal(to: equalValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_equalBaseExpression_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let equalValue = CLDExpression.initialHeight() let expectedValueResult = "width_div_30.3_eq_ih" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).equal(to: equalValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_equalExpression_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expressionValue = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "width_div_30.3_eq_ih_add_20" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).equal(to: expressionValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - notEqual func test_notEqual_floatValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let unequalValue = Float(10.1) let expectedValueResult = "width_div_30.3_ne_10.1" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).notEqual(to: unequalValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_notEqual_stringValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let unequalValue = "initialHeight" let expectedValueResult = "width_div_30.3_ne_initialHeight" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).notEqual(to: unequalValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_notEqual_baseExpressionValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let unequalValue = CLDExpression.initialHeight() let expectedValueResult = "width_div_30.3_ne_ih" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).notEqual(to: unequalValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_notEqual_expressionValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expressionValue = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "width_div_30.3_ne_ih_add_20" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).notEqual(to: expressionValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - less func test_less_stringValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let lessValue = "initialHeight" let expectedValueResult = "width_div_30.3_lt_initialHeight" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).less(then: lessValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_less_intValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let lessValue = 100 let expectedValueResult = "width_div_30.3_lt_100" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).less(then: lessValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_less_floatValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let lessValue: Float = 100.1 let expectedValueResult = "width_div_30.3_lt_100.1" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).less(then: lessValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_less_baseExpressionValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let lessValue = CLDExpression.initialHeight() let expectedValueResult = "width_div_30.3_lt_ih" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).less(then: lessValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_less_expressionValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expressionValue = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "width_div_30.3_lt_ih_add_20" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).less(then: expressionValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - greater func test_greater_stringValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let greaterValue = "initialHeight" let expectedValueResult = "width_div_30.3_gt_initialHeight" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).greater(then: greaterValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_greater_intValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let greaterValue = 100 let expectedValueResult = "width_div_30.3_gt_100" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).greater(then: greaterValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_greater_floatValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let greaterValue: Float = 100.1 let expectedValueResult = "width_div_30.3_gt_100.1" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).greater(then: greaterValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_greater_baseExpressionValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let greaterValue = CLDExpression.initialHeight() let expectedValueResult = "width_div_30.3_gt_ih" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).greater(then: greaterValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_greater_expressionValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expressionValue = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "width_div_30.3_gt_ih_add_20" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).greater(then: expressionValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - lessOrEqual func test_lessOrEqual_stringValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let lessOrEqualValue = "initialHeight" let expectedValueResult = "width_div_30.3_lte_initialHeight" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).lessOrEqual(to: lessOrEqualValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_lessOrEqual_intValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let lessOrEqualValue = 100 let expectedValueResult = "width_div_30.3_lte_100" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).lessOrEqual(to: lessOrEqualValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_lessOrEqual_floatValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let lessOrEqualValue: Float = 100.1 let expectedValueResult = "width_div_30.3_lte_100.1" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).lessOrEqual(to: lessOrEqualValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_lessOrEqual_baseExpressionValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let lessOrEqualValue = CLDExpression.initialHeight() let expectedValueResult = "width_div_30.3_lte_ih" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).lessOrEqual(to: lessOrEqualValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_lessOrEqual_expressionValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expressionValue = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "width_div_30.3_lte_ih_add_20" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).lessOrEqual(to: expressionValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - greaterOrEqual func test_greaterOrEqual_stringValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let greaterOrEqualValue = "initialHeight" let expectedValueResult = "width_div_30.3_gte_initialHeight" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).greaterOrEqual(to: greaterOrEqualValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_greaterOrEqual_intValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let greaterOrEqualValue = 100 let expectedValueResult = "width_div_30.3_gte_100" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).greaterOrEqual(to: greaterOrEqualValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_greaterOrEqual_floatValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let greaterOrEqualValue: Float = 100.1 let expectedValueResult = "width_div_30.3_gte_100.1" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).greaterOrEqual(to: greaterOrEqualValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_greaterOrEqual_baseExpressionValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let greaterOrEqualValue = CLDExpression.initialHeight() let expectedValueResult = "width_div_30.3_gte_ih" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).greaterOrEqual(to: greaterOrEqualValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_greaterOrEqual_expressionValue_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expressionValue = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "width_div_30.3_gte_ih_add_20" // When sut = CLDConditionExpression(value: "\(name) \(initialValue)").divide(by: value).greaterOrEqual(to: expressionValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - or func test_or_shouldAppendValidValue() { // Given let value = "width > 200" let expectedValueResult = ">_200_or" // When sut = CLDConditionExpression(value: value).or() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - and func test_and_shouldAppendValidValue() { // Given let value = "width > 200" let expectedValueResult = ">_200_and" // When sut = CLDConditionExpression(value: value).and() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - value func test_value_empty_shouldNotAppendValue() { // Given let value = "" let expectedValueResult = "" // When sut = CLDConditionExpression().value(value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_valueOnEmptyKey_validStringParamaters_shouldAppendValidValue() { // Given let value = "width > 200" let expectedValueResult = ">_200" // When sut = CLDConditionExpression().value(value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_valueValidKey_validStringParamaters_shouldAppendValidValue() { // Given let value = "> 200" let initialValue = "width" let expectedValueResult = ">_200" // When sut = CLDConditionExpression(value: initialValue).value(value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_value_expressionValue_shouldAppendValidValue() { // Given let initialValue = "width" let expressionValue = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "ih_add_20" // When sut = CLDConditionExpression(value: initialValue).value(expressionValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_valueAfterAnd_shouldAppendValidValue() { // Given let initialValue = "width > 200" let value = "height > 200" let expectedValueResult = ">_200_and_h_gt_200" // When sut = CLDConditionExpression(value: initialValue).and().value(value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - inside func test_inside_empty_shouldNotAppendValue() { // Given let tag = "!myTag2!" let expectedValueResult = "" // When sut = CLDConditionExpression().value(tag).inside("") // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_inside_validStringParamaters_shouldAppendValidValue() { // Given let tag = "!myTag2!" let expectedValueResult = "in_tags" // When sut = CLDConditionExpression().value(tag).inside("tags") // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_inside_expressionValue_shouldAppendValidValue() { // Given let tag = "!myTag2!" let expressionValue = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "in_ih_add_20" // When sut = CLDConditionExpression().value(tag).inside(expressionValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - notInside func test_notInside_empty_shouldNotAppendValue() { // Given let tag = "!myTag2!" let expectedValueResult = "" // When sut = CLDConditionExpression().value(tag).notInside("") // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_notInside_validStringParamaters_shouldAppendValidValue() { // Given let tag = "!myTag2!" let expectedValueResult = "nin_tags" // When sut = CLDConditionExpression().value(tag).notInside("tags") // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_notInside_expressionValue_shouldAppendValidValue() { // Given let tag = "!myTag2!" let expressionValue = CLDExpression.initialHeight().add(by: 20) let expectedValueResult = "nin_ih_add_20" // When sut = CLDConditionExpression().value(tag).notInside(expressionValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - test asString() func test_asString_emptyInputParamaters_shouldReturnEmptyString() { // Given let value = String() let expectedResult = String() // When sut = CLDConditionExpression(value: value) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString on an empty CLDConditionExpression, should return an empty string") } func test_asString_validParamaters_shouldReturnValidString() { // Given let name = "initialWidth" let value = "* 200 / faceCount" let expectedResult = "iw_mul_200_div_fc" // When sut = CLDConditionExpression(value: "\(name) \(value)") let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString on a CLDConditionExpression, should return a string") } func test_asString_emptyKey_shouldReturnEmptyString() { // Given let name = "initialWidth" let value = "* 200 / faceCount" let expectedValue = "*_200_/_faceCount" let expectedResult = "" // When sut = CLDConditionExpression(value: "\(name) \(value)") sut.currentKey = "" let actualResult = sut.asString() let actualValue = sut.currentValue // Then XCTAssertEqual(actualValue, expectedValue, "Calling asString on a CLDConditionExpression, should return a string") XCTAssertEqual(actualResult, expectedResult, "Calling asString on a CLDConditionExpression, should return an empty string") } func test_asString_extraSpacesStringParamaters_shouldReturnValidString() { // Given let name = "initialWidth" let value = "* 200" let expectedResult = "iw_mul_200" // When sut = CLDConditionExpression(value: "\(name) \(value)") let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString, should remove extra dashes/spaces") } // MARK: - test asParams() func test_asParams_emptyInputParamaters_shouldReturnEmptyString() { // Given let value = String() let expectedResult = [String:String]() // When sut = CLDConditionExpression(value: value) let actualResult = sut.asParams() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asParams, should build a paramater representation") } func test_asParams_validParamaters_shouldReturnValidString() { // Given let name = "initialWidth" let value = "* 200 / faceCount" let expectedResult = ["iw":"mul_200_div_fc"] // When sut = CLDConditionExpression(value: "\(name) \(value)") let actualResult = sut.asParams() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asParams, should build a paramater representation") } func test_asParam_emptyKey_shouldReturnEmptyString() { // Given let name = "initialWidth" let value = "* 200 / faceCount" let expectedValue = "*_200_/_faceCount" let expectedResult = [String:String]() // When sut = CLDConditionExpression(value: "\(name) \(value)") sut.currentKey = "" let actualResult = sut.asParams() let actualValue = sut.currentValue // Then XCTAssertEqual(actualValue, expectedValue, "Calling asString on a CLDConditionExpression, should return a string") XCTAssertEqual(actualResult, expectedResult, "Calling asString on a CLDConditionExpression, should return an empty string") } func test_asParams_extraSpacesStringParamaters_shouldReturnValidString() { // Given let name = "initialWidth" let value = "* 200" let expectedResult = ["iw":"mul_200"] // When sut = CLDConditionExpression(value: "\(name) \(value)") let actualResult = sut.asParams() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString, should remove extra dashes/spaces") } } ================================================ FILE: Example/Tests/TransformationTests/CLDExpressionTests/CLDExpressionTests.m ================================================ // // ObjcCLDExpressionTests.m // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "ObjcBaseTestCase.h" @interface ObjcCLDExpressionTests : ObjcBaseTestCase @end @implementation ObjcCLDExpressionTests CLDExpression *expSut; // MARK: - setup and teardown - (void)setUp { [super setUp]; } - (void)tearDown { [super tearDown]; expSut = nil; } // MARK: - creatingExpression - (void)test_creatingExpression_widthAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"w_add_30"; // When expSut = [CLDExpression width]; [expSut addByInt: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_heightSubtract_shouldCreateValidExpression { // Given float input = 30.3; NSString* expectedResult = @"h_sub_30.3"; // When expSut = [CLDExpression height]; [expSut subtractByFloat: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_initialWidthMultiple_shouldCreateValidExpression { // Given NSString* input = @"30"; NSString* expectedResult = @"iw_mul_30"; // When expSut = [CLDExpression initialWidth]; [expSut multipleByString: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_initialHeightDivide_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"ih_div_30"; // When expSut = [CLDExpression initialHeight]; [expSut divideByInt: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_aspectRatioPower_shouldCreateValidExpression { // Given float input = 30.3; NSString* expectedResult = @"ar_pow_30.3"; // When expSut = [CLDExpression aspectRatio]; [expSut powerByFloat: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_initialAspectRatioAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"iar_add_30"; // When expSut = [CLDExpression initialAspectRatio]; [expSut addByInt: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_pageCountAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"pc_add_30"; // When expSut = [CLDExpression pageCount]; [expSut addByInt: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_faceCountAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"fc_add_30"; // When expSut = [CLDExpression faceCount]; [expSut addByInt: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_tagsAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"tags_add_30"; // When expSut = [CLDExpression tags]; [expSut addByInt: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_pageXOffsetAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"px_add_30"; // When expSut = [CLDExpression pageXOffset]; [expSut addByInt: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_pageYOffsetAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"py_add_30"; // When expSut = [CLDExpression pageYOffset]; [expSut addByInt: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_illustrationScoreAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"ils_add_30"; // When expSut = [CLDExpression illustrationScore]; [expSut addByInt: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_currentPageIndexAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"cp_add_30"; // When expSut = [CLDExpression currentPageIndex]; [expSut addByInt: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_durationAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"du_add_30"; // When expSut = [CLDExpression duration]; [expSut addByInt: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } - (void)test_creatingExpression_initialDurationAdd_shouldCreateValidExpression { // Given int input = 30; NSString* expectedResult = @"idu_add_30"; // When expSut = [CLDExpression initialDuration]; [expSut addByInt: input]; NSString* actualResult = [expSut asString]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } // MARK: - asParams - (void)test_asParams_initialDurationAdd_shouldCreateValidExpression { // Given int input = 30; NSDictionary* expectedResult = @{@"idu":@"add_30"}; // When expSut = [CLDExpression initialDuration]; [expSut addByInt: input]; NSDictionary* actualResult = [expSut asParams]; // Then XCTAssertNotNil(actualResult, "Initilized object should contain a none nil name property"); XCTAssertTrue(actualResult.count == 1 && expectedResult.count == 1, "Calling get asString should return the expect string"); XCTAssertTrue([actualResult.allKeys[0] isEqualToString: expectedResult.allKeys[0]], "Calling get asString should return the expect string"); XCTAssertTrue([actualResult.allValues[0] isEqualToString: expectedResult.allValues[0]], "Calling get asString should return the expect string"); } @end ================================================ FILE: Example/Tests/TransformationTests/CLDExpressionTests/CLDExpressionTests.swift ================================================ // // CLDExpressionTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class CLDExpressionTests: BaseTestCase { var sut : CLDExpression! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - test initilization methods - empty func test_init_emptyInputParamaters_shouldStoreEmptyProperties() { // Given let name = String() // When sut = CLDExpression() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil currentKey property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil currentValue property") XCTAssertEqual(sut.currentKey, name, "Name property should be equal to name") XCTAssertEqual(sut.currentValue, String(), "Initilized object should contain an empty string as currentValue property") } // MARK: - test initilization methods - value func test_init_emptyNameParamater_shouldStoreEmptyNameProperty() { // Given let name = String() let value = "alue" // When sut = CLDExpression(value: "\(name) \(value)") // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil currentKey property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil currentValue property") XCTAssertEqual(sut.currentValue, value, "Initilized object should contain an empty string as value property") } func test_init_validStringParamatersAndNoNamePrefix_shouldStoreValidProperties() { // Given let name = "name" let value = "alue" // When sut = CLDExpression(value: "\(name) \(value)") // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, name, "currentKey should be equal to name") XCTAssertEqual(sut.currentValue, value, "Initilized object should contain a string as value property") } func test_init_validStringParamaters_shouldStoreValidProperties() { // Given let name = "w" let value = "* 2" let valueResult = "*_2" // When sut = CLDExpression(value: "\(name) \(value)") // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, name, "currentKey should be equal to name") XCTAssertEqual(sut.currentValue, valueResult, "Initilized object should contain a string as valueResult property") } // MARK: - test class methods func test_width_shouldStoreValidKey() { // Given let expectedResult = "w" // When sut = CLDExpression.width() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_height_shouldStoreValidKey() { // Given let expectedResult = "h" // When sut = CLDExpression.height() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_initialWidth_shouldStoreValidKey() { // Given let expectedResult = "iw" // When sut = CLDExpression.initialWidth() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_initialHeight_shouldStoreValidKey() { // Given let expectedResult = "ih" // When sut = CLDExpression.initialHeight() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_aspectRatio_shouldStoreValidKey() { // Given let expectedResult = "ar" // When sut = CLDExpression.aspectRatio() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_initialAspectRatio_shouldStoreValidKey() { // Given let expectedResult = "iar" // When sut = CLDExpression.initialAspectRatio() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_pageCount_shouldStoreValidKey() { // Given let expectedResult = "pc" // When sut = CLDExpression.pageCount() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_faceCount_shouldStoreValidKey() { // Given let expectedResult = "fc" // When sut = CLDExpression.faceCount() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_tags_shouldStoreValidKey() { // Given let expectedResult = "tags" // When sut = CLDExpression.tags() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_pageXOffset_shouldStoreValidKey() { // Given let expectedResult = "px" // When sut = CLDExpression.pageXOffset() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_pageYOffset_shouldStoreValidKey() { // Given let expectedResult = "py" // When sut = CLDExpression.pageYOffset() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_illustrationScore_shouldStoreValidKey() { // Given let expectedResult = "ils" // When sut = CLDExpression.illustrationScore() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } func test_currentPageIndex_shouldStoreValidKey() { // Given let expectedResult = "cp" // When sut = CLDExpression.currentPageIndex() // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentKey, expectedResult, "currentKey should be equal to expectedResult") } // MARK: - test instance method func test_addInt_shouldStoreValidFirstValue() { // Given let value = 20 let expectedValueResult = "add_20" // When sut = CLDExpression.width().add(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_addInt_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = 20 let expectedValueResult = "width_add_20" // When sut = CLDExpression(value: "\(name) \(initialValue)").add(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_addFloat_shouldStoreValidFirstValue() { // Given let value = Float(30.3) let expectedValueResult = "add_30.3" // When sut = CLDExpression.width().add(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_addFloat_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expectedValueResult = "width_add_30.3" // When sut = CLDExpression(value: "\(name) \(initialValue)").add(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_addString_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = "30.3" let expectedValueResult = "width_add_30.3" // When sut = CLDExpression(value: "\(name) \(initialValue)").add(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_subtractInt_shouldStoreValidFirstValue() { // Given let value = 20 let expectedValueResult = "sub_20" // When sut = CLDExpression.width().subtract(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_subtractInt_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = 20 let expectedValueResult = "width_sub_20" // When sut = CLDExpression(value: "\(name) \(initialValue)").subtract(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_subtractFloat_shouldStoreValidFirstValue() { // Given let value = Float(30.3) let expectedValueResult = "sub_30.3" // When sut = CLDExpression.width().subtract(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_subtractFloat_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expectedValueResult = "width_sub_30.3" // When sut = CLDExpression(value: "\(name) \(initialValue)").subtract(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_subtractString_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = "30.3" let expectedValueResult = "width_sub_30.3" // When sut = CLDExpression(value: "\(name) \(initialValue)").subtract(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_multipleInt_shouldStoreValidFirstValue() { // Given let value = 20 let expectedValueResult = "mul_20" // When sut = CLDExpression.width().multiple(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_multipleInt_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = 20 let expectedValueResult = "width_mul_20" // When sut = CLDExpression(value: "\(name) \(initialValue)").multiple(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_multipleFloat_shouldStoreValidFirstValue() { // Given let value = Float(30.3) let expectedValueResult = "mul_30.3" // When sut = CLDExpression.width().multiple(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_multipleFloat_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expectedValueResult = "width_mul_30.3" // When sut = CLDExpression(value: "\(name) \(initialValue)").multiple(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_multipleString_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = "30.3" let expectedValueResult = "width_mul_30.3" // When sut = CLDExpression(value: "\(name) \(initialValue)").multiple(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_divideInt_shouldStoreValidFirstValue() { // Given let value = 20 let expectedValueResult = "div_20" // When sut = CLDExpression.width().divide(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_divideInt_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = 20 let expectedValueResult = "width_div_20" // When sut = CLDExpression(value: "\(name) \(initialValue)").divide(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_divideFloat_shouldStoreValidFirstValue() { // Given let value = Float(30.3) let expectedValueResult = "div_30.3" // When sut = CLDExpression.width().divide(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_divideFloat_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expectedValueResult = "width_div_30.3" // When sut = CLDExpression(value: "\(name) \(initialValue)").divide(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_divideString_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = "30.3" let expectedValueResult = "width_div_30.3" // When sut = CLDExpression(value: "\(name) \(initialValue)").divide(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - power func test_powerInt_shouldStoreValidFirstValue() { // Given let value = 20 let expectedValueResult = "pow_20" // When sut = CLDExpression.width().power(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_powerInt_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = 20 let expectedValueResult = "width_pow_20" // When sut = CLDExpression(value: "\(name) \(initialValue)").power(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_powerFloat_shouldStoreValidFirstValue() { // Given let value = Float(30.3) let expectedValueResult = "pow_30.3" // When sut = CLDExpression.width().power(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_powerFloat_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = Float(30.3) let expectedValueResult = "width_pow_30.3" // When sut = CLDExpression(value: "\(name) \(initialValue)").power(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_powerFloat_shouldAppendValidValueToVariable() { // Given let initialValue = "$big $small ^ 1.5" let expectedValueResult = "$big_$small_pow_1.5" // When sut = CLDExpression(value: initialValue) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.asString(), expectedValueResult, "currentValue should be equal to expectedValueResult") } func test_powerString_shouldAppendValidValue() { // Given let name = "height" let initialValue = "width" let value = "30.3" let expectedValueResult = "width_pow_30.3" // When sut = CLDExpression(value: "\(name) \(initialValue)").power(by: value) // Then XCTAssertNotNil(sut.currentKey, "Initilized object should contain a none nil key property") XCTAssertNotNil(sut.currentValue, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.currentValue, expectedValueResult, "currentValue should be equal to expectedValueResult") } // MARK: - test asString() func test_asString_emptyInputParamaters_shouldReturnEmptyString() { // Given let value = String() let expectedResult = String() // When sut = CLDExpression(value: value) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString on an empty CLDExpression, should return an empty string") } func test_asString_validParamaters_shouldReturnValidString() { // Given let name = "initialWidth" let value = "* 200 / faceCount" let expectedResult = "iw_mul_200_div_fc" // When sut = CLDExpression(value: "\(name) \(value)") let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString on a CLDExpression, should return a string") } func test_asString_extraSpacesStringParamaters_shouldReturnValidString() { // Given let name = "initialWidth" let value = "* 200" let expectedResult = "iw_mul_200" // When sut = CLDExpression(value: "\(name) \(value)") let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString, should remove extra dashes/spaces") } func test_asString_complexStringValue_shouldReturnValidString() { // Given let name = "initialWidth" let value = "200 * $width" let expectedResult = "iw_200_mul_$width" // When sut = CLDExpression(value: "\(name) \(value)") let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString, should remove extra dashes/spaces") } // MARK: - test asParams() func test_asParams_emptyInputParamaters_shouldReturnEmptyString() { // Given let value = String() let expectedResult = [String:String]() // When sut = CLDExpression(value: value) let actualResult = sut.asParams() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asParams, should build a paramater representation") } func test_asParams_validParamaters_shouldReturnValidString() { // Given let name = "initialWidth" let value = "* 200 / faceCount" let expectedResult = ["iw":"mul_200_div_fc"] // When sut = CLDExpression(value: "\(name) \(value)") let actualResult = sut.asParams() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asParams, should build a paramater representation") } func test_asParams_extraSpacesStringParamaters_shouldReturnValidString() { // Given let name = "initialWidth" let value = "* 200" let expectedResult = ["iw":"mul_200"] // When sut = CLDExpression(value: "\(name) \(value)") let actualResult = sut.asParams() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asParams, should remove extra dashes/spaces") } func test_powerString_shouldAppendValidValuex() { // Given let name = "$________height" let initialValue = "$_____width" let value = "30.3" let expectedValueResult = "$_height_$_width_pow_30.3" // When sut = CLDExpression(value: "\(name) \(initialValue)").power(by: value) // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionNumber() { // Given let name = 10 let expectedValueResult = "10" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionSingleSpace_underscore() { // Given let name = " " let expectedValueResult = "_" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionBlankString_underscore() { // Given let name = " " let expectedValueResult = "_" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionUnderscore_underscore() { // Given let name = "_" let expectedValueResult = "_" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionUnderscores_underscore() { // Given let name = "___" let expectedValueResult = "_" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionUnderscoresAndSpaces_underscore() { // Given let name = " _ __ _" let expectedValueResult = "_" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionArbitraryText_isNotAffected() { // Given let name = "foobar" let expectedValueResult = "foobar" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDoubleAmpersand_replacedWithAndOperator() { // Given let name = "foo && bar" let expectedValueResult = "foo_and_bar" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDoubleAmpersandWithNoSpaceAtEnd_isNotAffected() { // Given let name = "foo&&bar" let expectedValueResult = "foo&&bar" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionWidth_recognizedAsVariableAndReplacedWithW() { // Given let name = "width" let expectedValueResult = "w" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionInitialAspectRatio_recognizedAsVariableAndReplacedWithW() { // Given let name = "width" let expectedValueResult = "w" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDollarWidth_recognizedAsUserVariableAndNotAffected() { // Given let name = "$width" let expectedValueResult = "$width" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDollarInitialAspectRatio_recognizedAsUserVariableAndAsVariableReplacedWithAr() { // Given let name = "$initial_aspect_ratio" let expectedValueResult = "$initial_ar" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDollarMyWidth_recognizedAsUserVariableAndNotAffected() { // Given let name = "$mywidth" let expectedValueResult = "$mywidth" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDollarWidthWidth_recognizedAsUserVariableAndNotAffected() { // Given let name = "$widthwidth" let expectedValueResult = "$widthwidth" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDollarUnderscoreWidth_recognizedAsUserVariableAndNotAffected() { // Given let name = "$_width" let expectedValueResult = "$_width" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDollarUnderscoreX2Width_recognizedAsUserVariableAndNotAffected() { // Given let name = "$__width" let expectedValueResult = "$_width" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDollarX2Width_recognizedAsUserVariableAndNotAffected() { // Given let name = "$\\$width" let expectedValueResult = "$\\$width" // When sut = CLDExpression(value: "\(name)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDoesntReplaceVariable_1() { // Given let name = "$height" let value = "100" let expectedValueResult = "$height_100" // When sut = CLDExpression(value: "\(name) \(value)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDoesntReplaceVariable_2() { // Given let name = "$heightt" let value = "100" let expectedValueResult = "$heightt_100" // When sut = CLDExpression(value: "\(name) \(value)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDoesntReplaceVariable_3() { // Given let name = "$\\$height" let value = "100" let expectedValueResult = "$\\$height_100" // When sut = CLDExpression(value: "\(name) \(value)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDoesntReplaceVariable_4() { // Given let name = "$heightmy" let value = "100" let expectedValueResult = "$heightmy_100" // When sut = CLDExpression(value: "\(name) \(value)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDoesntReplaceVariable_5() { // Given let name = "$myheight" let value = "100" let expectedValueResult = "$myheight_100" // When sut = CLDExpression(value: "\(name) \(value)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDoesntReplaceVariable_6() { // Given let name = "$heightheight" let value = "100" let expectedValueResult = "$heightheight_100" // When sut = CLDExpression(value: "\(name) \(value)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDoesntReplaceVariable_7() { // Given let name = "$theheight" let value = "100" let expectedValueResult = "$theheight_100" // When sut = CLDExpression(value: "\(name) \(value)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } func testExpressionDoesntReplaceVariable_8() { // Given let name = "$________height" let value = "100" let expectedValueResult = "$_height_100" // When sut = CLDExpression(value: "\(name) \(value)") // Then XCTAssertEqual(sut.asString(), expectedValueResult, "string should be equal to expectedValueResult") } } ================================================ FILE: Example/Tests/TransformationTests/CLDTransformationTests/CLDTransformationBaselineTests.swift ================================================ // // CLDTransformationBaselineTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest // MARK: - CLDTransformationBaselineTests class CLDTransformationBaselineTests: BaseTestCase { var sut : CLDTransformation! // MARK: - setup and teardown override func setUp() { super.setUp() sut = CLDTransformation() } override func tearDown() { sut = nil super.tearDown() } // MARK: - ifCondition with String func test_ifCondition_emptyStringProperty_shouldNotStoreNewParam() { // Given let stringInput = String() // When sut.ifCondition(stringInput) let actualResult = sut.ifParam! // Then XCTAssertTrue(actualResult.isEmpty, "Empty expression should not be stored in params") } // MARK: - func test_WithLiteral() { var transformation = CLDTransformation().ifCondition("w_lt_200").setCrop("fill").setHeight(120).setWidth(80) var sTransform = transformation.asString()! XCTAssertEqual(sTransform.firstIndex(of: "if"), 0, "should include the if parameter as the first component in the transformation string") XCTAssertEqual("if_w_lt_200,c_fill,h_120,w_80", sTransform, "should be proper transformation string") transformation = CLDTransformation().setCrop("fill").setHeight(120).ifCondition("w_lt_200").setWidth(80) sTransform = transformation.asString()! XCTAssertEqual(sTransform.firstIndex(of: "if"), 0, "should include the if parameter as the first component in the transformation string") XCTAssertEqual("if_w_lt_200,c_fill,h_120,w_80", sTransform, "components should be in proper order") transformation = CLDTransformation().ifCondition("w_lt_200").setCrop("fill").setHeight(120).setWidth(80) .chain().ifCondition("w_gt_400").setCrop("fit").setHeight(150).setWidth(150) .chain().setEffect("sepia") sTransform = transformation.asString()! XCTAssertEqual("if_w_lt_200,c_fill,h_120,w_80/if_w_gt_400,c_fit,h_150,w_150/e_sepia", sTransform, "should allow multiple conditions when chaining transformations") } func test_LiteralWithSpaces() { let transformation = CLDTransformation().ifCondition("w < 200").setCrop("fill").setHeight(120).setWidth(80) let sTransform = transformation.asString()! XCTAssertEqual("if_w_lt_200,c_fill,h_120,w_80", sTransform, "should translate operators") } func test_EndIf() { let transformation = CLDTransformation().ifCondition("w_lt_200").setCrop("fill").setHeight(120).setWidth(80).setEffect("sharpen") .chain().setEffect("brightness", param: "50") .chain().setEffect("shadow").setColor("red") .endIf() let sTransform = transformation.asString()! XCTAssertTrue(sTransform.hasSuffix("if_end"), "should include the if_end as the last parameter in its component") XCTAssertEqual("if_w_lt_200/c_fill,e_sharpen,h_120,w_80/e_brightness:50/co_red,e_shadow/if_end", sTransform, "should be proper transformation string") } func test_IfElse() { var transformation = CLDTransformation(input: [ CLDTransformation().ifCondition("w_lt_200").setCrop("fill").setHeight(120).setWidth(80), CLDTransformation().ifElse().setCrop("fill").setHeight(90).setWidth(100) ]) var sTransform = transformation.asString()! XCTAssertEqual("if_w_lt_200,c_fill,h_120,w_80/if_else,c_fill,h_90,w_100", sTransform, "should support if_else with transformation parameters") transformation = CLDTransformation(input: [ CLDTransformation().ifCondition("w_lt_200"), CLDTransformation().setCrop("fill").setHeight(120).setWidth(80), CLDTransformation().ifElse(), CLDTransformation().setCrop("fill").setHeight(90).setWidth(100) ]) sTransform = transformation.asString()! XCTAssertTrue(sTransform.contains("/if_else/"), "if_else should be without any transformation parameters") XCTAssertEqual("if_w_lt_200/c_fill,h_120,w_80/if_else/c_fill,h_90,w_100", sTransform, "should be proper transformation string") } func test_ChainedConditions() { var transformation = CLDTransformation().ifCondition().aspectRatio("gt", "3:4").then().setWidth(100).setCrop("scale") XCTAssertEqual("if_ar_gt_3:4,c_scale,w_100", transformation.asString()!, "passing an operator and a value adds a condition") transformation = CLDTransformation().ifCondition().aspectRatio("gt", "3:4").and().width("gt", 100).then().setWidth(50).setCrop("scale") XCTAssertEqual("if_ar_gt_3:4_and_w_gt_100,c_scale,w_50", transformation.asString()!, "should chaining condition with `and`") transformation = CLDTransformation().ifCondition().aspectRatio("gt", "3:4").and().width("gt", 100).or().width("gt", 200).then().setWidth(50).setCrop("scale") XCTAssertEqual("if_ar_gt_3:4_and_w_gt_100_or_w_gt_200,c_scale,w_50", transformation.asString()!, "should chain conditions with `or`") transformation = CLDTransformation().ifCondition().aspectRatio(">", "3:4").and().width("<=", 100).or().width("gt", 200).then().setWidth(50).setCrop("scale") XCTAssertEqual("if_ar_gt_3:4_and_w_lte_100_or_w_gt_200,c_scale,w_50", transformation.asString()!, "should translate operators") transformation = CLDTransformation().ifCondition().aspectRatio(">", "3:4").and().width("<=", 100).or().width(">", 200).then().setWidth(50).setCrop("scale") XCTAssertEqual("if_ar_gt_3:4_and_w_lte_100_or_w_gt_200,c_scale,w_50", transformation.asString()!, "should translate operators") transformation = CLDTransformation().ifCondition().aspectRatio(">=", "3:4").and().pageCount(">=", 100).or().pageCount("!=", 0).then().setWidth(50).setCrop("scale") XCTAssertEqual("if_ar_gte_3:4_and_pc_gte_100_or_pc_ne_0,c_scale,w_50", transformation.asString()!, "should translate operators") transformation = CLDTransformation().ifCondition().aspectRatio("gt", "3:4").and().initialHeight(">", 100).and().initialWidth("<", 500).then().setWidth(100).setCrop("scale") XCTAssertEqual("if_ar_gt_3:4_and_ih_gt_100_and_iw_lt_500,c_scale,w_100", transformation.asString()!, "passing an operator and a value adds a condition") transformation = CLDTransformation().ifCondition().initialDuration(">", 30).and().initialHeight(">", 100).and().initialWidth("<", 500).then().setWidth(100).setCrop("scale") XCTAssertEqual("if_idu_gt_30_and_ih_gt_100_and_iw_lt_500,c_scale,w_100", transformation.asString()!, "passing an operator and a value adds a condition") transformation = CLDTransformation().ifCondition().duration("<", 30).and().initialHeight(">", 100).and().initialWidth("<", 500).then().setWidth(100).setCrop("scale") XCTAssertEqual("if_du_lt_30_and_ih_gt_100_and_iw_lt_500,c_scale,w_100", transformation.asString()!, "passing an operator and a value adds a condition") } func test_ShouldSupportAndTranslateOperators() { let allOperators = "if_" + "w_eq_0_and" + "_h_ne_0_or" + "_ar_lt_0_and" + "_pc_gt_0_and" + "_fc_lte_0_and" + "_w_gte_0" + ",e_grayscale" XCTAssertEqual(allOperators, CLDTransformation().ifCondition() .width("=", 0).and() .height("!=", 0).or() .aspectRatio("<", "0").and() .pageCount(">", 0).and() .faceCount("<=", 0).and() .width(">=", 0) .then().setEffect("grayscale").asString()!, "should support and translate operators: '=', '!=', '<', '>', '<=', '>=', '&&', '||'") XCTAssertEqual(allOperators, CLDTransformation().ifCondition("w = 0 && height != 0 || aspectRatio < 0 and pageCount > 0 and faceCount <= 0 and width >= 0") .setEffect("grayscale") .asString()!) } func test_EndIf2() { var transformation = CLDTransformation().ifCondition().width("gt", 100).and().width("lt", 200).then().setWidth(50).setCrop("scale").endIf() XCTAssertEqual("if_w_gt_100_and_w_lt_200/c_scale,w_50/if_end", transformation.asString()!, "should serialize to 'if_end'") transformation = CLDTransformation().ifCondition().width("gt", 100).and().width("lt", 200).then().setWidth(50).setCrop("scale").endIf() XCTAssertEqual("if_w_gt_100_and_w_lt_200/c_scale,w_50/if_end", transformation.asString()!, "force the if clause to be chained") transformation = CLDTransformation().ifCondition().width("gt", 100).and().width("lt", 200).then().setWidth(50).setCrop("scale").ifElse().setWidth(100).setCrop("crop").endIf() XCTAssertEqual("if_w_gt_100_and_w_lt_200/c_scale,w_50/if_else/c_crop,w_100/if_end", transformation.asString()!, "force the if_else clause to be chained") } func test_TestExpressionOperators() { let transformationStr = "$foo_10,$foostr_!my:str:ing!/if_fc_gt_2_and" + "_pc_lt_300_or" + "_!myTag1!_in_tags_and" + "_!myTag2!_nin_tags_and" + "_w_gte_200_and" + "_h_eq_$foo_and" + "_w_ne_$foo_mul_2_and" + "_h_lt_$foo_or" + "_w_lte_500_and" + "_ils_lt_0_and" + "_cp_eq_10_and" + "_px_lt_300_and" + "_py_lt_300_and" + "_py_ne_400_and" + "_ar_gt_3:4_and" + "_iar_gt_3:4_and" + "_h_lt_iw_div_2_add_1_and" + "_w_lt_ih_sub_$foo_and" + "_du_eq_$foo_and" + "_du_ne_$foo_and" + "_du_lt_30_and" + "_du_lte_$foo_and" + "_du_gt_30_and" + "_du_gte_$foo_and" + "_idu_eq_$foo_and" + "_idu_ne_$foo_and" + "_idu_lt_30_and" + "_idu_lte_$foo_and" + "_idu_gt_30_and" + "_idu_gte_$foo" + "/c_scale,l_$foostr,w_$foo_mul_200_div_fc/if_end" let transformation = CLDTransformation() .setVariable("$foo", int: 10) .setVariable("$foostr", values: ["my", "str", "ing"]) .chain() .ifCondition( CLDConditionExpression.faceCount().greater(then: 2) .and().value(CLDConditionExpression.pageCount().less(then: 300)) .or("!myTag1!").inside(CLDConditionExpression.tags()) .and("!myTag2!").notInside(CLDConditionExpression.tags()) .and().value(CLDConditionExpression.width().greaterOrEqual(to: 200)) .and().value(CLDConditionExpression.height().equal(to: "$foo")) .and().value(CLDConditionExpression.width().notEqual(to: "$foo").multiple(by: 2)) .and().value(CLDConditionExpression.height().less(then: "$foo")) .or().value(CLDConditionExpression.width().lessOrEqual(to: 500)) .and().value(CLDConditionExpression.illustrationScore().less(then: 0)) .and().value(CLDConditionExpression.currentPageIndex().equal(to: 10)) .and().value(CLDConditionExpression.pageXOffset().less(then: 300)) .and().value(CLDConditionExpression.pageYOffset().less(then: 300)) .and().value(CLDConditionExpression.pageYOffset().notEqual(to: 400)) .and().value(CLDConditionExpression.aspectRatio().greater(then: "3:4")) .and().value(CLDConditionExpression.initialAspectRatio().greater(then: "3:4")) .and().value(CLDConditionExpression.height().less(then: CLDConditionExpression.initialWidth().divide(by: 2).add(by: 1))) .and().value(CLDConditionExpression.width().less(then: CLDConditionExpression.initialHeight().subtract(by: "$foo"))) .and().value(CLDConditionExpression.duration().equal(to: "$foo")) .and().value(CLDConditionExpression.duration().notEqual(to: "$foo")) .and().value(CLDConditionExpression.duration().less(then: 30)) .and().value(CLDConditionExpression.duration().lessOrEqual(to: "$foo")) .and().value(CLDConditionExpression.duration().greater(then: 30)) .and().value(CLDConditionExpression.duration().greaterOrEqual(to: "$foo")) .and().value(CLDConditionExpression.initialDuration().equal(to: "$foo")) .and().value(CLDConditionExpression.initialDuration().notEqual(to: "$foo")) .and().value(CLDConditionExpression.initialDuration().less(then: 30)) .and().value(CLDConditionExpression.initialDuration().lessOrEqual(to: "$foo")) .and().value(CLDConditionExpression.initialDuration().greater(then: 30)) .and().value(CLDConditionExpression.initialDuration().greaterOrEqual(to: "$foo"))) .setCrop("scale") .setWidth(CLDConditionExpression(value: "$foo * 200 / faceCount")) .setOverlay("$foostr") .endIf() XCTAssertEqual(transformationStr, transformation.asString()!) } func test_TestExpressionOperatorsWithValues() { let transformationStr = "$foo_10,$foostr_!my:str:ing!/if_fc_gt_2_and" + "_pc_lt_300_or" + "_!myTag1!_in_tags_and" + "_!myTag2!_nin_tags_and" + "_w_gte_200_and" + "_h_eq_$foo_and" + "_w_ne_$foo_mul_2_and" + "_h_lt_$foo_or" + "_w_lte_500_and" + "_ils_lt_0_and" + "_cp_eq_10_and" + "_px_lt_300_and" + "_py_lt_300_and" + "_py_ne_400_and" + "_ar_gt_3:4_and" + "_iar_gt_3:4_and" + "_h_lt_iw_div_2_add_1_and" + "_w_lt_ih_sub_$foo" + "/c_scale,l_$foostr,w_$foo_mul_200_div_fc/if_end" let transformation = CLDTransformation() .setVariable("$foo", int: 10) .setVariable("$foostr", values: ["my", "str", "ing"]) .chain() .ifCondition( CLDConditionExpression.faceCount().greater(then: 2) .and(CLDConditionExpression.pageCount().less(then: 300)) .or("!myTag1!").inside(CLDConditionExpression.tags()) .and("!myTag2!").notInside(CLDConditionExpression.tags()) .and(CLDConditionExpression.width().greaterOrEqual(to: 200)) .and(CLDConditionExpression.height().equal(to: "$foo")) .and(CLDConditionExpression.width().notEqual(to: "$foo").multiple(by: 2)) .and(CLDConditionExpression.height().less(then: "$foo")) .or(CLDConditionExpression.width().lessOrEqual(to: 500)) .and(CLDConditionExpression.illustrationScore().less(then: 0)) .and(CLDConditionExpression.currentPageIndex().equal(to: 10)) .and(CLDConditionExpression.pageXOffset().less(then: 300)) .and(CLDConditionExpression.pageYOffset().less(then: 300)) .and(CLDConditionExpression.pageYOffset().notEqual(to: 400)) .and(CLDConditionExpression.aspectRatio().greater(then: "3:4")) .and(CLDConditionExpression.initialAspectRatio().greater(then: "3:4")) .and(CLDConditionExpression.height().less(then: CLDConditionExpression.initialWidth().divide(by: 2).add(by: 1))) .and(CLDConditionExpression.width().less(then: CLDConditionExpression.initialHeight().subtract(by: "$foo"))) ) .setCrop("scale") .setWidth(CLDConditionExpression(value: "$foo * 200 / faceCount")) .setOverlay("$foostr") .endIf() XCTAssertEqual(transformationStr, transformation.asString()!) } func test_TestExpressionsClone() { let transformationStr = "if_pc_lt_300/c_scale/if_end" let expression = CLDConditionExpression.pageCount().less(then: 300) let transformation = CLDTransformation() .ifCondition(expression) .setCrop("scale") .endIf() let clone = transformation expression.greater(then: 2) XCTAssertEqual(transformationStr, clone.asString()!) } func test_TestShouldNotChangeVariableNamesWhenTheyNamedAfterKeyword() { let transformation = CLDTransformation() .setVariable("$width", int: 10) .chain() .setWidth("$width + 10 + width") let sTransform = transformation.asString()! XCTAssertEqual("$width_10/w_$width_add_10_add_w", sTransform) } func test_TestShouldSupportPowOperator() { let transformation = CLDTransformation().setVariables([ CLDVariable(name: "$small", value: 150), CLDVariable(name: "$big" , value: "$small ^ 1.5"), ]) let sTransform = transformation.asString()! XCTAssertEqual("$small_150,$big_$small_pow_1.5", sTransform) } } ================================================ FILE: Example/Tests/TransformationTests/CLDTransformationTests/CLDTransformationConditionsTests.swift ================================================ // // CLDTransformationConditionsTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest // MARK: - ConditionalCLDTransformationTests class CLDTransformationConditionsTests: BaseTestCase { var sut : CLDTransformation! // MARK: - setup and teardown override func setUp() { super.setUp() sut = CLDTransformation() } override func tearDown() { sut = nil super.tearDown() } // MARK: - ifCondition with String func test_ifCondition_emptyStringProperty_shouldNotStoreNewParam() { // Given let stringInput = String() // When sut.ifCondition(stringInput) let actualResult = sut.ifParam! // Then XCTAssertTrue(actualResult.isEmpty, "Empty expression should not be stored in params") } func test_ifCondition_spacedStringProperty_shouldStoreValidString() { // Given let stringInput = "w < 200" let expectedResult = "w_lt_200" // When sut.ifCondition(stringInput) let actualResult = sut.ifParam! // Then XCTAssertFalse(actualResult.isEmpty, "valid value should be stored") XCTAssertEqual(actualResult, expectedResult, "Calling for valid value should return the expected result") } // MARK: - ifCondition with CLDConditionExpression func test_ifCondition_emptyConditionProperty_shouldNotStoreNewParam() { // Given let conditionObject = CLDConditionExpression() // When sut.ifCondition(conditionObject) let actualResult = sut.ifParam! // Then XCTAssertTrue(actualResult.isEmpty, "Empty expression should not be stored in params") } func test_ifCondition_conditionProperty_shouldStoreValidString() { // Given let initialValue = "width < 200" let conditionObject = CLDConditionExpression(value: initialValue) let expectedResult = "w_lt_200" // When sut.ifCondition(conditionObject) let actualResult = sut.ifParam! // Then XCTAssertFalse(actualResult.isEmpty, "valid value should be stored") XCTAssertEqual(actualResult, expectedResult, "Calling for valid value should return the expected result") } // MARK: - asString() func test_ifConditionAsString_emptyConditionProperty_shouldStoreNil() { // Given let conditionObject = CLDConditionExpression() // When sut.ifCondition(conditionObject) let actualResult = sut.asString() // Then XCTAssertNil(actualResult, "Empty expression should not return from asString()") } func test_ifConditionAsString_emptyStringProperty_shouldStoreNil() { // Given let emptyString = String() // When sut.ifCondition(emptyString) let actualResult = sut.asString() // Then XCTAssertNil(actualResult, "Empty expression should not return from asString()") } func test_ifConditionAsString_spacedStringProperty_shouldReturnValidString() { // Given let stringInput = "w < 200" let expectedResult = "if_w_lt_200" // When sut.ifCondition(stringInput) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertEqual(actualResult, expectedResult, "Calling asString should return the expected result") } func test_ifConditionAsString_extraSpacedStringProperty_shouldReturnValidString() { // Given let stringInput = "w < 200" let expectedResult = "if_w_lt_200" // When sut.ifCondition(stringInput) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertEqual(actualResult, expectedResult, "Calling asString, should remove extra dashes/spaces") } func test_ifConditionAsString_stringProperty_shouldReturnValidString() { // Given let stringInput = "w_lt_200" let expectedResult = "if_w_lt_200" // When sut.ifCondition(stringInput) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertEqual(actualResult, expectedResult, "Calling asString should return the expected result") } func test_ifCondition_orderedStringConditionAndMultiProperties_shouldReturnValidString() { // Given let conditionStringInput = "w_lt_200" let expectedResult = "if_w_lt_200,c_fill,h_120,w_80" // When sut.ifCondition(conditionStringInput).setCrop(.fill).setHeight(120).setWidth(80) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertTrue(actualResult.hasPrefix("if"), "ifCondition should appear at the beginning of the trasformation string") XCTAssertEqual(actualResult, expectedResult, "Calling asString should return the expected result") } func test_ifCondition_unorderedStringConditionAndMultiProperties_shouldReturnValidOrderedString() { // Given let conditionStringInput = "w_lt_200" let expectedResult = "if_w_lt_200,c_fill,h_120,w_80" // When sut.setCrop(.fill).setHeight(120).ifCondition(conditionStringInput).setWidth(80) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertTrue(actualResult.hasPrefix("if"), "ifCondition should appear at the beginning of the trasformation string") XCTAssertEqual(actualResult, expectedResult, "components should be in proper order") } // MARK: - multi transformations condition func test_multiTransformationIfCondition_StringConditionAndMultiProperties_shouldReturnValidOrderedString() { // Given let conditionStringInput1 = "w_lt_200" let conditionStringInput2 = "w_gt_400" let expectedResult = "if_w_lt_200,c_fill,h_120,w_80/if_w_gt_400,c_fit,h_150,w_150/e_sepia" // When sut.ifCondition(conditionStringInput1).setCrop(.fill).setHeight(120).setWidth(80) .chain().ifCondition(conditionStringInput2).setCrop(.fit).setHeight(150).setWidth(150) .chain().setEffect(.sepia) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertTrue(actualResult.hasPrefix("if"), "ifCondition should appear at the beginning of the trasformation string") XCTAssertEqual(actualResult, expectedResult, "should allow multiple conditions when chaining transformations") } func test_multiTransformationUnorderedIfCondition_StringConditionAndMultiProperties_shouldReturnValidOrderedString() { // Given let conditionStringInput1 = "w_lt_200" let conditionStringInput2 = "w_gt_400" let expectedResult = "if_w_lt_200,c_fill,h_120,w_80/if_w_gt_400,c_fit,h_150,w_150/e_sepia" // When sut.setCrop(.fill).ifCondition(conditionStringInput1).setHeight(120).setWidth(80) .chain().setCrop(.fit).setHeight(150).ifCondition(conditionStringInput2).setWidth(150) .chain().setEffect(.sepia) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertTrue(actualResult.hasPrefix("if"), "ifCondition should appear at the beginning of the trasformation string") XCTAssertEqual(actualResult, expectedResult, "should order multiple conditions when chaining transformations") } // MARK: - operators func test_ifCondition_specialOperators_shouldReturnValidString() { // Given let initialValue = "width > 200" let valueHeight = "height > 200" let valueWidth = "width < 300" let expectedResult = "if_w_gt_200_and_h_gt_200_or_w_lt_300" let conditionObject = CLDConditionExpression(value: initialValue).and().value(valueHeight).or().value(valueWidth) // When sut.ifCondition(conditionObject) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertTrue(actualResult.hasPrefix("if"), "ifCondition should appear at the beginning of the trasformation string") XCTAssertEqual(actualResult, expectedResult, "Calling asString should return the expected result") } // MARK: - ifElse func test_ifElse_shouldStoreNewParam() { // Given let expectedResult = "else" // When sut.ifElse() let actualResult = sut.ifParam! // Then XCTAssertFalse(actualResult.isEmpty, "valid value should be stored") XCTAssertEqual(actualResult, expectedResult, "Calling for valid value should return the expected result") } func test_ifElse_shouldReturnValidString() { // Given let expectedResult = "if_else" // When sut.ifElse() let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "valid value should be stored") XCTAssertEqual(actualResult, expectedResult, "Calling for valid value should return the expected result") } func test_ifElse_multiProperties_shouldReturnValidString() { // Given let conditionStringInput = "w_lt_200" let expectedResult = "if_w_lt_200,c_fill,h_120,w_80/if_else,c_fit,h_150,w_150/e_sepia" // When sut.ifCondition(conditionStringInput).setCrop(.fill).setHeight(120).setWidth(80) .ifElse().setCrop(.fit).setHeight(150).setWidth(150) .chain().setEffect(.sepia) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertTrue(actualResult.hasPrefix("if"), "ifCondition should appear at the beginning of the trasformation string") XCTAssertEqual(actualResult, expectedResult, "should order multiple conditions when chaining transformations") } // MARK: - endIf func test_endIf_emptyTransformation_shouldReturnNil() { // When sut.endIf() let actualResult = sut.ifParam // Then XCTAssertNil(actualResult, "endIf should not stand alone") } func test_endIf_multiProperties_shouldReturnValidString() { // Given let conditionStringInput = "w_lt_200" let expectedResult = "if_w_lt_200/c_fill,h_120,w_80/if_else/c_fit,h_150,w_150/e_sepia/if_end" // When sut.ifCondition(conditionStringInput).setCrop(.fill).setHeight(120).setWidth(80) .ifElse().setCrop(.fit).setHeight(150).setWidth(150) .chain().setEffect(.sepia).endIf() let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertTrue(actualResult.hasSuffix("if_end"), "asString() return value should have an 'if_end' suffix") XCTAssertEqual(actualResult, expectedResult, "endIf() should separate the first ifCondition to a new transformation") } func test_endIf_callEndIfTwice_shouldReturnValidString() { // Given let conditionStringInput = "w_lt_200" let expectedResult = "if_w_lt_200/c_fill,h_120,w_80/if_else/c_fit,h_150/if_end/w_150/e_sepia/if_end" // When sut.ifCondition(conditionStringInput).setCrop(.fill).setHeight(120).setWidth(80) .ifElse().setCrop(.fit).setHeight(150).endIf().setWidth(150) .chain().setEffect(.sepia).endIf() let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertTrue(actualResult.hasSuffix("if_end"), "asString() return value should have an 'if_end' suffix") XCTAssertEqual(actualResult, expectedResult, "calling endIf() twice should return a valid string (although this probebly shouldn't happen)") } } ================================================ FILE: Example/Tests/TransformationTests/CLDTransformationTests/CLDTransformationExpressionsTests.swift ================================================ // // CLDTransformationExpressionsTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class CLDTransformationExpressionsTests: BaseTestCase { var sut : CLDTransformation! // MARK: - setup and teardown override func setUp() { super.setUp() sut = CLDTransformation() } override func tearDown() { sut = nil super.tearDown() } // MARK: - test set expression using get property func test_setWidth_emptyInputParamaters_shouldNotStoreNewVariable() { // Given let input = String() let expression = CLDExpression(value: input) // When sut.setWidth(expression) let actualResult = sut.width! // Then XCTAssertTrue(actualResult.isEmpty, "Empty expression should not be stored in params") } func test_setWidth_inputExpression_shouldStoreNewValue() { // Given let input = "initialHeight * 2" let expression = CLDExpression(value: input) let expectedResult = "ih_mul_2" // When sut.setWidth(expression) let actualResult = sut.width! // Then XCTAssertFalse(actualResult.isEmpty, "width should stored new value") XCTAssertEqual(actualResult, expectedResult, "Calling get width should return its value") } func test_setHeight_emptyInputParamaters_shouldNotStoreNewVariable() { // Given let input = String() let expression = CLDExpression(value: input) // When sut.setHeight(expression) let actualResult = sut.height! // Then XCTAssertTrue(actualResult.isEmpty, "Empty expression should not be stored in params") } func test_setHeight_inputExpression_shouldStoreNewValue() { // Given let input = "initialHeight * 2" let expression = CLDExpression(value: input) let expectedResult = "ih_mul_2" // When sut.setHeight(expression) let actualResult = sut.height! // Then XCTAssertFalse(actualResult.isEmpty, "x should stored new value") XCTAssertEqual(actualResult, expectedResult, "Calling get height should return its value") } func test_setX_emptyInputParamaters_shouldNotStoreNewVariable() { // Given let input = String() let expression = CLDExpression(value: input) // When sut.setX(expression) let actualResult = sut.x! // Then XCTAssertTrue(actualResult.isEmpty, "Empty expression should not be stored in params") } func test_setX_inputExpression_shouldStoreNewValue() { // Given let input = "initialHeight * 2" let expression = CLDExpression(value: input) let expectedResult = "ih_mul_2" // When sut.setX(expression) let actualResult = sut.x! // Then XCTAssertFalse(actualResult.isEmpty, "x should stored new value") XCTAssertEqual(actualResult, expectedResult, "Calling get x should return its value") } func test_setStartOffset_emptyInputParamaters_shouldNotStoreNewVariable() { // Given let input = String() let expression = CLDExpression(value: input) // When sut.setStartOffset(expression) let actualResult = sut.startOffset! // Then XCTAssertTrue(actualResult.isEmpty, "Empty expression should not be stored in params") } func test_setStartOffset_inputExpression_shouldStoreNewValue() { // Given let input = "duration - 3" let expression = CLDExpression(value: input) let expectedResult = "du_sub_3" // When sut.setStartOffset(expression) let actualResult = sut.startOffset! // Then XCTAssertFalse(actualResult.isEmpty, "actualResult should not be empty") XCTAssertEqual(actualResult, expectedResult, "actualResult and expectedResult should be equal") } func test_setEndOffset_emptyInputParamaters_shouldNotStoreNewVariable() { // Given let input = String() let expression = CLDExpression(value: input) // When sut.setEndOffset(expression) let actualResult = sut.endOffset! // Then XCTAssertTrue(actualResult.isEmpty, "Empty expression should not be stored in params") } func test_setEndOffset_inputExpression_shouldStoreNewValue() { // Given let input = "duration - 3" let expression = CLDExpression(value: input) let expectedResult = "du_sub_3" // When sut.setEndOffset(expression) let actualResult = sut.endOffset! // Then XCTAssertFalse(actualResult.isEmpty, "actualResult should not be empty") XCTAssertEqual(actualResult, expectedResult, "actualResult and expectedResult should be equal") } func test_setY_emptyInputParamaters_shouldNotStoreNewVariable() { // Given let input = String() let expression = CLDExpression(value: input) // When sut.setY(expression) let actualResult = sut.y! // Then XCTAssertTrue(actualResult.isEmpty, "Empty expression should not be stored in params") } func test_setY_inputExpression_shouldStoreNewValue() { // Given let input = "initialHeight * 2" let expression = CLDExpression(value: input) let expectedResult = "ih_mul_2" // When sut.setY(expression) let actualResult = sut.y! // Then XCTAssertFalse(actualResult.isEmpty, "y should stored new value") XCTAssertEqual(actualResult, expectedResult, "Calling get y should return its value") } func test_setRadius_emptyInputParamaters_shouldNotStoreNewVariable() { // Given let input = String() let expression = CLDExpression(value: input) // When sut.setRadius(expression) let actualResult = sut.radius! // Then XCTAssertTrue(actualResult.isEmpty, "Empty expression should not be stored in params") } func test_setRadius_inputExpression_shouldStoreNewValue() { // Given let input = "initialHeight * 2" let expression = CLDExpression(value: input) let expectedResult = "ih_mul_2" // When sut.setRadius(expression) let actualResult = sut.radius! // Then XCTAssertFalse(actualResult.isEmpty, "y should stored new value") XCTAssertEqual(actualResult, expectedResult, "Calling get y should return its value") } // MARK: - test asString() on empty expression func test_asString_expressionWidthWithEmptyInputParamaters_shouldReturnEmptyString() { // Given let value = String() let expression = CLDExpression(value: value) // When sut.setWidth(expression) let actualResult = sut.asString() // Then XCTAssertNil(actualResult, "Empty CLDExpression should not be stored in params") } func test_asString_expressionHeightWithEmptyInputParamaters_shouldReturnEmptyString() { // Given let value = String() let expression = CLDExpression(value: value) // When sut.setHeight(expression) let actualResult = sut.asString() // Then XCTAssertNil(actualResult, "Empty CLDExpression should not be stored in params") } func test_asString_expressionXWithEmptyInputParamaters_shouldReturnEmptyString() { // Given let value = String() let expression = CLDExpression(value: value) // When sut.setX(expression) let actualResult = sut.asString() // Then XCTAssertNil(actualResult, "Empty CLDExpression should not be stored in params") } func test_asString_expressionYWithEmptyInputParamaters_shouldReturnEmptyString() { // Given let value = String() let expression = CLDExpression(value: value) // When sut.setY(expression) let actualResult = sut.asString() // Then XCTAssertNil(actualResult, "Empty CLDExpression should not be stored in params") } func test_asString_expressionRadiusWithEmptyInputParamaters_shouldReturnEmptyString() { // Given let value = String() let expression = CLDExpression(value: value) // When sut.setRadius(expression) let actualResult = sut.asString() // Then XCTAssertNil(actualResult, "Empty CLDExpression should not be stored in params") } // MARK: - test asString() on expression func test_asString_expressionOnWidth_shouldReturnValidString() { // Given let input = "initialHeight * 2" let expression = CLDExpression(value: input) let expectedResult = "w_ih_mul_2" // When sut.setWidth(expression) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertEqual(actualResult, expectedResult, "Calling get asString should return its value") } func test_asString_expressionOnHeight_shouldReturnValidString() { // Given let input = "initialHeight * 2" let expression = CLDExpression(value: input) let expectedResult = "h_ih_mul_2" // When sut.setHeight(expression) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertEqual(actualResult, expectedResult, "Calling get asString should return its value") } func test_asString_expressionOnX_shouldReturnValidString() { // Given let input = "initialHeight * 2" let expression = CLDExpression(value: input) let expectedResult = "x_ih_mul_2" // When sut.setX(expression) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertEqual(actualResult, expectedResult, "Calling get asString should return its value") } func test_asString_expressionOnY_shouldReturnValidString() { // Given let input = "initialHeight * 2" let expression = CLDExpression(value: input) let expectedResult = "y_ih_mul_2" // When sut.setY(expression) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertEqual(actualResult, expectedResult, "Calling get asString should return its value") } func test_asString_expressionOnRadius_shouldReturnValidString() { // Given let input = "initialHeight * 2" let expression = CLDExpression(value: input) let expectedResult = "r_ih_mul_2" // When sut.setRadius(expression) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertEqual(actualResult, expectedResult, "Calling get asString should return its value") } func test_asString_extraSpacedExpression_shouldReturnValidString() { // Given let input = "initialHeight * 2" let expression = CLDExpression(value: input) let expectedResult = "r_ih_mul_2" // When sut.setRadius(expression) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertEqual(actualResult, expectedResult, "Calling asString should remove extra spaces") } func test_asString_expressionOnTwoProperties_shouldReturnValidString() { // Given let widthInput = "initialHeight * 2" let widthExpression = CLDExpression(value: widthInput) let radiusInput = "initialWidth * 2" let radiusExpression = CLDExpression(value: radiusInput) let expectedResult = "r_iw_mul_2,w_ih_mul_2" // When sut.setRadius(radiusExpression) sut.setWidth(widthExpression) let actualResult = sut.asString()! // Then XCTAssertFalse(actualResult.isEmpty, "asString should stored valid value") XCTAssertEqual(actualResult, expectedResult, "Calling get asString should return its value") } func test_complexVariablesAtExpressionStart_shouldRetainNames() { // Given let variable = CLDVariable(name: "$width", value: 10) let expectedResult = "$width_10/w_$width_add_10_add_w" // When sut.setVariable(variable) sut.chain() sut.setWidth("$width + 10 + width") let actualResult = sut.asString() // Then XCTAssertFalse(actualResult!.isEmpty, "asString should stored valid value") XCTAssertEqual(actualResult!, expectedResult, "Calling get asString should return its value") } func test_complexVariablesAtExpressionEnd_shouldRetainNames() { // Given let variable = CLDVariable(name: "$width", value: 10) let expectedResult = "$width_10/w_w_add_10_add_$width" // When sut.setVariable(variable) sut.chain() sut.setWidth("width + 10 + $width") let actualResult = sut.asString() // Then XCTAssertFalse(actualResult!.isEmpty, "asString should stored valid value") XCTAssertEqual(actualResult!, expectedResult, "Calling get asString should return its value") } // MARK: - chained conditions func test_chainedConditions_then_shouldReturnValidString() { // Given let expectedResult = "if_w_gt_200,c_scale,w_100" // When sut.ifCondition().width(">", 200).then().setCrop(.scale).setWidth(100) let actualResult = sut.asString()! // Then XCTAssertEqual(actualResult, expectedResult, "chained conditions should create the expected result") } func test_chainedConditions_ifElse_shouldReturnValidString() { // Given let expectedResult = "if_w_gt_200,c_fit,w_200/if_else,c_scale,w_100" // When sut.ifCondition().width(">", 200).then().setCrop(.fit).setWidth(200).ifElse().setCrop(.scale).setWidth(100) let actualResult = sut.asString()! // Then XCTAssertEqual(actualResult, expectedResult, "chained conditions should create the expected result") } func test_chainedConditions_endIf_shouldReturnValidString() { // Given let expectedResult = "if_w_gt_200/c_scale,w_100/if_end" // When sut.ifCondition().width(">", 200).then().setCrop(.scale).setWidth(100).endIf() let actualResult = sut.asString()! // Then XCTAssertEqual(actualResult, expectedResult, "chained conditions should create the expected result") } func test_chainedConditions_thenInFunc_shouldReturnValidString() { // Given let condition = CLDConditionExpression.width().greater(then: 100) let expression = CLDExpression.height().multiple(by: 10) let expectedResult = "if_w_gt_100,h_mul_10" // When sut.ifCondition(condition, then: expression) let actualResult = sut.asString()! // Then XCTAssertEqual(actualResult, expectedResult, "chained conditions should create the expected result") } } ================================================ FILE: Example/Tests/TransformationTests/CLDTransformationTests/CLDTransformationTests.m ================================================ // // ObjcCLDTransformationTests.m // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "ObjcBaseTestCase.h" @interface ObjcCLDTransformationTests : ObjcBaseTestCase @property (nonatomic, strong, nullable) CLDTransformation *sut; @end @implementation ObjcCLDTransformationTests // MARK: - setup and teardown - (void)setUp { [super setUp]; self.sut = [[CLDTransformation alloc] init]; } - (void)tearDown { [super tearDown]; self.sut = nil; } // MARK: - complexConditionExpression - (void)test_complexTransformations_shouldCreateValidString { // Given NSString * expectedResult = [self getExpectedResultToComplexConditionTest]; // When self.sut = [self.sut setVariable:@"$foo" intValue: 10]; self.sut = [self.sut setVariable:@"$foostr" valuesArray: @[@"my", @"str", @"ing"]]; self.sut = [self.sut chain]; self.sut = [self.sut ifCondition: [self getIfConditionForComplexConditionTest]]; self.sut = [self.sut setCrop: @"scale"]; CLDConditionExpression *conditionForWidth = [[CLDConditionExpression alloc] initWithValue: @"$foo * 200 / faceCount"]; self.sut = [self.sut setWidthWithExpression: conditionForWidth]; self.sut = [self.sut setOverlay: @"$foostr"]; self.sut = [self.sut endIf]; NSString * actualResult = [self.sut asString]; // Then XCTAssertTrue([actualResult isEqualToString: expectedResult], "Calling get asString should return the expect string"); } -(CLDConditionExpression*)getIfConditionForComplexConditionTest { CLDConditionExpression* condition = [[CLDConditionExpression faceCount] greaterThenInt: 2]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression pageCount]] lessThenInt: 300]; condition = [[condition orString: @"!myTag1!"] insideExpression:[CLDConditionExpression tags]]; condition = [[condition andString: @"!myTag2!"] notInsideExpression:[CLDConditionExpression tags]]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression width]] greaterOrEqualToInt: 200]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression height]] equalToString: @"$foo"]; condition = [[[[condition and] valueFromExpression:[CLDConditionExpression width]] notEqualToString: @"$foo"] multipleByInt: 2]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression height]] lessThenString: @"$foo"]; condition = [[[condition or] valueFromExpression:[CLDConditionExpression width]] lessOrEqualToInt: 500]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression illustrationScore]] lessThenInt: 0]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression currentPageIndex]] equalToInt: 10]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression pageXOffset]] lessThenInt: 300]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression pageYOffset]] lessThenInt: 300]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression pageYOffset]] notEqualToInt: 400]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression aspectRatio]] greaterThenString: @"3:4"]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression initialAspectRatio]] greaterThenString: @"3:4"]; // condition in condition CLDConditionExpression* innerConditionInitialWidth = [[[CLDConditionExpression initialWidth] divideByInt: 2] addByInt: 1]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression height]] lessThenExpression:innerConditionInitialWidth]; CLDConditionExpression* innerConditionInitialHeight = [[CLDConditionExpression initialHeight] subtractByString: @"$foo"]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression width]] lessThenExpression:innerConditionInitialHeight]; // duration condition = [[[condition and] valueFromExpression:[CLDConditionExpression duration]] equalToString: @"$foo"]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression duration]] notEqualToString: @"$foo"]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression duration]] lessThenInt: 30]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression duration]] lessOrEqualToString: @"$foo"]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression duration]] greaterThenInt: 30]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression duration]] greaterOrEqualToString: @"$foo"]; // initial duration condition = [[[condition and] valueFromExpression:[CLDConditionExpression initialDuration]] equalToString: @"$foo"]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression initialDuration]] notEqualToString: @"$foo"]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression initialDuration]] lessThenInt: 30]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression initialDuration]] lessOrEqualToString: @"$foo"]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression initialDuration]] greaterThenInt: 30]; condition = [[[condition and] valueFromExpression:[CLDConditionExpression initialDuration]] greaterOrEqualToString: @"$foo"]; return condition; } -(NSString*)getExpectedResultToComplexConditionTest { NSMutableString* expectedResult = [NSMutableString string]; [expectedResult appendString:@"$foo_10,$foostr_!my:str:ing!/if_fc_gt_2_and"]; [expectedResult appendString:@"_pc_lt_300_or"]; [expectedResult appendString:@"_!myTag1!_in_tags_and"]; [expectedResult appendString:@"_!myTag2!_nin_tags_and"]; [expectedResult appendString:@"_w_gte_200_and"]; [expectedResult appendString:@"_h_eq_$foo_and"]; [expectedResult appendString:@"_w_ne_$foo_mul_2_and"]; [expectedResult appendString:@"_h_lt_$foo_or"]; [expectedResult appendString:@"_w_lte_500_and"]; [expectedResult appendString:@"_ils_lt_0_and"]; [expectedResult appendString:@"_cp_eq_10_and"]; [expectedResult appendString:@"_px_lt_300_and"]; [expectedResult appendString:@"_py_lt_300_and"]; [expectedResult appendString:@"_py_ne_400_and"]; [expectedResult appendString:@"_ar_gt_3:4_and"]; [expectedResult appendString:@"_iar_gt_3:4_and"]; [expectedResult appendString:@"_h_lt_iw_div_2_add_1_and"]; [expectedResult appendString:@"_w_lt_ih_sub_$foo_and"]; [expectedResult appendString:@"_du_eq_$foo_and"]; [expectedResult appendString:@"_du_ne_$foo_and"]; [expectedResult appendString:@"_du_lt_30_and"]; [expectedResult appendString:@"_du_lte_$foo_and"]; [expectedResult appendString:@"_du_gt_30_and"]; [expectedResult appendString:@"_du_gte_$foo_and"]; [expectedResult appendString:@"_idu_eq_$foo_and"]; [expectedResult appendString:@"_idu_ne_$foo_and"]; [expectedResult appendString:@"_idu_lt_30_and"]; [expectedResult appendString:@"_idu_lte_$foo_and"]; [expectedResult appendString:@"_idu_gt_30_and"]; [expectedResult appendString:@"_idu_gte_$foo"]; [expectedResult appendString:@"/c_scale,l_$foostr,w_$foo_mul_200_div_fc/if_end"]; return [NSString stringWithString: expectedResult]; } // MARK: - customPreFunction - (void)test_setCustomPreFunction_emptyWasm_shouldReturnValidString { // Given NSString* input = @""; NSString* expectedResult = @"wasm:"; // When CLDCustomFunction* customFunc = [CLDCustomFunction wasm:input]; [self.sut setCustomPreFunction:customFunc]; NSString* actualResult = [self.sut customPreFunction]; // Then XCTAssertEqualObjects(actualResult, expectedResult, "Calling for inserted param should return its value"); } - (void)test_setCustomPreFunction_wasm_shouldReturnValidString { // Given NSString* input = @"func"; NSString* expectedResult = @"wasm:func"; // When [self.sut setCustomPreFunction:[CLDCustomFunction wasm:input]]; NSString* actualResult = [self.sut customPreFunction]; // Then XCTAssertEqualObjects(actualResult, expectedResult, "Calling for inserted param should return its value"); } - (void)test_setCustomPreFunction_emptyRemote_shouldReturnValidString { // Given NSString* input = @""; NSString* expectedResult = @"remote:"; // When [self.sut setCustomPreFunction:[CLDCustomFunction remote:input]]; NSString* actualResult = [self.sut customPreFunction]; // Then XCTAssertEqualObjects(actualResult, expectedResult, "Calling for inserted param should return its value"); } - (void)test_setCustomPreFunction_remote_shouldReturnValidString { // Given NSString* input = @"func"; NSString* expectedResult = @"remote:ZnVuYw=="; // When [self.sut setCustomPreFunction:[CLDCustomFunction remote:input]]; NSString* actualResult = [self.sut customPreFunction]; // Then XCTAssertEqualObjects(actualResult, expectedResult, "Calling for inserted param should return its value"); } // MARK: - custom pre functions - (void)test_customPreFunction_wasm_shouldReturnExpectedValue { // Given NSString* input = @"blur_wasm"; NSString* expectedResult = @"fn_pre:wasm:blur_wasm"; // When self.sut = [[[CLDTransformation alloc] init] setCustomPreFunction:[CLDCustomFunction wasm:input]]; NSString* actualResult = [self.sut asString]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, "actualResult should be equal to expectedResult"); } - (void)test_customPreFunction_remote_shouldReturnExpectedValue { // Given NSString* input = @"https://df34ra4a.execute-api.us-west-2.amazonaws.com/default/cloudinaryFunction"; NSString* expectedResult = @"fn_pre:remote:aHR0cHM6Ly9kZjM0cmE0YS5leGVjdXRlLWFwaS51cy13ZXN0LTIuYW1hem9uYXdzLmNvbS9kZWZhdWx0L2Nsb3VkaW5hcnlGdW5jdGlvbg=="; // When self.sut = [[[CLDTransformation alloc] init] setCustomPreFunction:[CLDCustomFunction remote:input]]; NSString* actualResult = [self.sut asString]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, "actualResult should be equal to expectedResult"); } // MARK: - custom functions - (void)test_customFunction_wasm_shouldReturnExpectedValue { // Given NSString* input = @"blur_wasm"; NSString* expectedResult = @"fn_wasm:blur_wasm"; // When self.sut = [[[CLDTransformation alloc] init] setCustomFunction:[CLDCustomFunction wasm:input]]; NSString* actualResult = [self.sut asString]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, "actualResult should be equal to expectedResult"); } - (void)test_customFunction_remote_shouldReturnExpectedValue { // Given NSString* input = @"https://df34ra4a.execute-api.us-west-2.amazonaws.com/default/cloudinaryFunction"; NSString* expectedResult = @"fn_remote:aHR0cHM6Ly9kZjM0cmE0YS5leGVjdXRlLWFwaS51cy13ZXN0LTIuYW1hem9uYXdzLmNvbS9kZWZhdWx0L2Nsb3VkaW5hcnlGdW5jdGlvbg=="; // When self.sut = [[[CLDTransformation alloc] init] setCustomFunction:[CLDCustomFunction remote:input]]; NSString* actualResult = [self.sut asString]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, "actualResult should be equal to expectedResult"); } // MARK: - custom functions combinations - (void)test_customFunctionCombinations_setPreFirstAndWasm_shouldReturnExpectedValue { // Given NSString* inputPre = @"preFunc"; NSString* input = @"func"; NSString* expectedResult = @"fn_wasm:func"; // When self.sut = [[[[CLDTransformation alloc] init] setCustomPreFunction:[CLDCustomFunction wasm:inputPre]] setCustomFunction:[CLDCustomFunction wasm:input]]; NSString* actualResult = [self.sut asString]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, "custom pre function should only be used when custom function is not set"); } - (void)test_customFunctionCombinations_setPreLastAndRemote_shouldReturnExpectedValue { // Given NSString* inputPre = @"preFunc"; NSString* input = @"func"; NSString* expectedResult = @"fn_pre:remote:cHJlRnVuYw=="; // When self.sut = [[[[CLDTransformation alloc] init] setCustomFunction:[CLDCustomFunction remote:input]] setCustomPreFunction:[CLDCustomFunction remote:inputPre]]; NSString* actualResult = [self.sut asString]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, "custom pre function should only be used when custom function is not set"); } - (void)test_customFunctionCombinations_setBothWasmAndMultiParams_shouldReturnExpectedValue { // Given NSString* inputPre = @"preFunc"; NSString* input = @"func"; NSString* expectedResult = @"fn_pre:wasm:preFunc,r_50,w_20,x_40"; // When self.sut = [[[[[[[CLDTransformation alloc] init] setWidthWithInt:20] setCustomFunction:[CLDCustomFunction wasm:input]] setXFromInt:40] setCustomPreFunction:[CLDCustomFunction wasm:inputPre]] setRadiusFromInt:50]; NSString* actualResult = [self.sut asString]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, "custom pre function should only be used when custom function is not set"); } - (void)test_customFunctionCombinations_setBothRemoteAndMultiParams_shouldReturnExpectedValue { // Given NSString* inputPre = @"preFunc"; NSString* input = @"func"; NSString* expectedResult = @"fn_pre:remote:cHJlRnVuYw==,r_50,w_20,x_40"; // When self.sut = [[[[[[[CLDTransformation alloc] init] setWidthWithInt:20] setCustomFunction:[CLDCustomFunction remote:input]] setXFromInt:40] setCustomPreFunction:[CLDCustomFunction remote:inputPre]] setRadiusFromInt:50]; NSString* actualResult = [self.sut asString]; // Then XCTAssertEqualObjects(actualResult ,expectedResult, "custom pre function should only be used when custom function is not set"); } @end ================================================ FILE: Example/Tests/TransformationTests/CLDTransformationTests/CLDTransformationTests.swift ================================================ // // CLDTransformationTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class CLDTransformationTests: BaseTestCase { var sut : CLDTransformation! // MARK: - setup and teardown override func setUp() { super.setUp() sut = CLDTransformation() } override func tearDown() { sut = nil super.tearDown() } // MARK: - width func test_setWidth_int_shouldReturnValidString() { // Given let input = 200 let expectedResult = "200" // When sut.setWidth(input) let actualResult = sut.width! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setWidth_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3" // When sut.setWidth(input) let actualResult = sut.width! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setWidth_string_shouldReturnValidString() { // Given let input = "200" let expectedResult = "200" // When sut.setWidth(input) let actualResult = sut.width! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setWidth_expression_shouldReturnValidString() { // Given let input = CLDExpression.width().add(by: 200) let expectedResult = "w_add_200" // When sut.setWidth(input) let actualResult = sut.width! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - height func test_setHeight_int_shouldReturnValidString() { // Given let input = 200 let expectedResult = "200" // When sut.setHeight(input) let actualResult = sut.height! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setHeight_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3" // When sut.setHeight(input) let actualResult = sut.height! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setHeight_string_shouldReturnValidString() { // Given let input = "height 200" let expectedResult = "h_200" // When sut.setHeight(input) let actualResult = sut.height! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setHeight_expression_shouldReturnValidString() { // Given let input = CLDExpression.height().add(by: 200) let expectedResult = "h_add_200" // When sut.setHeight(input) let actualResult = sut.height! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - named func test_setNamed_string_shouldReturnValidString() { // Given let input = "name1" let expectedResult = "name1" // When sut.setNamed(input) let actualResult = sut.named! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setNamed_array_shouldReturnValidString() { // Given let input1 = "name1" let input2 = "name2" let expectedResult = "name1.name2" // When sut.setNamed([input1, input2]) let actualResult = sut.named! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - crop func test_setCrop_string_shouldReturnValidString() { // Given let input = "fill" let expectedResult = "fill" // When sut.setCrop(input) let actualResult = sut.crop! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setCrop_CLDCrop_shouldReturnValidString() { // Given let input = CLDTransformation.CLDCrop.fill let expectedResult = "fill" // When sut.setCrop(input) let actualResult = sut.crop! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - background func test_setBackground_stringValue_shouldReturnValidString() { // Given let input = "#rrraaa" let expectedResult = "rgb:rrraaa" // When sut.setBackground(input) let actualResult = sut.background! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - color func test_setColor_stringValue_shouldReturnValidString() { // Given let input = "#rrraaa" let expectedResult = "rgb:rrraaa" // When sut.setColor(input) let actualResult = sut.color! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - effect func test_setEffect_string_shouldReturnValidString() { // Given let input = "gamma" let expectedResult = "gamma" // When sut.setEffect(input) let actualResult = sut.effect! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setEffect_CLDEffect_shouldReturnValidString() { // Given let input = CLDTransformation.CLDEffect.oilPaint let expectedResult = "oil_paint" // When sut.setEffect(input) let actualResult = sut.effect! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setEffect_CLDArt_shouldReturnValidString() { // Given let input = CLDTransformation.CLDArtFilters.alDente let expectedResult = "art:al_dente" // When sut.setEffect(input) let actualResult = sut.effect! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setEffect_CLDEffectWithParam_shouldReturnValidString() { // Given let input = CLDTransformation.CLDEffect.gamma let param = "200" let expectedResult = "gamma:200" // When sut.setEffect(input, param: param) let actualResult = sut.effect! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setEffect_stringEffectWithParam_shouldReturnValidString() { // Given let input = "gamma" let param = "200" let expectedResult = "gamma:200" // When sut.setEffect(input, param: param) let actualResult = sut.effect! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - angle func test_setAngle_int_shouldReturnValidString() { // Given let input = 200 let expectedResult = "200" // When sut.setAngle(input) let actualResult = sut.angle! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setAngle_string_shouldReturnValidString() { // Given let input = "200" let expectedResult = "200" // When sut.setAngle(input) let actualResult = sut.angle! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setAngle_array_shouldReturnValidString() { // Given let input = ["200", "300"] let expectedResult = "200.300" // When sut.setAngle(input) let actualResult = sut.angle! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - opacity func test_setOpacity_int_shouldReturnValidString() { // Given let input = 200 let expectedResult = "200" // When sut.setOpacity(input) let actualResult = sut.opacity! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setOpacity_string_shouldReturnValidString() { // Given let input = "200" let expectedResult = "200" // When sut.setOpacity(input) let actualResult = sut.opacity! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - border func test_setBorder_widthAndColor_shouldReturnValidString() { // Given let input = 200 let color = "rgb:222.111.333" let expectedResult = "200px_solid_rgb:222.111.333" // When sut.setBorder(input, color: color) let actualResult = sut.border! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setBorder_string_shouldReturnValidString() { // Given let input = "5px_solid_#111111" let expectedResult = "5px_solid_rgb:111111" // When sut.setBorder(input) let actualResult = sut.border! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - x func test_setX_int_shouldReturnValidString() { // Given let input = 200 let expectedResult = "200" // When sut.setX(input) let actualResult = sut.x! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setX_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3" // When sut.setX(input) let actualResult = sut.x! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setX_string_shouldReturnValidString() { // Given let input = "+ 200" let expectedResult = "+_200" // When sut.setX(input) let actualResult = sut.x! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setX_expression_shouldReturnValidString() { // Given let input = CLDExpression.pageXOffset().add(by: 200) let expectedResult = "px_add_200" // When sut.setX(input) let actualResult = sut.x! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - y func test_setY_int_shouldReturnValidString() { // Given let input = 200 let expectedResult = "200" // When sut.setY(input) let actualResult = sut.y! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setY_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3" // When sut.setY(input) let actualResult = sut.y! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setY_string_shouldReturnValidString() { // Given let input = "+ 200" let expectedResult = "+_200" // When sut.setY(input) let actualResult = sut.y! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setY_expression_shouldReturnValidString() { // Given let input = CLDExpression.pageYOffset().add(by: 200) let expectedResult = "py_add_200" // When sut.setY(input) let actualResult = sut.y! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - radius func test_setRadius_int_shouldReturnValidString() { // Given let input = 200 let expectedResult = "200" // When sut.setRadius(input) let actualResult = sut.radius! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setRadius_string_shouldReturnValidString() { // Given let input = "+ 200" let expectedResult = "+_200" // When sut.setRadius(input) let actualResult = sut.radius! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - quality func test_setQuality_int_shouldReturnValidString() { // Given let input = 200 let expectedResult = "200" // When sut.setQuality(.fixed(input)) let actualResult = sut.quality! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setQuality_string_shouldReturnValidString() { // Given let input = "200" let expectedResult = "200" // When sut.setQuality(input) let actualResult = sut.quality! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setQuality_CLDQuality_shouldReturnValidString() { // Given let input = CLDTransformation.CLDQuality.auto() let expectedResult = "auto" // When sut.setQuality(input) let actualResult = sut.quality! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setQuality_CLDQualityBest_shouldReturnValidString() { // Given let input = CLDTransformation.CLDQuality.auto(.best) let expectedResult = "auto:best" // When sut.setQuality(input) let actualResult = sut.quality! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setQuality_CLDQualityGood_shouldReturnValidString() { // Given let input = CLDTransformation.CLDQuality.auto(.good) let expectedResult = "auto:good" // When sut.setQuality(input) let actualResult = sut.quality! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setQuality_CLDQualityEco_shouldReturnValidString() { // Given let input = CLDTransformation.CLDQuality.auto(.eco) let expectedResult = "auto:eco" // When sut.setQuality(input) let actualResult = sut.quality! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setQuality_CLDQualityLow_shouldReturnValidString() { // Given let input = CLDTransformation.CLDQuality.auto(.low) let expectedResult = "auto:low" // When sut.setQuality(input) let actualResult = sut.quality! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - defaultImage func test_setDefaultImage_string_shouldReturnValidString() { // Given let input = "image1" let expectedResult = "image1" // When sut.setDefaultImage(input) let actualResult = sut.defaultImage! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - gravity func test_setGravity_string_shouldReturnValidString() { // Given let input = "right" let expectedResult = "right" // When sut.setGravity(input) let actualResult = sut.gravity! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setGravity_CLDGravity_shouldReturnValidString() { // Given let input = CLDTransformation.CLDGravity.center let expectedResult = "center" // When sut.setGravity(input) let actualResult = sut.gravity! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - colorSpace func test_setColorSpace_string_shouldReturnValidString() { // Given let input = "blue" let expectedResult = "blue" // When sut.setColorSpace(input) let actualResult = sut.colorSpace! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - prefix func test_setPrefix_string_shouldReturnValidString() { // Given let input = "www" let expectedResult = "www" // When sut.setPrefix(input) let actualResult = sut.prefix! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - overlay func test_setOverlay_string_shouldReturnValidString() { // Given let input = "image" let expectedResult = "image" // When sut.setOverlay(input) let actualResult = sut.overlay! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - underlay func test_setUnderlay_string_shouldReturnValidString() { // Given let input = "image" let expectedResult = "image" // When sut.setUnderlay(input) let actualResult = sut.underlay! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - fetschFormat func test_setFetchFormat_string_shouldReturnValidString() { // Given let input = "format" let expectedResult = "format" // When sut.setFetchFormat(input) let actualResult = sut.fetchFormat! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - density func test_setDensity_int_shouldReturnValidString() { // Given let input = 30 let expectedResult = "30" // When sut.setDensity(input) let actualResult = sut.density! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setDensity_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setDensity(input) let actualResult = sut.density! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - page func test_setPage_int_shouldReturnValidString() { // Given let input = 30 let expectedResult = "30" // When sut.setPage(input) let actualResult = sut.page! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setPage_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setPage(input) let actualResult = sut.page! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - delay func test_setDelay_int_shouldReturnValidString() { // Given let input = 30 let expectedResult = "30" // When sut.setDelay(input) let actualResult = sut.delay! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setDelay_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setDelay(input) let actualResult = sut.delay! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - rawTransformation func test_setRawTransformation_shouldReturnValidString() { // Given let input = "r_w_add_200" let expectedResult = "r_w_add_200" // When sut.setRawTransformation(input) let actualResult = sut.rawTransformation! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setRawTransformation_multiProperties_shouldOrderTransformation() { // Given let input = "r_w_add_200" let expectedResult = "c_fit,w_100,r_w_add_200" // When sut.setCrop(.fit).setRawTransformation(input).setWidth(100) let actualResult = sut.asString()! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setRawTransformation_ifEnd_shouldOrderTransformation() { // Given let input = "r_w_add_200" let expectedResult = "if_w_gt_100/c_fit,w_100,r_w_add_200/if_end" // When sut.ifCondition().width(">", 100).then().setCrop(.fit).setRawTransformation(input).setWidth(100).endIf() let actualResult = sut.asString()! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - flags func test_setFlags_string_shouldReturnValidString() { // Given let input = "30.20.10" let expectedResult = "30.20.10" // When sut.setFlags(input) let actualResult = sut.flags! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setFlags_array_shouldReturnValidString() { // Given let input = ["30","20","10"] let expectedResult = "30.20.10" // When sut.setFlags(input) let actualResult = sut.flags! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - dpr func test_setDpr_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3" // When sut.setDpr(input) let actualResult = sut.dpr! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setDpr_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setDpr(input) let actualResult = sut.dpr! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - zoom func test_setZoom_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3" // When sut.setZoom(input) let actualResult = sut.zoom! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setZoom_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setZoom(input) let actualResult = sut.zoom! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - aspectRatio func test_setAspectRatio_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3" // When sut.setAspectRatio(input) let actualResult = sut.aspectRatio! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setAspectRatio_string_shouldReturnValidString() { // Given let input = "3:4" let expectedResult = "3:4" // When sut.setAspectRatio(input) let actualResult = sut.aspectRatio! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setAspectRatio_int_shouldReturnValidString() { // Given let inputNominator = 3 let inputDenuminator = 4 let expectedResult = "3:4" // When sut.setAspectRatio(nominator: inputNominator, denominator: inputDenuminator) let actualResult = sut.aspectRatio! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - custom pre functions func test_customPreFunction_wasm_shouldReturnExpectedValue(){ // Given let input = "blur_wasm" let expectedResult = "fn_pre:wasm:blur_wasm" // When let sut = CLDTransformation().setCustomPreFunction(.wasm(input)) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult ,expectedResult, "actualResult should be equal to expectedResult") } func test_customPreFunction_remote_shouldReturnExpectedValue(){ // Given let input = "https://df34ra4a.execute-api.us-west-2.amazonaws.com/default/cloudinaryFunction" let expectedResult = "fn_pre:remote:aHR0cHM6Ly9kZjM0cmE0YS5leGVjdXRlLWFwaS51cy13ZXN0LTIuYW1hem9uYXdzLmNvbS9kZWZhdWx0L2Nsb3VkaW5hcnlGdW5jdGlvbg==" // When let sut = CLDTransformation().setCustomPreFunction(.remote(input)) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult ,expectedResult, "actualResult should be equal to expectedResult") } func test_setCustomPreFunction_emptyWasm_shouldReturnValidString() { // Given let input = "" let expectedResult = "wasm:" // When sut.setCustomPreFunction(.wasm(input)) let actualResult = sut.customPreFunction! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setCustomPreFunction_wasm_shouldReturnValidString() { // Given let input = "func" let expectedResult = "wasm:func" // When sut.setCustomPreFunction(.wasm(input)) let actualResult = sut.customPreFunction! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setCustomPreFunction_emptyRemote_shouldReturnValidString() { // Given let input = "" let expectedResult = "remote:" // When sut.setCustomPreFunction(.remote(input)) let actualResult = sut.customPreFunction! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setCustomPreFunction_remote_shouldReturnValidString() { // Given let input = "func" let expectedResult = "remote:ZnVuYw==" // When sut.setCustomPreFunction(.remote(input)) let actualResult = sut.customPreFunction! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - custom functions func test_customFunction_wasm_shouldReturnExpectedValue(){ // Given let input = "blur_wasm" let expectedResult = "fn_wasm:blur_wasm" // When let sut = CLDTransformation().setCustomFunction(.wasm(input)) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult ,expectedResult, "actualResult should be equal to expectedResult") } func test_customFunction_remote_shouldReturnExpectedValue(){ // Given let input = "https://df34ra4a.execute-api.us-west-2.amazonaws.com/default/cloudinaryFunction" let expectedResult = "fn_remote:aHR0cHM6Ly9kZjM0cmE0YS5leGVjdXRlLWFwaS51cy13ZXN0LTIuYW1hem9uYXdzLmNvbS9kZWZhdWx0L2Nsb3VkaW5hcnlGdW5jdGlvbg==" // When let sut = CLDTransformation().setCustomFunction(.remote(input)) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult ,expectedResult, "actualResult should be equal to expectedResult") } func test_setCustomFunction_wasm_shouldReturnValidString() { // Given let input = "func" let expectedResult = "wasm:func" // When sut.setCustomFunction(.wasm(input)) let actualResult = sut.customFunction! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setCustomFunction_remote_shouldReturnValidString() { // Given let input = "func" let expectedResult = "remote:ZnVuYw==" // When sut.setCustomFunction(.remote(input)) let actualResult = sut.customFunction! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - custom functions combinations func test_customFunctionCombinations_setPreFirstAndWasm_shouldReturnExpectedValue(){ // Given let inputPre = "preFunc" let input = "func" let expectedResult = "fn_wasm:func" // When let sut = CLDTransformation().setCustomPreFunction(.wasm(inputPre)).setCustomFunction(.wasm(input)) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult ,expectedResult, "custom pre function should only be used when custom function is not set") } func test_customFunctionCombinations_setPreLastAndRemote_shouldReturnExpectedValue(){ // Given let inputPre = "preFunc" let input = "func" let expectedResult = "fn_pre:remote:cHJlRnVuYw==" // When let sut = CLDTransformation().setCustomFunction(.remote(input)).setCustomPreFunction(.remote(inputPre)) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult ,expectedResult, "custom pre function should only be used when custom function is not set") } func test_customFunctionCombinations_setBothWasmAndMultiParams_shouldReturnExpectedValue(){ // Given let inputPre = "preFunc" let input = "func" let expectedResult = "dpr_20.0,fn_pre:wasm:preFunc,r_10,w_40,x_30" // When let sut = CLDTransformation().setWidth(40).setCustomFunction(.wasm(input)).setX(30).setCustomPreFunction(.wasm(inputPre)).setDpr(20.0).setRadius(10) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult ,expectedResult, "custom pre function should only be used when custom function is not set") } func test_customFunctionCombinations_setBothRemoteAndMultiParams_shouldReturnExpectedValue(){ // Given let inputPre = "preFunc" let input = "func" let expectedResult = "dpr_20.0,fn_pre:remote:cHJlRnVuYw==,r_10,w_40,x_30" // When let sut = CLDTransformation().setWidth(40).setCustomFunction(.remote(input)).setX(30).setCustomPreFunction(.remote(inputPre)).setDpr(20.0).setRadius(10) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult ,expectedResult, "custom pre function should only be used when custom function is not set") } // MARK: - audioCodec func test_setAudioCodec_string_shouldReturnValidString() { // Given let input = "sound" let expectedResult = "sound" // When sut.setAudioCodec(input) let actualResult = sut.audioCodec! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - audioFrequency func test_setAudioFrequency_int_shouldReturnValidString() { // Given let input = 30 let expectedResult = "30" // When sut.setAudioFrequency(input) let actualResult = sut.audioFrequency! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setAudioFrequency_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setAudioFrequency(input) let actualResult = sut.audioFrequency! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - bitRate func test_setBitRate_int_shouldReturnValidString() { // Given let input = 30 let expectedResult = "30" // When sut.setBitRate(input) let actualResult = sut.bitRate! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setBitRate_intK_shouldReturnValidString() { // Given let input = 30 let expectedResult = "30k" // When sut.setBitRate(kb: input) let actualResult = sut.bitRate! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setBitRate_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setBitRate(input) let actualResult = sut.bitRate! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - videoSampling func test_setVideoSampling_int_shouldReturnValidString() { // Given let input = 30 let expectedResult = "30" // When sut.setVideoSampling(frames:input) let actualResult = sut.videoSampling! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setVideoSampling_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3s" // When sut.setVideoSampling(delay: input) let actualResult = sut.videoSampling! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setVideoSampling_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setVideoSampling(input) let actualResult = sut.videoSampling! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - duration func test_setDuration_int_shouldReturnValidString() { // Given let input = 30 let expectedResult = "30p" // When sut.setDuration(percent: input) let actualResult = sut.duration! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setDuration_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3" // When sut.setDuration(seconds: input) let actualResult = sut.duration! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setDuration_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setDuration(input) let actualResult = sut.duration! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - startOffset func test_setStartOffset_int_shouldReturnValidString() { // Given let input = 30 let expectedResult = "30p" // When sut.setStartOffset(percent: input) let actualResult = sut.startOffset! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setStartOffset_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3" // When sut.setStartOffset(seconds: input) let actualResult = sut.startOffset! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setStartOffset_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setStartOffset(input) let actualResult = sut.startOffset! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - endOffset func test_setEndOffset_int_shouldReturnValidString() { // Given let input = 30 let expectedResult = "30p" // When sut.setEndOffset(percent: input) let actualResult = sut.endOffset! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setEndOffset_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3" // When sut.setEndOffset(seconds: input) let actualResult = sut.endOffset! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setEndOffset_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setEndOffset(input) let actualResult = sut.endOffset! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - startOffsetAndEndOffset func test_setStartOffsetAndEndOffset_shouldReturnValidString() { // Given let inputStart = Float(30.3) let inputEnd = Float(20.2) let expectedResult = "eo_20.2,so_30.3" // When sut.setStartOffsetAndEndOffset(startSeconds: inputStart, endSeconds: inputEnd) let actualResult = sut.asString()! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setStartOffsetAndEndOffset_percent_shouldReturnValidString() { // Given let inputStart = 20 let inputEnd = 30 let expectedResult = "eo_30p,so_20p" // When sut.setStartOffsetAndEndOffset(startPercent: inputStart, endPercent: inputEnd) let actualResult = sut.asString()! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - videoCodec func test_setVideoCodec_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setVideoCodec(input) let actualResult = sut.videoCodec! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - param func test_setParam_string_shouldReturnValidString() { // Given let input = "30" let key = CLDTransformation.TransformationParam.ANGLE.rawValue let expectedResult = "30" // When sut.setParam(key, value: input) let actualResult = sut.angle! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - offset func test_setOffset_intArray_shouldReturnValidString() { // Given let input = [30,20] let expectedResult = ["30p","20p"] // When sut.setOffset(percents: input) let actualResult = sut.offset! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setOffset_oneValueIntArray_shouldReturnValidString() { // Given let input = [30] let expectedResult = ["30p","30p"] // When sut.setOffset(percents: input) let actualResult = sut.offset! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setOffset_emptyIntArray_shouldNotStoreParam() { // Given let input = [Int]() // When sut.setOffset(percents: input) let actualResult = sut.offset // Then XCTAssertNil(actualResult, "offset should not be stored without a valid array") } func test_setOffset_floatArray_shouldReturnValidString() { // Given let input = [Float(30.3),Float(20.2)] let expectedResult = ["30.3","20.2"] // When sut.setOffset(seconds: input) let actualResult = sut.offset! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setOffset_oneValueFloatArray_shouldReturnValidString() { // Given let input = [Float(30.3)] let expectedResult = ["30.3","30.3"] // When sut.setOffset(seconds: input) let actualResult = sut.offset! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setOffset_emptyFloatArray_shouldNotStoreParam() { // Given let input = [Float]() // When sut.setOffset(seconds: input) let actualResult = sut.offset // Then XCTAssertNil(actualResult, "offset should not be stored without a valid array") } func test_setOffset_stringArray_shouldReturnValidString() { // Given let input = ["30","20"] let expectedResult = ["30","20"] // When sut.setOffset(input) let actualResult = sut.offset! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setOffset_oneValueStringArray_shouldReturnValidString() { // Given let input = ["30"] let expectedResult = ["30","30"] // When sut.setOffset(input) let actualResult = sut.offset! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setOffset_emptyStringArray_shouldNotStoreParam() { // Given let input = [String]() // When sut.setOffset(input) let actualResult = sut.offset // Then XCTAssertNil(actualResult, "offset should not be stored without a valid array") } // MARK: - fps func test_setFps_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3" // When sut.setFps(input) let actualResult = sut.fps! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setFps_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setFps(input) let actualResult = sut.fps! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setFps_CLDFps_shouldReturnValidString() { // Given let input = CLDTransformation.CLDFps.fromFloat(30.3) let expectedResult = "30.3" // When sut.setFps(input) let actualResult = sut.fps! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - overlayWithLayer func test_setOverlayWithLayer_float_shouldReturnValidString() { // Given let input = CLDLayer().setPublicId(publicId: "logo") let expectedResult = "logo" // When sut.setOverlayWithLayer(input) let actualResult = sut.overlay! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - underlayWithLayer func test_setUnderlayWithLayer_float_shouldReturnValidString() { // Given let input = CLDLayer().setPublicId(publicId: "logo") let expectedResult = "logo" // When sut.setUnderlayWithLayer(input) let actualResult = sut.underlay! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - topLeftPoint func test_setTopLeftPoint_shouldReturnValidString() { // Given let input = CGPoint(x: 20.2, y: 30.3) let expectedResult = "x_20.2,y_30.3" // When sut.setTopLeftPoint(input) let actualResult = sut.asString()! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - keyframeInterval func test_setKeyframeInterval_float_shouldReturnValidString() { // Given let input = Float(30.3) let expectedResult = "30.3" // When sut.setKeyframeInterval(interval: input) let actualResult = sut.keyframeInterval! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } func test_setKeyframeInterval_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setKeyframeInterval(input) let actualResult = sut.keyframeInterval! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } // MARK: - keyframeInterval func test_setStreamingProfile_string_shouldReturnValidString() { // Given let input = "30" let expectedResult = "30" // When sut.setStreamingProfile(input) let actualResult = sut.streamingProfile! // Then XCTAssertEqual(actualResult, expectedResult, "Calling for inserted param should return its value") } } ================================================ FILE: Example/Tests/TransformationTests/CLDTransformationTests/CLDTransformationVariablesTests.swift ================================================ // // CLDTransformationVariablesTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class CLDTransformationVariablesTests: BaseTestCase { var sut : CLDTransformation! // MARK: - setup and teardown override func setUp() { super.setUp() sut = CLDTransformation() } override func tearDown() { sut = nil super.tearDown() } // MARK: - test invalid variable using get param func test_setVariable_emptyInputParamaters_shouldNotStoreNewVariable() { // Given let variableName = String() let variableValue = String() // When sut.setVariable(variableName, string: variableValue) let actualResult = sut.getParam(variableName) // Then XCTAssertNil(actualResult, "Empty CLDVariable should not be stored in params") } func test_setVariable_emptyValueParamater_shouldNotStoreNewVariable() { // Given let variableName = "$foo" let variableValue = String() // When sut.setVariable(variableName, string: variableValue) let actualResult = sut.getParam(variableName) // Then XCTAssertNil(actualResult, "Empty CLDVariable should not be stored in params") } // MARK: - test set variable using get param func test_setVariable_validParamaters_shouldStoreNewVariable() { // Given let variableName = "$foo" let variableValue = Float(30.3) let expectedResult = "30.3" // When sut.setVariable(variableName, float: variableValue) let actualResult = sut.getParam(variableName) // Then XCTAssertEqual(actualResult, expectedResult, "Calling getParam on an CLDTransformation with a valid CLDVariable name as param, should return its value") } func test_setVariable_stringParamaters_shouldStoreNewVariable() { // Given let variableName = "$foo" let variableValue = "bar" let expectedResult = "bar" // When sut.setVariable(variableName, string: variableValue) let actualResult = sut.getParam(variableName) // Then XCTAssertEqual(actualResult, expectedResult, "Calling getParam on an CLDTransformation with a valid CLDVariable name as param, should return its value") } func test_setVariable_validVariableObject_shouldStoreNewVariable() { // Given let variableName = "$foo" let variableValue = "bar" let variable = CLDVariable(name: variableName, value: variableValue) let expectedResult = "bar" // When sut.setVariable(variable) let actualResult = sut.getParam(variableName) // Then XCTAssertEqual(actualResult, expectedResult, "Calling getParam on an CLDTransformation with a valid CLDVariable name as param, should return its value") } func test_setVariable_twoValidVariableObjectsOneLine_shouldStoreNewVariable() { // Given let variableName1 = "$foo" let variableValue1 = "bar" let variableName2 = "$nurf" let variableValue2 = "baz" let variable1 = CLDVariable(name: variableName1, value: variableValue1) let variable2 = CLDVariable(name: variableName2, value: variableValue2) let expectedResult1 = "bar" let expectedResult2 = "baz" // When sut.setVariable(variable1).setVariable(variable2) let actualResult1 = sut.getParam(variableName1) let actualResult2 = sut.getParam(variableName2) // Then XCTAssertEqual(actualResult1, expectedResult1, "Calling getParam on an CLDTransformation with a valid CLDVariable name as param, should return its value") XCTAssertEqual(actualResult2, expectedResult2, "Calling getParam on an CLDTransformation with a valid CLDVariable name as param, should return its value") } func test_setVariables_validVariablesArray_shouldStoreNewVariable() { // Given let variableName = "$foo" let variableValue = "bar" let variable = CLDVariable(name: variableName, value: variableValue) let expectedResult = "$foo_bar" // When sut.setVariables([variable]) let actualResult = sut.variables // Then XCTAssertEqual(actualResult, expectedResult, "Calling sut.variables should return its value") } func test_setVariables_twoValidVariablesArray_shouldStoreNewVariable() { // Given let variableName = "$foo" let variableValue = "bar" let variableName2 = "$nurf" let variableValue2 = "baz" let variable = CLDVariable(name: variableName , value: variableValue) let variable2 = CLDVariable(name: variableName2, value: variableValue2) let expectedResult = "$foo_bar,$nurf_baz" // When sut.setVariables([variable, variable2]) let actualResult = sut.variables // Then XCTAssertEqual(actualResult, expectedResult, "Calling sut.variables should return its value") } // MARK: - test asString() on empty variable func test_asString_variableWithEmptyInputParamaters_shouldReturnEmptyString() { // Given let variableName = String() let variableValue = String() let expectedResult = String() // When sut.setVariable(variableName, string: variableValue) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Empty CLDVariable should not be stored in params") } func test_asString_variablesArrayWithEmptyInputParamaters_shouldReturnEmptyString() { // Given let variable = CLDVariable() let expectedResult: String? = nil // When sut.setVariables([variable]) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Empty CLDVariable should not be stored in params") } // MARK: - test asString() on variable func test_asString_validVariable_shouldReturnValidString() { // Given let variableName = "foo" let variableValue = "bar" let expectedResult = "$foo_bar" // When sut.setVariable(variableName, string: variableValue) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString() on an CLDTransformation with a valid CLDVariable as param, should return the expected string") } func test_asString_validOneVariableArray_shouldReturnValidString() { // Given let variableName = "foo" let variableValue = "bar" let variable = CLDVariable(name: variableName, value: variableValue) let expectedResult = "$foo_bar" // When sut.setVariables([variable]) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString() on an CLDTransformation with a valid CLDVariable as param, should return the expected string") } func test_asString_validTwoVariablesArray_shouldReturnValidString() { // Given let variableName = "foo" let variableValue = "bar" let variableName2 = "foo2" let variableValue2 = "bar2" let variable = CLDVariable(name: variableName, value: variableValue) let variable2 = CLDVariable(name: variableName2, value: variableValue2) let expectedResult = "$foo_bar,$foo2_bar2" // When sut.setVariables([variable, variable2]) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString() on an CLDTransformation with a valid CLDVariable as param, should return the expected string") } func test_asString_validTwoVariablesArray_shouldReturnValidStringOrderedByEntry() { // Given let variableName = "foo" let variableValue = "bar" let variableName2 = "nurf" let variableValue2 = "baz" let variable = CLDVariable(name: variableName, value: variableValue) let variable2 = CLDVariable(name: variableName2, value: variableValue2) let expectedResult = "$nurf_baz,$foo_bar" // When sut.setVariables([variable2, variable]) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString() on an CLDTransformation with a valid CLDVariable as param, should return the expected string") } func test_asString_validTwoVariablesArrayAndParams_shouldReturnValidSortedString() { // Given let variableName = "foo" let variableValue = "bar" let variableName2 = "foo2" let variableValue2 = "bar2" let variable = CLDVariable(name: variableName, value: variableValue) let variable2 = CLDVariable(name: variableName2, value: variableValue2) let expectedResult = "$foo_bar,$foo2_bar2,h_12.0,w_11.0" // When sut.setWidth(11.0) sut.setVariables([variable, variable2]) sut.setHeight(12.0) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString() on an CLDTransformation with a valid CLDVariable as param, should return the expected string - variables first!") } func test_asString_validTwoVariablesArrayAndParams_shouldReturnValidSortedStringVariablesOrderedByEntry() { // Given let variableName1 = "foo1" let variableValue1 = "bar1" let variableName2 = "foo2" let variableValue2 = "bar2" let variableName3 = "foo3" let variableValue3 = "bar3" let variable1 = CLDVariable(name: variableName1, value: variableValue1) let variable2 = CLDVariable(name: variableName2, value: variableValue2) let variable3 = CLDVariable(name: variableName3, value: variableValue3) let expectedResult = "$foo3_bar3,$foo1_bar1,$foo2_bar2,h_12.0,w_11.0" // When sut.setVariable(variable2) sut.setWidth(11.0) sut.setVariables([variable3, variable1]) sut.setHeight(12.0) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString() on an CLDTransformation with a valid CLDVariable as param, should return the expected string - variables first!") } } ================================================ FILE: Example/Tests/TransformationTests/CLDVariableTests/CLDVariableTests.m ================================================ // // ObjcCLDVariableTests.m // // Copyright (c) 2018 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "ObjcBaseTestCase.h" @interface ObjcCLDVariableTests : ObjcBaseTestCase @end @implementation ObjcCLDVariableTests CLDVariable *sut; // MARK: - setup and teardown - (void)setUp { [super setUp]; } - (void)tearDown { [super tearDown]; sut = nil; } // MARK: - test initilization methods - empty - (void)test_init_emptyInputParamaters_shouldStoreEmptyProperties { // Given NSString *name = [NSString string]; // When sut = [[CLDVariable alloc] init]; // Then XCTAssertNotNil(sut.name, "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: [CLDVariable.variableNamePrefix stringByAppendingString:name]], "Name property should contain a valid prefix"); XCTAssertTrue([sut.value isEqualToString: [NSString string]], "Initilized object should contain an empty string as value property"); XCTAssertFalse([sut.name isEqualToString: name], "Name property should contain \"\(CLDVariable.variableNamePrefix)\" prefix"); } // MARK: - test initilization methods - value -(void)test_init_emptyNameParamater_shouldStoreEmptyNameProperty { // Given NSString *name = [NSString string]; NSString *value = @"alue"; // When sut = [[CLDVariable alloc] initWithName:name stringValue:value]; // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: [CLDVariable.variableNamePrefix stringByAppendingString:name]], "Name property should contain a valid prefix"); XCTAssertTrue([sut.value isEqualToString: value], "Initilized object should contain an empty string as value property"); } -(void)test_init_validStringParamatersAndNoNamePrefix_shouldStoreValidProperties { // Given NSString *name = @"name"; NSString *value = @"alue"; // When sut = [[CLDVariable alloc] initWithName:name stringValue:value]; // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: [CLDVariable.variableNamePrefix stringByAppendingString:name]], "Name property should have a valid prefix"); XCTAssertTrue([sut.value isEqualToString: value], "Initilized object should contain a string as value property"); } -(void)test_init_validStringParamaters_shouldStoreValidProperties { // Given NSString *name = @"$foo"; NSString *value = @"alue"; // When sut = [[CLDVariable alloc] initWithName:name stringValue:value]; // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: name], "Initilized object should contain a string as name property"); XCTAssertTrue([sut.value isEqualToString: value], "Initilized object should contain a string as value property"); } -(void)test_init_emptyNameParamaterIntValue_shouldStoreEmptyNameProperty { // Given NSString *name = [NSString string]; int value = 4; NSString* valueAsString = [@(value) stringValue]; // When sut = [[CLDVariable alloc] initWithName:name intValue:value]; // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: [CLDVariable.variableNamePrefix stringByAppendingString:name]], "Name property should have a valid prefix"); XCTAssertTrue([sut.value isEqualToString: valueAsString], "Initilized object should contain a string as value property"); } -(void)test_init_validIntValue_shouldStoreValidProperties { // Given NSString *name = @"name"; int value = 4; NSString* valueAsString = [@(value) stringValue]; // When sut = [[CLDVariable alloc] initWithName:name intValue:value]; // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: [CLDVariable.variableNamePrefix stringByAppendingString:name]], "Name property should have a valid prefix"); XCTAssertTrue([sut.value isEqualToString: valueAsString], "Initilized object should contain a string as value property"); } -(void)test_init_emptyNameParamaterDoubleValue_shouldStoreEmptyNameProperty { // Given NSString *name = [NSString string]; double value = 3.14; NSString* valueAsString = [@(value) stringValue]; // When sut = [[CLDVariable alloc] initWithName:name doubleValue:value]; // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: [CLDVariable.variableNamePrefix stringByAppendingString:name]], "Name property should have a valid prefix"); XCTAssertTrue([sut.value isEqualToString: valueAsString], "Initilized object should contain a string as value property"); } -(void)test_init_validDoubleValue_shouldStoreValidProperties { // Given NSString *name = @"name"; double value = 3.14; NSString* valueAsString = [@(value) stringValue]; // When sut = [[CLDVariable alloc] initWithName:name doubleValue:value]; // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: [CLDVariable.variableNamePrefix stringByAppendingString:name]], "Name property should have a valid prefix"); XCTAssertTrue([sut.value isEqualToString: valueAsString], "Initilized object should contain a string as value property"); } // MARK: - test initilization methods - values -(void)test_initWithValuesArray_emptyInputParamaters_shouldStoreEmptyProperties { // Given NSString *name = [NSString string]; NSArray *values = @[]; // When sut = [[CLDVariable alloc] initWithName:name values:values]; // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: [CLDVariable.variableNamePrefix stringByAppendingString:name]], "Name property should have a valid prefix"); XCTAssertTrue([sut.value isEqualToString: [NSString string]], "Initilized object should contain a string as value property"); } -(void)test_initWithValuesArray_emptyValueParamater_shouldStoreEmptyValueProperty { // Given NSString *name = @"name"; NSArray *values = @[]; // When sut = [[CLDVariable alloc] initWithName:name values:values]; // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: [CLDVariable.variableNamePrefix stringByAppendingString:name]], "Name property should have a valid prefix"); XCTAssertTrue([sut.value isEqualToString: [NSString string]], "Initilized object should contain a string as value property"); } -(void)test_initWithValuesArray_validOneValueArray_shouldStoreValidProperties { // Given NSString *name = @"name"; NSArray *values = @[@"my"]; NSString *expectedResult = @"!my!"; // When sut = [[CLDVariable alloc] initWithName:name values:values]; // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: [CLDVariable.variableNamePrefix stringByAppendingString:name]], "Name property should have a valid prefix"); XCTAssertTrue([sut.value isEqualToString: expectedResult], "Initilized object should contain a string as value property"); } -(void)test_initWithValuesArray_validTwoValueArray_shouldStoreValidProperties { // Given NSString *name = @"name"; NSArray *values = @[@"my",@"str"]; NSString *expectedResult = @"!my:str!"; // When sut = [[CLDVariable alloc] initWithName:name values:values]; // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: [CLDVariable.variableNamePrefix stringByAppendingString:name]], "Name property should have a valid prefix"); XCTAssertTrue([sut.value isEqualToString: expectedResult], "Initilized object should contain a string as value property"); } -(void)test_initWithValuesArray_validThreeValuesArray_shouldStoreValidProperties { // Given NSString *name = @"name"; NSArray *values = @[@"my",@"str",@"ing"]; NSString *expectedResult = @"!my:str:ing!"; // When sut = [[CLDVariable alloc] initWithName:name values:values]; // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property"); XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property"); XCTAssertTrue([sut.name isEqualToString: [CLDVariable.variableNamePrefix stringByAppendingString:name]], "Name property should have a valid prefix"); XCTAssertTrue([sut.value isEqualToString: expectedResult], "Initilized object should contain a string as value property"); } @end ================================================ FILE: Example/Tests/TransformationTests/CLDVariableTests/CLDVariableTests.swift ================================================ // // CLDTransformationTests.swift // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class CLDVariableTests: BaseTestCase { var sut : CLDVariable! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - test initilization methods - empty func test_init_emptyInputParamaters_shouldStoreEmptyProperties() { // Given let name = String() // When sut = CLDVariable() // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name , CLDVariable.variableNamePrefix + name, "Name property should contain a valid prefix") XCTAssertEqual(sut.value, String(), "Initilized object should contain an empty string as value property") XCTAssertNotEqual(sut.name , name, "Name property should contain \"\(CLDVariable.variableNamePrefix)\" prefix") } // MARK: - test initilization methods - value func test_init_emptyNameParamater_shouldStoreEmptyNameProperty() { // Given let name = String() let value = "alue" // When sut = CLDVariable(name: name, value: value) // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name , CLDVariable.variableNamePrefix + name, "Name property should have a valid prefix") XCTAssertEqual(sut.value, value, "Initilized object should contain an empty string as value property") } func test_init_validStringParamatersAndNoNamePrefix_shouldStoreValidProperties() { // Given let name = "name" let value = "alue" // When sut = CLDVariable(name: name, value: value) // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name , CLDVariable.variableNamePrefix + name, "Name property should have a valid prefix") XCTAssertEqual(sut.value, value, "Initilized object should contain a string as value property") } func test_init_validStringParamaters_shouldStoreValidProperties() { // Given let name = "$foo" let value = "alue" // When sut = CLDVariable(name: name, value: value) // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name , name, "Name property should have a valid prefix") XCTAssertEqual(sut.value, value, "Initilized object should contain a string as value property") } func test_init_emptyNameParamaterIntValue_shouldStoreEmptyNameProperty() { // Given let name = String() let value = 4 // When sut = CLDVariable(name: name, value: value) // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name , CLDVariable.variableNamePrefix + name, "Name property should have a valid prefix") XCTAssertEqual(sut.value, String(value), "Initilized object should contain a string as value property") } func test_init_validIntValue_shouldStoreValidProperties() { // Given let name = "name" let value = 4 // When sut = CLDVariable(name: name, value: value) // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name , CLDVariable.variableNamePrefix + name, "Name property should have a valid prefix") XCTAssertEqual(sut.value, String(value), "Initilized object should contain a string as value property") } func test_init_emptyNameParamaterDoubleValue_shouldStoreEmptyNameProperty() { // Given let name = String() let value = 3.14 // When sut = CLDVariable(name: name, value: value) // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name , CLDVariable.variableNamePrefix + name, "Name property should have a valid prefix") XCTAssertEqual(sut.value, String(value), "Initilized object should contain a string as value property") } func test_init_validDoubleValue_shouldStoreValidProperties() { // Given let name = "name" let value = 3.14 // When sut = CLDVariable(name: name, value: value) // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name , CLDVariable.variableNamePrefix + name, "Name property should have a valid prefix") XCTAssertEqual(sut.value, String(value), "Initilized object should contain a string as value property") } // MARK: - test initilization methods - values func test_initWithValuesArray_emptyInputParamaters_shouldStoreEmptyProperties() { // Given let name = String() let values = [String]() // When sut = CLDVariable(name: name, values: values) // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name, CLDVariable.variableNamePrefix + name, "Name property should have a valid prefix") XCTAssertEqual(sut.value, String(), "Initilized object should contain an empty string as value property") } func test_initWithValuesArray_emptyValueParamater_shouldStoreEmptyValueProperty() { // Given let name = "name" let values = [String]() // When sut = CLDVariable(name: name, values: values) // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name , CLDVariable.variableNamePrefix + name, "Name property should have a valid prefix") XCTAssertEqual(sut.value, String(), "Initilized object should contain an empty string as value property") } func test_initWithValuesArray_validOneValueArray_shouldStoreValidProperties() { // Given let name = "foo" let values = ["my"] let expectedResult = "!my!" // When sut = CLDVariable(name: name, values: values) // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name , CLDVariable.variableNamePrefix + name, "Name property should have a valid prefix") XCTAssertEqual(sut.value, expectedResult, "Initilized object should contain an encoded string as value property") } func test_initWithValuesArray_validTwoValuesArray_shouldStoreValidProperties() { // Given let name = "foo" let values = ["my","str"] let expectedResult = "!my:str!" // When sut = CLDVariable(name: name, values: values) // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name , CLDVariable.variableNamePrefix + name, "Name property should have a valid prefix") XCTAssertEqual(sut.value, expectedResult, "Initilized object should contain an encoded string as value property") } func test_initWithValuesArray_validThreeValuesArray_shouldStoreValidProperties() { // Given let name = "foo" let values = ["my","str","ing"] let expectedResult = "!my:str:ing!" // When sut = CLDVariable(name: name, values: values) // Then XCTAssertNotNil(sut.name , "Initilized object should contain a none nil name property") XCTAssertNotNil(sut.value, "Initilized object should contain a none nil value property") XCTAssertEqual(sut.name , CLDVariable.variableNamePrefix + name, "Name property should have a valid prefix") XCTAssertEqual(sut.value, expectedResult, "Initilized object should contain an encoded string as value property") } // MARK: - test asString() func test_asString_emptyInputParamaters_shouldReturnEmptyString() { // Given let name = String() let value = String() let expectedResult = String() // When sut = CLDVariable(name: name, value: value) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString on an empty CLDVariable, should return an empty string") } func test_asString_validParamaters_shouldReturnValidString() { // Given let name = "$foo" let value = "bar" let expectedResult = "$foo_bar" // When sut = CLDVariable(name: name, value: value) let actualResult = sut.asString() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asString on a CLDVariable, should return a string") } // MARK: - test asParams() func test_asParams_emptyInputParamaters_shouldReturnEmptyString() { // Given let name = String() let value = String() let expectedResult = [String:String]() // When sut = CLDVariable(name: name, value: value) let actualResult = sut.asParams() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asParams, should build a paramater representation") } func test_asParams_validParamaters_shouldReturnValidString() { // Given let name = "foo" let values = ["my","str","ing"] let expectedResult = ["$foo":"!my:str:ing!"] // When sut = CLDVariable(name: name, values: values) let actualResult = sut.asParams() // Then XCTAssertEqual(actualResult, expectedResult, "Calling asParams, should build a paramater representation") } } ================================================ FILE: Example/Tests/UIExtensions/CLDVideoPlayerTests.swift ================================================ // // CLDVideoPlayerTests.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest import Cloudinary import AVFoundation import AVKit class CLDVideoPlayerTests: UIBaseTest { // MARK: - Tests func testVideoLoadingAndSetFromURL() { var videoLoadedAndSet = false let videoPlayer = CLDVideoPlayer(url: "https://testurl.com") if videoPlayer.currentItem != nil { videoLoadedAndSet = true } XCTAssertTrue(videoLoadedAndSet) } func testInitializationWithValidData() { let publicId = "test_public_id" let transformation = CLDTransformation().setWidth(640).setHeight(480) let player = CLDVideoPlayer(publicId: publicId, cloudinary: cloudinarySecured, transformation: transformation) XCTAssertNotNil(player, "CLDVideoPlayer should initialize with valid data.") } func testPlaybackPauses() { var publicId: String? = "" let file = TestResourceType.dog.url let params = CLDUploadRequestParams().setColors(true) params.setResourceType(.video) XCTAssertNotNil(publicId) let player = CLDVideoPlayer(url: file) // Assuming you have a test expectation for the video to pause. let expectation = XCTestExpectation(description: "Video should pause.") // Start playing and pause after 2 seconds. player.play() DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { player.pause() expectation.fulfill() } wait(for: [expectation], timeout: 5.0) } func testEventsManagerReceivesDurationValues() { let eventsManager = VideoEventsManager() XCTAssertNoThrow({ eventsManager.sendLoadMetadataEvent(duration: 0) }, "Sending duration 0 should not crash") XCTAssertNoThrow({ eventsManager.sendLoadMetadataEvent(duration: 10) }, "Sending valid duration should not crash") XCTAssertNoThrow({ eventsManager.sendLoadMetadataEvent(duration: -1) }, "Events manager should handle any integer value") XCTAssertNoThrow({ eventsManager.sendLoadMetadataEvent(duration: Int.max) }, "Events manager should handle large values") } func testDurationHandlingFlowIntegration() { let expectation = XCTestExpectation(description: "Duration flow should complete") let player = CLDVideoPlayer(url: "data:,") DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { XCTAssertNoThrow({ _ = player.currentItem _ = player.status }, "Accessing player properties should not crash") XCTAssertNoThrow({ player.play() player.pause() }, "Player methods should work after problematic URL") expectation.fulfill() } wait(for: [expectation], timeout: 2.0) } } ================================================ FILE: Example/Tests/UIExtensions/UIBaseTest.swift ================================================ // // UIBaseTest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest import Cloudinary import UIKit class UIBaseTest: NetworkBaseTest { let url = "https://res-1.cloudinary.com/cloudinary/image/asset/dpr_2.0/logo-e0df892053afd966cc0bfe047ba93ca4.png" // MARK: - Setup override func setUp() { super.setUp() cloudinary!.enableUrlCache = false continueAfterFailure = false } } ================================================ FILE: Example/Tests/UIExtensions/UIButtonTests.swift ================================================ // // UIButtonTests.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest class UIButtonTests: UIBaseTest { // MARK: - Tests func testImageDownloadedAndSetFromURL() { let expectation = self.expectation(description: "should succeed downloading and setting image.") var imageDownloadedAndSet = false let btn = TestButton { (image) in if image != nil { imageDownloadedAndSet = true } expectation.fulfill() } btn.cldSetImage(url, forState: .normal, cloudinary: cloudinarySecured) waitForExpectations(timeout: timeout, handler: nil) XCTAssertTrue(imageDownloadedAndSet) } func testImageDownloadedAndSetFromPublicID() { var expectation = self.expectation(description: "Upload should succeed") var publicId: String? uploadFile().response({ (result, error) in publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "should succeed downloading and setting image.") var imageDownloadedAndSet = false let btn = TestButton { (image) in if image != nil { imageDownloadedAndSet = true } expectation.fulfill() } btn.cldSetImage(publicId: pubId, cloudinary: cloudinarySecured, forState: .normal) waitForExpectations(timeout: timeout, handler: nil) XCTAssertTrue(imageDownloadedAndSet) } func testBackgroundImageDownloadedAndSetFromURL() { let expectation = self.expectation(description: "should succeed downloading and setting image.") var imageDownloadedAndSet = false let btn = TestButton { (image) in if image != nil { imageDownloadedAndSet = true } expectation.fulfill() } btn.cld_setBackgroundImage(url, forState: .normal, cloudinary: cloudinarySecured) waitForExpectations(timeout: timeout, handler: nil) XCTAssertTrue(imageDownloadedAndSet) } func testBackgroundImageDownloadedAndSetFromPublicID() { var expectation = self.expectation(description: "Upload should succeed") var publicId: String? uploadFile().response({ (result, error) in publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "should succeed downloading and setting image.") var imageDownloadedAndSet = false let btn = TestButton { (image) in if image != nil { imageDownloadedAndSet = true } expectation.fulfill() } btn.cld_setBackgroundImage(publicId: pubId, cloudinary: cloudinarySecured, forState: .normal) waitForExpectations(timeout: timeout, handler: nil) XCTAssertTrue(imageDownloadedAndSet) } // MARK: - Helpers fileprivate class TestButton: UIButton { var imageSetListener: ((UIImage?) -> ())? init(imageSetListener: ((UIImage?) -> ())? = nil) { super.init(frame: CGRect.zero) self.imageSetListener = imageSetListener } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } fileprivate override func setImage(_ image: UIImage?, for state: UIControl.State) { super.setImage(image, for: state) imageSetListener?(image) } fileprivate override func setBackgroundImage(_ image: UIImage?, for state: UIControl.State) { super.setBackgroundImage(image, for: state) imageSetListener?(image) } } } ================================================ FILE: Example/Tests/UIExtensions/UIImageViewTests.swift ================================================ // // UIImageViewTests.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest import Cloudinary class UIImageViewTests: UIBaseTest { // MARK: - Tests func testImageDownloadedAndSetFromURL() { let expectation = self.expectation(description: "should succeed downloading and setting image.") var imageDownloadedAndSet = false let imageView = TestImageView { (image) in if image != nil { imageDownloadedAndSet = true } expectation.fulfill() } imageView.cldSetImage(url, cloudinary: cloudinarySecured) waitForExpectations(timeout: timeout, handler: nil) XCTAssertTrue(imageDownloadedAndSet) } func testImageDownloadedWithUrlCache() { let expectation = self.expectation(description: "should succeed downloading and setting image.") var imageDownloadedAndSet = false let imageView = TestImageView { (image) in if image != nil { imageDownloadedAndSet = true } expectation.fulfill() } cloudinarySecured.enableUrlCache = true imageView.cldSetImage(url, cloudinary: cloudinarySecured) waitForExpectations(timeout: timeout, handler: nil) XCTAssertTrue(imageDownloadedAndSet) } func testResponsiveImageDownloadedAndSetFromURL() { let expectation = self.expectation(description: "Upload should succeed") var publicId: String? uploadFile().response({ (result, error) in publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } let dpr = UIScreen.main.scale // Test auto_fill // ************** var params = CLDResponsiveParams.autoFill().setStepSize(50).setMaxDimension(350).setMinDimension(100) validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 145, viewHeight: 172, expectedImageWidth: 150 * dpr, expectedImageHeight: 200 * dpr, label: "fill, step size rounding") validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 210, viewHeight: 142, expectedImageWidth: 250 * dpr, expectedImageHeight: 150 * dpr, label: "fill, step size rounding") validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 100, viewHeight: 100, expectedImageWidth: 100 * dpr, expectedImageHeight: 100 * dpr, label: "fill, step size rounding, equal to min") validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 10, viewHeight: 10, expectedImageWidth: 100 * dpr, expectedImageHeight: 100 * dpr, label: "fill, step size rounding, below minimum") validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 10000, viewHeight: 10000, expectedImageWidth: 350 * dpr, expectedImageHeight: 350 * dpr, label: "fill, step size rounding, above maximum") // Test fit // ******** params = CLDResponsiveParams.fit().setStepSize(50).setMaxDimension(350).setMinDimension(100) validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 100, viewHeight: 300, expectedImageWidth: 100 * dpr, expectedImageHeight: 100 * dpr, label: "fit, step size rounding, equal to min") validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 210, viewHeight: 190, expectedImageWidth: 200 * dpr, expectedImageHeight: 200 * dpr, label: "fit, step size rounding") validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 300, viewHeight: 100, expectedImageWidth: 100 * dpr, expectedImageHeight: 100 * dpr, label: "fit, step size rounding, equal to min") validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 1, viewHeight: 1000, expectedImageWidth: 100 * dpr, expectedImageHeight: 100 * dpr, label: "fit, step size rounding, below min") validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 1000, viewHeight: 1, expectedImageWidth: 100 * dpr, expectedImageHeight: 100 * dpr, label: "fit, step size rounding, below min") validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 200, viewHeight: 10000, expectedImageWidth: 200 * dpr, expectedImageHeight: 200 * dpr, label: "fit, step size rounding, above max") validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 20000, viewHeight: 200, expectedImageWidth: 200 * dpr, expectedImageHeight: 200 * dpr, label: "fit, step size rounding, above max") validateResponsiveGeneration(publicId: pubId, params: params, viewWidth: 200, viewHeight: 200, expectedImageWidth: 200 * dpr, expectedImageHeight: 200 * dpr, label: "fit, step size rounding, equal to step size") } func testImageDownloadedAndSetFromPublicID() { var expectation = self.expectation(description: "Upload should succeed") var publicId: String? uploadFile().response({ (result, error) in publicId = result?.publicId expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) guard let pubId = publicId else { XCTFail("Public ID should not be nil at this point") return } expectation = self.expectation(description: "should succeed downloading and setting image.") var imageDownloadedAndSet = false let imageView = TestImageView { (image) in if image != nil { imageDownloadedAndSet = true } expectation.fulfill() } imageView.cldSetImage(publicId: pubId, cloudinary: cloudinarySecured) waitForExpectations(timeout: timeout, handler: nil) XCTAssertTrue(imageDownloadedAndSet) } // MARK: - Helpers private class TestImageView: CLDUIImageView { var imageSetListener: ((UIImage?) -> ())? override var image: UIImage? { didSet { imageSetListener?(image) } } init(imageSetListener: ((UIImage?) -> ())? = nil) { super.init(frame: CGRect.zero) self.imageSetListener = imageSetListener } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } private func validateResponsiveGeneration(publicId: String, params: CLDResponsiveParams, viewWidth: Int, viewHeight: Int, expectedImageWidth: CGFloat, expectedImageHeight: CGFloat, label: String) { let expectation = self.expectation(description: "Should succeed fetching image with correct dimensions for \(label)") var imageDownloadedAndSet = false let imageView = TestImageView { (image) in if image != nil && image!.size.width == expectedImageWidth && image!.size.height == expectedImageHeight { imageDownloadedAndSet = true } expectation.fulfill() } imageView.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight) imageView.layoutMargins.bottom = 0 imageView.layoutMargins.top = 0 imageView.layoutMargins.left = 0 imageView.layoutMargins.right = 0 imageView.layoutSubviews() imageView.cldSetImage(publicId: publicId, cloudinary: cloudinarySecured, responsiveParams: params) waitForExpectations(timeout: timeout, handler: nil) XCTAssertTrue(imageDownloadedAndSet) } } ================================================ FILE: Example/Tests/UIExtensions/Video Analytics/VideoEventsManagerTests.swift ================================================ // // VideoEventsManagerTests.swift // Cloudinary_Tests // // Created by Adi Mizrahi on 14/12/2023. // Copyright © 2023 CocoaPods. All rights reserved. // import Foundation import XCTest import Cloudinary final class VideoEventsManagerTests: XCTestCase { var videoEventManager: VideoEventsManager! override func setUp() { super.setUp() videoEventManager = VideoEventsManager() // Additional setup if needed } override func tearDown() { videoEventManager = nil super.tearDown() } func testAddingEventsToQueue() { let initialQueueCount = videoEventManager.eventQueue.count // Call functions that add events to the queue // Example: videoEventManager.sendViewStartEvent(videoUrl: "testURL") videoEventManager.sendViewEndEvent() videoEventManager.sendPlayEvent() XCTAssertEqual(videoEventManager.eventQueue.count, initialQueueCount + 3) } func testSendingEvents() { // Create an expectation for async testing let expectation = XCTestExpectation(description: "Events sent successfully") // Assuming you have mock URLSession or use a testable endpoint to avoid actual network calls videoEventManager.CLD_ANALYTICS_ENDPOINT_DEVELOPMENT_URL = "http://localhost:3001/mock_events" // Change to a mock endpoint // Add events to the queue videoEventManager.sendViewStartEvent(videoUrl: "testURL") videoEventManager.sendViewEndEvent() videoEventManager.sendPlayEvent() videoEventManager.sendEvents() // Simulate sending events // videoEventManager.sendEvents() // Wait for a certain amount of time or use a completion block to verify successful sending DispatchQueue.main.asyncAfter(deadline: .now() + 8.0) { // Assuming the response is received // Check if event queue is empty after sending XCTAssertTrue(self.videoEventManager.eventQueue.isEmpty) expectation.fulfill() } wait(for: [expectation], timeout: 16.0) // Wait for an expectation } } ================================================ FILE: Example/Tests/UIExtensions/Video Analytics/VideoEventsTests.swift ================================================ // // VideoEventsTests.swift // Cloudinary_Tests // // Created by Adi Mizrahi on 18/12/2023. // Copyright © 2023 CocoaPods. All rights reserved. // import XCTest import Cloudinary public class VideoEventTests: XCTestCase { func testVideoViewStartEventInitialization() { let videoUrl = "https://www.example.com/video.mp4" let trackingData = ["cloudName": "exampleCloud", "publicId": "abc123"] let providedData = ["key": "value"] let event = VideoViewStartEvent(videoUrl: videoUrl, trackingData: trackingData, providedData: providedData) XCTAssertEqual(event.eventName, EventNames.viewStart.rawValue) XCTAssertEqual(event.eventDetails[VideoEventJSONKeys.trackingType.rawValue] as? String, TrackingType.auto.rawValue) XCTAssertEqual(event.eventDetails[VideoEventJSONKeys.videoUrl.rawValue] as? String, videoUrl) if let customerData = event.eventDetails[VideoEventJSONKeys.customerData.rawValue] as? [String: Any], let videoData = customerData[VideoEventJSONKeys.videoData.rawValue] as? [String: Any] { XCTAssertEqual(videoData[VideoEventJSONKeys.cloudName.rawValue] as? String, trackingData["cloudName"]) XCTAssertEqual(videoData[VideoEventJSONKeys.publicId.rawValue] as? String, trackingData["publicId"]) } if let providedDataObject = event.eventDetails[VideoEventJSONKeys.providedData.rawValue] as? [String: Any] { XCTAssertEqual(providedDataObject["key"] as? String, providedData["key"]) } } func testVideoLoadMetadataEventInitialization() { let duration = 120 let event = VideoLoadMetadata(duration: duration) XCTAssertEqual(event.eventName, EventNames.loadMetadata.rawValue) XCTAssertEqual(event.eventDetails[VideoEventJSONKeys.trackingType.rawValue] as? String, TrackingType.auto.rawValue) XCTAssertEqual(event.eventDetails[VideoEventJSONKeys.videoDuration.rawValue] as? Int, duration) } func testVideoViewEndEventInitialization() { let providedData = ["key": "value"] let event = VideoViewEnd(providedData: providedData) XCTAssertEqual(event.eventName, EventNames.viewEnd.rawValue) XCTAssertEqual(event.eventDetails[VideoEventJSONKeys.trackingType.rawValue] as? String, TrackingType.auto.rawValue) if let providedDataObject = event.eventDetails[VideoEventJSONKeys.providedData.rawValue] as? [String: Any] { XCTAssertEqual(providedDataObject["key"] as? String, providedData["key"]) } } func testVideoPlayEventInitialization() { let providedData = ["key": "value"] let event = VideoPlayEvent(providedData: providedData) XCTAssertEqual(event.eventName, EventNames.play.rawValue) if let providedDataObject = event.eventDetails[VideoEventJSONKeys.providedData.rawValue] as? [String: Any] { XCTAssertEqual(providedDataObject["key"] as? String, providedData["key"]) } } func testVideoPauseEventInitialization() { let providedData = ["key": "value"] let event = VideoPauseEvent(providedData: providedData) XCTAssertEqual(event.eventName, EventNames.pause.rawValue) if let providedDataObject = event.eventDetails[VideoEventJSONKeys.providedData.rawValue] as? [String: Any] { XCTAssertEqual(providedDataObject["key"] as? String, providedData["key"]) } } } ================================================ FILE: Example/Tests/UploadWidgetTests/UploadWidgetHelpersTests/UploaderWidgetAssetContainerTests.swift ================================================ // // UploaderWidgetAssetContainerTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // @testable import Cloudinary import Foundation import XCTest import AVKit class UploaderWidgetAssetContainerTests: NetworkBaseTest { var sut: CLDWidgetAssetContainer! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - test init func test_init_image_shouldStoreInputValues() { // Given let image = UIImage() // When sut = CLDWidgetAssetContainer(originalImage: image, editedImage: image) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.originalImage, "object's properties should store value from init call") XCTAssertNotNil(sut.editedImage, "object's properties should store value from init call") XCTAssertEqual (sut.originalImage, image, "object's properties should store value from init call") XCTAssertEqual (sut.editedImage, image, "object's properties should store value from init call") XCTAssertNil (sut.originalVideo, "video should be nil") XCTAssertNotNil(sut.presentationImage, "presentationImage should be created in the object's init") XCTAssertEqual (sut.assetType, .image ,"assetType should be created in the object's init") } func test_init_localImages_shouldStoreInputValues() { // Given let originalImage = getImage(.borderCollie) let editedImage = getImage(.logo) // When sut = CLDWidgetAssetContainer(originalImage: originalImage, editedImage: editedImage) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.originalImage, "object's properties should store value from init call") XCTAssertNotNil(sut.editedImage, "object's properties should store value from init call") XCTAssertEqual (sut.originalImage, originalImage, "object's properties should store value from init call") XCTAssertEqual (sut.editedImage, editedImage, "object's properties should store value from init call") XCTAssertNil (sut.originalVideo, "video should be nil") XCTAssertNotNil(sut.presentationImage, "presentationImage should be created in the object's init") XCTAssertEqual (sut.assetType, .image ,"assetType should be created in the object's init") } func test_init_localVideoUrl_shouldStoreInputValues() { // Given let videoUrl = TestResourceType.dog.url // When sut = CLDWidgetAssetContainer(videoUrl: videoUrl) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNil (sut.originalImage, "image should be nil") XCTAssertNil (sut.editedImage, "image should be nil") XCTAssertNotNil(sut.originalVideo, "object's properties should store value from init call") XCTAssertNotNil(sut.presentationImage, "presentationImage should be created in the object's init") XCTAssertEqual (sut.assetType, .video ,"assetType should be created in the object's init") } func test_init_localVideoItem_shouldStoreInputValues() { // Given let videoItem = getVideo(.dog) // When sut = CLDWidgetAssetContainer(videoItem: videoItem) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNil (sut.originalImage, "image should be nil") XCTAssertNil (sut.editedImage, "image should be nil") XCTAssertNotNil(sut.originalVideo, "object's properties should store value from init call") XCTAssertEqual (sut.originalVideo, videoItem, "object's properties should store value from init call") XCTAssertNotNil(sut.presentationImage, "presentationImage should be created in the object's init") XCTAssertEqual (sut.assetType, .video ,"assetType should be created in the object's init") } // MARK: - test input func test_updateValues_localImages_shouldUpdateInputValues() { // Given let initialImage = getImage(.borderCollie) let newImage = getImage(.logo) // When sut = CLDWidgetAssetContainer(originalImage: initialImage, editedImage: initialImage) sut.editedImage = newImage // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertEqual (sut.originalImage, initialImage, "object's properties should store value from init call") XCTAssertEqual (sut.editedImage, newImage, "object's properties should update its value") } func test_thumbnailCreation_localVideo_shouldCreateExpectedThumbnail() { // Given let videoItem = getVideo(.dog) let knownSize = CGSize.init(width: 854, height: 480) // When sut = CLDWidgetAssetContainer(videoItem: videoItem) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertEqual (sut.presentationImage.size, knownSize, "object's properties should store value from init call") } } ================================================ FILE: Example/Tests/UploadWidgetTests/UploadWidgetHelpersTests/UploaderWidgetConfigurationTests.m ================================================ // // ObjcUploaderWidgetConfigurationTests.m // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import @interface ObjcUploaderWidgetConfigurationTests : XCTestCase @property (nonatomic, strong, nullable) CLDWidgetConfiguration* sut; @end @implementation ObjcUploaderWidgetConfigurationTests // MARK: - setup and teardown - (void)setUp { [super setUp]; } - (void)tearDown { [super tearDown]; self.sut = nil; } // MARK: - test initilization methods - (void)test_init_falseInputParamaters_shouldStoreInputValues { // Given BOOL allowRotate = false; AspectRatioLockState initialAspectLockState = AspectRatioLockStateDisabled; CLDUploadType* uploadType = [[CLDUploadType alloc] initWithSigned:false preset:nil]; // When self.sut = [[CLDWidgetConfiguration alloc] initWithAllowRotate:allowRotate initialAspectLockState:initialAspectLockState uploadType:uploadType]; // Then XCTAssertNotNil(self.sut, "object should be initialized"); XCTAssertFalse (self.sut.allowRotate, "object's properties should store value from init call"); XCTAssertEqual (self.sut.initialAspectLockState, initialAspectLockState, "object's properties should store value from init call"); XCTAssertEqual (self.sut.uploadType, uploadType, "object's properties should store value from init call"); } - (void)test_init_mixInputParamaters_shouldStoreInputValues { // Given BOOL allowRotate = true; AspectRatioLockState initialAspectLockState = AspectRatioLockStateEnabledAndOn; CLDUploadType* uploadType = [[CLDUploadType alloc] initWithSigned:true preset:@"preset"]; // When self.sut = [[CLDWidgetConfiguration alloc] initWithAllowRotate:allowRotate initialAspectLockState:initialAspectLockState uploadType:uploadType]; // Then XCTAssertNotNil(self.sut, "object should be initialized"); XCTAssertTrue (self.sut.allowRotate, "object's properties should store value from init call"); XCTAssertEqual (self.sut.initialAspectLockState, initialAspectLockState, "object's properties should store value from init call"); XCTAssertEqual (self.sut.uploadType, uploadType, "object's properties should store value from init call"); } @end ================================================ FILE: Example/Tests/UploadWidgetTests/UploadWidgetHelpersTests/UploaderWidgetConfigurationTests.swift ================================================ // // UploaderWidgetConfigurationTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class UploaderWidgetConfigurationTests: XCTestCase { var sut: CLDWidgetConfiguration! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - test initilization methods func test_init_emptyInputParamaters_shouldStoreDefaultValues() { // When sut = CLDWidgetConfiguration() // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertTrue (sut.allowRotate, "allowRotate default value should be true") XCTAssertEqual (sut.initialAspectLockState, CLDWidgetConfiguration.AspectRatioLockState.enabledAndOff, "initialAspectLockState default value should be enabledAndOff") XCTAssertTrue (sut.uploadType.signed, "uploadType.signed default value should be true") XCTAssertNil (sut.uploadType.preset, "uploadType.preset default value should be nil") } func test_init_falseInputParamaters_shouldStoreInputValues() { // Given let allowRotate = false let initialAspectLockState = CLDWidgetConfiguration.AspectRatioLockState.disabled let uploadType: CLDUploadType = CLDUploadType(signed: false, preset: "preset") // When sut = CLDWidgetConfiguration.init(allowRotate: allowRotate, initialAspectLockState: initialAspectLockState, uploadType: uploadType) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertFalse (sut.allowRotate, "object's properties should store value from init call") XCTAssertEqual (sut.initialAspectLockState, initialAspectLockState, "object's properties should store value from init call") XCTAssertEqual (sut.uploadType, uploadType, "object's properties should store value from init call") } func test_init_mixInputParamaters_shouldStoreInputValues() { // Given let allowRotate = true let initialAspectLockState = CLDWidgetConfiguration.AspectRatioLockState.enabledAndOn let uploadType = CLDUploadType(signed: true, preset: nil) // When sut = CLDWidgetConfiguration(allowRotate: allowRotate, initialAspectLockState: initialAspectLockState, uploadType: uploadType) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertTrue (sut.allowRotate, "object's properties should store value from init call") XCTAssertEqual (sut.initialAspectLockState, initialAspectLockState, "object's properties should store value from init call") XCTAssertEqual (sut.uploadType, uploadType, "object's properties should store value from init call") } } ================================================ FILE: Example/Tests/UploadWidgetTests/UploadWidgetVideoTests/UploaderWidgetVideoControlsTests.swift ================================================ // // UploaderWidgetVideoControlsTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // @testable import Cloudinary import Foundation import XCTest import AVKit class UploaderWidgetVideoControlsTests: NetworkBaseTest, CLDVideoControlsViewDelegate { var sut: CLDVideoControlsView! func pausePressedOnVideoControls(_ videoControls: CLDVideoControlsView) {} func playPressedOnVideoControls(_ videoControls: CLDVideoControlsView) {} // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - init func test_init_shouldCreateElement() { // Given let frame = CGRect.zero // When sut = CLDVideoControlsView(frame: frame, delegate: self) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.playPauseButton, "object should be initialized") XCTAssertNotNil(sut.visibilityTimer, "object should be initialized") XCTAssertNotNil(sut.delegate, "object should be initialized") XCTAssertNotNil(sut.currentState, "object should be initialized") XCTAssertNotNil(sut.shownAndPlayingState, "object should be initialized") XCTAssertNotNil(sut.shownAndPausedState, "object should be initialized") XCTAssertNotNil(sut.hiddenAndPlayingState, "object should be initialized") XCTAssertNotNil(sut.hiddenAndPausedState, "object should be initialized") } func test_init_noDelegate_shouldCreateElement() { // Given let frame = CGRect.zero // When sut = CLDVideoControlsView(frame: frame, delegate: nil) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.playPauseButton, "object should be initialized") XCTAssertNotNil(sut.visibilityTimer, "object should be initialized") XCTAssertNil (sut.delegate, "object should not be initialized") XCTAssertNotNil(sut.currentState, "object should be initialized") XCTAssertNotNil(sut.shownAndPlayingState, "object should be initialized") XCTAssertNotNil(sut.shownAndPausedState, "object should be initialized") XCTAssertNotNil(sut.hiddenAndPlayingState, "object should be initialized") XCTAssertNotNil(sut.hiddenAndPausedState, "object should be initialized") } // MARK: - states func test_initialState_shownAndPlayingState_shouldBeEqualToExpectedState() { // Given let frame = CGRect.zero // When sut = CLDVideoControlsView(frame: frame, delegate: nil) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.currentState, "object should be initialized") XCTAssertEqual (sut.currentState as? CLDVideoShownAndPlayingState, sut.shownAndPlayingState as? CLDVideoShownAndPlayingState, "objects should be equal") } func test_updateState_shownAndPausedState_shouldUpdateState() { // Given let frame = CGRect.zero // When sut = CLDVideoControlsView(frame: frame, delegate: nil) sut.setNewState(newState: sut.shownAndPausedState) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.currentState, "object should be initialized") XCTAssertEqual (sut.currentState as? CLDVideoShownAndPausedState, sut.shownAndPausedState as? CLDVideoShownAndPausedState,"objects should be equal") } func test_updateState_hiddenAndPlayingState_shouldUpdateState() { // Given let frame = CGRect.zero // When sut = CLDVideoControlsView(frame: frame, delegate: nil) sut.setNewState(newState: sut.hiddenAndPlayingState) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.currentState, "object should be initialized") XCTAssertEqual (sut.currentState as? CLDVideoHiddenAndPlayingState, sut.hiddenAndPlayingState as? CLDVideoHiddenAndPlayingState,"objects should be equal") } func test_updateState_hiddenAndPausedState_shouldUpdateState() { // Given let frame = CGRect.zero // When sut = CLDVideoControlsView(frame: frame, delegate: nil) sut.setNewState(newState: sut.hiddenAndPausedState) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.currentState, "object should be initialized") XCTAssertEqual (sut.currentState as? CLDVideoShownAndPausedState, sut.hiddenAndPausedState as? CLDVideoShownAndPausedState,"objects should be equal") } } ================================================ FILE: Example/Tests/UploadWidgetTests/UploadWidgetVideoTests/UploaderWidgetVideoDisplayLinkTests.swift ================================================ // // UploaderWidgetVideoDisplayLinkTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // @testable import Cloudinary import Foundation import XCTest import AVKit class UploaderWidgetVideoDisplayLinkTests: NetworkBaseTest, CLDDisplayLinkObserverDelegate { var sut: CLDDisplayLinkObserver! func displayLinkObserverDidTick(_ linkObserver: CLDDisplayLinkObserver) {} // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - init func test_init_shouldCreateElement() { // When sut = CLDDisplayLinkObserver(delegate: self) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.tickerTimestamp, "object should be initialized") } func test_init_noDelegate_shouldCreateElement() { // When sut = CLDDisplayLinkObserver(delegate: nil) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.tickerTimestamp, "object should be initialized") } // MARK: - internal methods func test_startTicker_shouldStartTheTickerLogic() { // When sut = CLDDisplayLinkObserver(delegate: nil) sut.startTicker() // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.tickerTimestamp, "object should be initialized") XCTAssertNotNil(sut.displayLinkTicker, "object should be initialized") } func test_stopTicker_shouldStopTheTickerLogic() { // When sut = CLDDisplayLinkObserver(delegate: nil) sut.startTicker() sut.stopTicker() // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.tickerTimestamp, "object should be initialized") XCTAssertNil (sut.displayLinkTicker, "object should not be initialized") } func test_isValid_notRunning_shouldRepresentCurrentTickerState() { // When sut = CLDDisplayLinkObserver(delegate: nil) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.tickerTimestamp, "object should be initialized") XCTAssertFalse (sut.isValid(), "object should not be valid") } func test_isValid_running_shouldRepresentCurrentTickerState() { // When sut = CLDDisplayLinkObserver(delegate: nil) sut.startTicker() // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.tickerTimestamp, "object should be initialized") XCTAssertTrue (sut.isValid(), "object should be valid") } } ================================================ FILE: Example/Tests/UploadWidgetTests/UploadWidgetVideoTests/UploaderWidgetVideoPlayerTests.swift ================================================ // // UploaderWidgetVideoPlayerTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // @testable import Cloudinary import Foundation import XCTest import AVKit class UploaderWidgetVideoPlayerTests: NetworkBaseTest { var sut: CLDVideoPlayerView! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - init func test_init_shouldCreateElement() { // Given let frame = CGRect.zero // When sut = CLDVideoPlayerView(frame: frame) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.playerLayer, "object should be initialized") } // MARK: - update func test_init_addPlayer_shouldCreateElement() { // Given let frame = CGRect.zero let player = AVPlayer() // When sut = CLDVideoPlayerView(frame: frame) sut.player = player // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertEqual (sut.player, player, "objects should be equal") } } ================================================ FILE: Example/Tests/UploadWidgetTests/UploadWidgetVideoTests/UploaderWidgetVideoViewTests.swift ================================================ // // UploaderWidgetVideoViewTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // @testable import Cloudinary import Foundation import XCTest import AVKit class UploaderWidgetVideoViewTests: NetworkBaseTest { var sut: CLDVideoView! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - init func test_init_shouldCreateElement() { // Given let frame = CGRect.zero let playerItem = getVideo(.dog) let isMuted = true // When sut = CLDVideoView(frame: frame, playerItem: playerItem, isMuted: isMuted) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.videoControlsView, "object should be initialized") XCTAssertNotNil(sut.videoPlayerView, "object should be initialized") XCTAssertNotNil(sut.player, "object should be initialized") XCTAssertEqual (sut.player.currentItem, playerItem, "objects should be equal") } func test_init_noVideo_shouldCreateElement() { // Given let frame = CGRect.zero let isMuted = true // When sut = CLDVideoView(frame: frame, playerItem: nil, isMuted: isMuted) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.videoControlsView, "object should be initialized") XCTAssertNotNil(sut.videoPlayerView, "object should be initialized") XCTAssertNotNil(sut.player, "object should be initialized") XCTAssertNil (sut.player.currentItem, "object should not be initialized") } // MARK: - update func test_replace_playerItem_shouldUpdateElements() { // Given let frame = CGRect.zero let playerItem = getVideo(.dog) let playerItemUpdated = getVideo(.dog2) let isMuted = true // When sut = CLDVideoView(frame: frame, playerItem: playerItem, isMuted: isMuted) sut.replaceCurrentItem(with: playerItemUpdated) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertEqual (sut.player.currentItem, playerItemUpdated, "objects should be equal") } } ================================================ FILE: Example/Tests/UploadWidgetTests/UploaderWidgetEditTests/UploaderWidgetEditViewControllerTests.swift ================================================ // // UploaderWidgetEditViewControllerTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class UploaderWidgetEditViewControllerTests: NetworkBaseTest, CLDWidgetEditDelegate { var sut: CLDWidgetEditViewController! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - Helper func createImageContainer() -> CLDWidgetAssetContainer { return CLDWidgetAssetContainer(originalImage: getImage(.logo), editedImage: getImage(.logo)) } // MARK: - delegate func widgetEditViewController(_ controller: CLDWidgetEditViewController, didFinishEditing image: CLDWidgetAssetContainer) { print("delegate didFinishEditing") } func widgetEditViewControllerDidReset(_ controller: CLDWidgetEditViewController) { print("delegate didReset") } func widgetEditViewControllerDidCancel(_ controller: CLDWidgetEditViewController) { print("delegate didCancel") } // MARK: - test init func test_init_emptyImage_shouldCreateObject() { // Given let image = CLDWidgetAssetContainer(originalImage: UIImage(), editedImage: UIImage()) // When sut = CLDWidgetEditViewController(image: image) // Then XCTAssertNotNil(sut, "object should be initialized") } func test_init_emptyProperties_shouldCreateObject() { // Given let image = CLDWidgetAssetContainer(originalImage: UIImage(), editedImage: UIImage()) // When sut = CLDWidgetEditViewController(image: image, configuration: nil, delegate: nil) // Then XCTAssertNotNil(sut, "object should be initialized") } func test_init_properties_shouldCreateObjectWithProperties() { // Given let image = createImageContainer() let configuration = CLDWidgetConfiguration(allowRotate: true, initialAspectLockState: .enabledAndOn) // When sut = CLDWidgetEditViewController(image: image, configuration: configuration, delegate: self) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "delegate should be initialized") XCTAssertEqual (sut.configuration, configuration, "objects should be equal") XCTAssertEqual (sut.image, image, "objects should be equal") } func test_init_delegateAfterInit_shouldCreateObjectAndUpdateDelegate() { // Given let image = createImageContainer() let configuration = CLDWidgetConfiguration(allowRotate: true, initialAspectLockState: .enabledAndOn) // When sut = CLDWidgetEditViewController(image: image, configuration: configuration, delegate: nil) sut.delegate = self // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "delegate should be initialized") XCTAssertEqual (sut.configuration, configuration, "objects should be equal") XCTAssertEqual (sut.image, image, "objects should be equal") } // MARK: - test views func test_createView_shouldCreateObjectWithUIElements() { // Given let image = createImageContainer() // When sut = CLDWidgetEditViewController(image: image) let _ = sut.view // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.buttonsView, "object should be initialized") XCTAssertNotNil(sut.cancelButton, "object should be initialized") XCTAssertNotNil(sut.rotateButton, "object should be initialized") XCTAssertNotNil(sut.doneButton, "object should be initialized") XCTAssertNotNil(sut.cropView, "object should be initialized") } func test_createView_shouldCreateCustomButtonsInSameSuperview() { // Given let image = createImageContainer() // When sut = CLDWidgetEditViewController(image: image) let _ = sut.view // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertEqual (sut.cancelButton.buttonType, .custom, "objects should be equal") XCTAssertEqual (sut.rotateButton.buttonType, .custom, "objects should be equal") XCTAssertEqual (sut.doneButton.buttonType, .custom, "objects should be equal") XCTAssertEqual (sut.cancelButton.superview, sut.buttonsView, "objects should be equal") XCTAssertEqual (sut.rotateButton.superview, sut.buttonsView, "objects should be equal") XCTAssertEqual (sut.doneButton.superview, sut.buttonsView, "objects should be equal") } func test_createView_allowRotateTrue_rotateButtonShouldBeVisible() { // Given let image = createImageContainer() let configuration = CLDWidgetConfiguration(allowRotate: true, initialAspectLockState: .enabledAndOn) // When sut = CLDWidgetEditViewController(image: image, configuration: configuration) let _ = sut.view // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertFalse (sut.rotateButton.isHidden, "rotate button should be visible when allowRotate = true") } func test_createView_allowRotateFalse_rotateButtonShouldBeHidden() { // Given let image = createImageContainer() let configuration = CLDWidgetConfiguration(allowRotate: false, initialAspectLockState: .enabledAndOn) // When sut = CLDWidgetEditViewController(image: image, configuration: configuration) let _ = sut.view // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertTrue (sut.rotateButton.isHidden, "rotate button should be hidden when allowRotate = false") } } ================================================ FILE: Example/Tests/UploadWidgetTests/UploaderWidgetPreviewTests/UploaderWidgetCollectionCellTests.swift ================================================ // // UploaderWidgetCollectionCellTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class UploaderWidgetCollectionCellTests: NetworkBaseTest { var sut: CLDWidgetPreviewCollectionCell! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - test init func test_init_shouldCreateElements() { // Given let frame = CGRect(x: 0, y: 0, width: 400, height: 300) // When sut = CLDWidgetPreviewCollectionCell(frame: frame) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.imageView, "object's elements should be initialized") XCTAssertEqual (sut.frame, frame, "object's frame should be set") } func test_init_imageViewFrame_shouldSetImageViewFrame() { // Given let frame = CGRect(x: 0, y: 0, width: 400, height: 300) // When sut = CLDWidgetPreviewCollectionCell(frame: frame) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.imageView, "object's elements should be initialized") XCTAssertEqual (sut.imageView.frame, frame, "object's imageView frame should be set") } func test_init_imageViewImage_shouldSetImageViewImage() { // Given let image = UIImage() let frame = CGRect(x: 0, y: 0, width: 400, height: 300) // When sut = CLDWidgetPreviewCollectionCell(frame: frame) sut.imageView.image = image // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.imageView, "object's elements should be initialized") XCTAssertEqual (sut.imageView.image, image, "object's imageView image should be set") } // MARK: - test update func test_update_imageViewImage_shouldUpdateImageViewImage() { // Given let initialImage = getImage(.logo) let updatedImage = getImage(.borderCollie) let frame = CGRect(x: 0, y: 0, width: 400, height: 300) // When sut = CLDWidgetPreviewCollectionCell(frame: frame) sut.imageView.image = initialImage sut.imageView.image = updatedImage // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.imageView, "object's elements should be initialized") XCTAssertEqual (sut.imageView.image, updatedImage, "object's imageView image should update") } } ================================================ FILE: Example/Tests/UploadWidgetTests/UploaderWidgetPreviewTests/UploaderWidgetPreviewViewControllerTests.swift ================================================ // // UploaderWidgetPreviewViewControllerTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // @testable import Cloudinary import Foundation import XCTest class UploaderWidgetPreviewViewControllerTests: WidgetBaseTest, CLDWidgetPreviewDelegate { var sut: CLDWidgetPreviewViewController! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - delegate func widgetPreviewViewController(_ controller: CLDWidgetPreviewViewController, didFinishEditing assets: [CLDWidgetAssetContainer]) {} func widgetPreviewViewController(_ controller: CLDWidgetPreviewViewController, didSelect asset: CLDWidgetAssetContainer) {} func widgetPreviewViewControllerDidCancel(_ controller: CLDWidgetPreviewViewController) {} // MARK: - test init func test_init_emptyArray_shouldCreateObject() { // Given let assets: [CLDWidgetAssetContainer] = [] // When sut = CLDWidgetPreviewViewController(assets: assets) // Then XCTAssertNotNil(sut, "object should be initialized") } func test_init_emptyDelegate_shouldCreateObject() { // Given let assets = createMixAssetContainers() // When sut = CLDWidgetPreviewViewController(assets: assets, delegate: nil) // Then XCTAssertNotNil(sut, "object should be initialized") } func test_init_mixAssetsAndDelegate_shouldCreateObjectWithProperties() { // Given let assetContainers = createMixAssetContainers() // When sut = CLDWidgetPreviewViewController(assets: assetContainers, delegate: self) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "delegate should be initialized") XCTAssertEqual (sut.assets, assetContainers, "objects should be equal") XCTAssertEqual (sut.selectedIndex, 0, "selectedIndex should be created with default value of 0") } func test_init_imageAssetsAndDelegate_shouldCreateObjectWithProperties() { // Given let assetContainers = createImageOnlyAssetContainers() // When sut = CLDWidgetPreviewViewController(assets: assetContainers, delegate: self) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "delegate should be initialized") XCTAssertEqual (sut.assets, assetContainers, "objects should be equal") XCTAssertEqual (sut.selectedIndex, 0, "selectedIndex should be created with default value of 0") } func test_init_videoAssetsAndDelegate_shouldCreateObjectWithProperties() { // Given let assetContainers = createVideoOnlyAssetContainers() // When sut = CLDWidgetPreviewViewController(assets: assetContainers, delegate: self) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "delegate should be initialized") XCTAssertEqual (sut.assets, assetContainers, "objects should be equal") XCTAssertEqual (sut.selectedIndex, 0, "selectedIndex should be created with default value of 0") } func test_init_delegateAfterInit_shouldCreateObjectWithDelegate() { // Given let assetContainers = createMixAssetContainers() // When sut = CLDWidgetPreviewViewController(assets: assetContainers, delegate: nil) sut.delegate = self // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "delegate should be initialized") } func test_createView_shouldCreateObjectWithUIElements() { // Given let assetContainers = createMixAssetContainers() // When sut = CLDWidgetPreviewViewController(assets: assetContainers, delegate: self) let _ = sut.view // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.collectionView, "object should be initialized") XCTAssertNotNil(sut.mainImageView, "object should be initialized") XCTAssertNotNil(sut.videoView, "object should be initialized") XCTAssertNotNil(sut.uploadButton, "object should be initialized") } func test_initWithAssetsAndCreateView_shouldCreateCollectionWithSpecificCellCount() { // Given let assetContainers = createMixAssetContainers() // When sut = CLDWidgetPreviewViewController(assets: assetContainers, delegate: self) let _ = sut.view // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "object should be initialized") XCTAssertEqual (sut.collectionView.numberOfItems(inSection: 0),assetContainers.count, "objects should be equal") } func test_initWithMixAssetsAndCreateView_shouldCreateImageViewWithSpecificImage() { // Given let assetContainers = createMixAssetContainers() // When sut = CLDWidgetPreviewViewController(assets: assetContainers, delegate: self) let _ = sut.view // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "object should be initialized") XCTAssertEqual (sut.mainImageView.image, assetContainers[0].presentationImage, "objects should be equal") } func test_initWithImageAssetsAndCreateView_shouldCreateImageViewWithSpecificImage() { // Given let assetContainers = createImageOnlyAssetContainers() // When sut = CLDWidgetPreviewViewController(assets: assetContainers, delegate: self) let _ = sut.view // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "object should be initialized") XCTAssertEqual (sut.mainImageView.image, assetContainers[0].presentationImage, "objects should be equal") } func test_initWithVideoAssetsAndCreateView_shouldCreateImageViewWithSpecificImage() { // Given let assetContainers = createVideoOnlyAssetContainers() // When sut = CLDWidgetPreviewViewController(assets: assetContainers, delegate: self) let _ = sut.view // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "object should be initialized") XCTAssertEqual (sut.videoView.player.currentItem, assetContainers[0].originalVideo, "objects should be equal") } func test_initWithAssetsAndCreateView_shouldCreateUploadButtonWithSpecificType() { // Given let assetContainers = createMixAssetContainers() // When sut = CLDWidgetPreviewViewController(assets: assetContainers, delegate: self) let _ = sut.view // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "object should be initialized") XCTAssertEqual (sut.uploadButton.buttonType, .custom, "objects should be equal") } } ================================================ FILE: Example/Tests/UploadWidgetTests/UploaderWidgetTests/UploaderWidgetTests.m ================================================ // // ObjcUploaderWidgetTests.m // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #import #import #import "NetworkBaseTestObjc.h" #import @interface ObjcUploaderWidgetTests : NetworkBaseTestObjc @property (nonatomic, strong, nullable) CLDUploaderWidget* sut; @end @implementation ObjcUploaderWidgetTests // MARK: - setup and teardown - (void)setUp { [super setUp]; } - (void)tearDown { [super tearDown]; self.sut = nil; } // MARK: - helper methods - (NSArray*)createImagesArray { NSMutableArray* imagesArray = [NSMutableArray new]; for (int index = 0; index < 10; index++) { UIImage* image = [self getImageBy:logo]; [imagesArray addObject:image]; } return [NSArray arrayWithArray:imagesArray]; } - (NSArray*)createVideosArray { NSMutableArray* videosArray = [NSMutableArray new]; for (int index = 0; index < 10; index++) { AVPlayerItem* video = [self getVideoBy:dog]; [videosArray addObject:video]; } return [NSArray arrayWithArray:videosArray]; } // MARK: - delegate - (void)uploadWidget:(CLDUploaderWidget * _Nonnull)widget willCall:(NSArray * _Nonnull)uploadRequests {} - (void)uploadWidgetDidDismiss {} - (void)widgetDidCancel:(CLDUploaderWidget * _Nonnull)widget {} // MARK: - test init - (void)test_init_cloudinary_shouldCreateObject { // Given CLDCloudinary* cloudinaryObject = self.cloudinary; // When self.sut = [[CLDUploaderWidget alloc] initWithCloudinary:cloudinaryObject configuration:nil images:nil videos:nil delegate:nil]; // Then XCTAssertNotNil(self.sut, "object should be initialized"); XCTAssertEqual (self.sut.cloudinaryObject, cloudinaryObject, "objects should be equal"); } - (void)test_init_cloudinaryConfiguration_shouldCreateObject { // Given CLDCloudinary* cloudinaryObject = self.cloudinary; CLDUploadType* uploadType = [[CLDUploadType alloc] initWithSigned:true preset:nil]; CLDWidgetConfiguration* configuration = [[CLDWidgetConfiguration alloc] initWithAllowRotate:true initialAspectLockState:AspectRatioLockStateEnabledAndOff uploadType:uploadType]; // When self.sut = [[CLDUploaderWidget alloc] initWithCloudinary:cloudinaryObject configuration:configuration images:nil videos:nil delegate:nil]; // Then XCTAssertNotNil(self.sut, "object should be initialized"); XCTAssertEqual (self.sut.cloudinaryObject, cloudinaryObject, "objects should be equal"); XCTAssertEqual (self.sut.configuration, configuration, "objects should be equal"); } - (void)test_init_cloudinaryConfigurationImages_shouldCreateObject { // Given CLDCloudinary* cloudinaryObject = self.cloudinary; NSArray* images = [self createImagesArray]; CLDUploadType* uploadType = [[CLDUploadType alloc] initWithSigned:true preset:nil]; CLDWidgetConfiguration* configuration = [[CLDWidgetConfiguration alloc] initWithAllowRotate:true initialAspectLockState:AspectRatioLockStateEnabledAndOff uploadType:uploadType]; // When self.sut = [[CLDUploaderWidget alloc] initWithCloudinary:cloudinaryObject configuration:configuration images:images videos:nil delegate:nil]; // Then XCTAssertNotNil(self.sut, "object should be initialized"); XCTAssertEqual (self.sut.cloudinaryObject, cloudinaryObject, "objects should be equal"); XCTAssertEqual (self.sut.configuration, configuration, "objects should be equal"); XCTAssertEqual (self.sut.images, images, "objects should be equal"); } - (void)test_convenienceInit_cloudinaryConfigurationImages_shouldCreateObject { // Given CLDCloudinary* cloudinaryObject = self.cloudinary; NSArray* images = [self createImagesArray]; CLDUploadType* uploadType = [[CLDUploadType alloc] initWithSigned:true preset:nil]; CLDWidgetConfiguration* configuration = [[CLDWidgetConfiguration alloc] initWithAllowRotate:true initialAspectLockState:AspectRatioLockStateEnabledAndOff uploadType:uploadType]; // When self.sut = [[CLDUploaderWidget alloc] initWithCloudinary:cloudinaryObject configuration:configuration images:images delegate:nil]; // Then XCTAssertNotNil(self.sut, "object should be initialized"); XCTAssertEqual (self.sut.cloudinaryObject, cloudinaryObject, "objects should be equal"); XCTAssertEqual (self.sut.configuration, configuration, "objects should be equal"); XCTAssertEqual (self.sut.images, images, "objects should be equal"); } - (void)test_init_cloudinaryConfigurationImagesVideos_shouldCreateObject { // Given CLDCloudinary* cloudinaryObject = self.cloudinary; NSArray* images = [self createImagesArray]; NSArray* videos = [self createVideosArray]; CLDUploadType* uploadType = [[CLDUploadType alloc] initWithSigned:true preset:nil]; CLDWidgetConfiguration* configuration = [[CLDWidgetConfiguration alloc] initWithAllowRotate:true initialAspectLockState:AspectRatioLockStateEnabledAndOff uploadType:uploadType]; // When self.sut = [[CLDUploaderWidget alloc] initWithCloudinary:cloudinaryObject configuration:configuration images:images videos:videos delegate:nil]; // Then XCTAssertNotNil(self.sut, "object should be initialized"); XCTAssertEqual (self.sut.cloudinaryObject, cloudinaryObject, "objects should be equal"); XCTAssertEqual (self.sut.configuration, configuration, "objects should be equal"); XCTAssertEqual (self.sut.images, images, "objects should be equal"); XCTAssertEqual (self.sut.videos, videos, "objects should be equal"); } - (void)test_init_allProperties_shouldCreateObject { // Given CLDCloudinary* cloudinaryObject = self.cloudinary; NSArray* images = [self createImagesArray]; NSArray* videos = [self createVideosArray]; CLDUploadType* uploadType = [[CLDUploadType alloc] initWithSigned:true preset:nil]; CLDWidgetConfiguration* configuration = [[CLDWidgetConfiguration alloc] initWithAllowRotate:true initialAspectLockState:AspectRatioLockStateEnabledAndOff uploadType:uploadType]; // When self.sut = [[CLDUploaderWidget alloc] initWithCloudinary:cloudinaryObject configuration:configuration images:images videos:videos delegate:self]; // Then XCTAssertNotNil(self.sut, "object should be initialized"); XCTAssertNotNil(self.sut.delegate, "object should be initialized"); XCTAssertEqual (self.sut.cloudinaryObject, cloudinaryObject, "objects should be equal"); XCTAssertEqual (self.sut.configuration, configuration, "objects should be equal"); XCTAssertEqual (self.sut.images, images, "objects should be equal"); XCTAssertEqual (self.sut.videos, videos, "objects should be equal"); } // MARK: - test update - (void)test_update_allProperties_shouldCreateObject { // Given CLDCloudinary* initialCloudinaryObject = self.cloudinary; CLDCloudinary* updatedCloudinaryObject = self.cloudinary; NSArray* images = [self createImagesArray]; NSArray* videos = [self createVideosArray]; CLDUploadType* uploadType = [[CLDUploadType alloc] initWithSigned:true preset:nil]; CLDWidgetConfiguration* configuration = [[CLDWidgetConfiguration alloc] initWithAllowRotate:true initialAspectLockState:AspectRatioLockStateEnabledAndOff uploadType:uploadType]; // When self.sut = [[CLDUploaderWidget alloc] initWithCloudinary:initialCloudinaryObject configuration:nil images:nil videos:nil delegate:nil]; [[[[[self.sut setCloudinaryFromCloudinary:updatedCloudinaryObject] setConfigurationFromConfiguration:configuration] setImagesFromImages:images] setVideosFromVideoItems:videos] setDelegate:self]; // Then XCTAssertNotNil(self.sut, "object should be initialized"); XCTAssertNotNil(self.sut.delegate, "object should be initialized"); XCTAssertEqual (self.sut.cloudinaryObject, updatedCloudinaryObject, "objects should be equal"); XCTAssertEqual (self.sut.configuration, configuration, "objects should be equal"); XCTAssertEqual (self.sut.videos, videos, "objects should be equal"); } @end ================================================ FILE: Example/Tests/UploadWidgetTests/UploaderWidgetTests/UploaderWidgetTests.swift ================================================ // // UploaderWidgetTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import XCTest @testable import Cloudinary class UploaderWidgetTests: WidgetBaseTest, CLDUploaderWidgetDelegate { var sut: CLDUploaderWidget! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - delegate func uploadWidget(_ widget: CLDUploaderWidget, willCall uploadRequests: [CLDUploadRequest]) {} func widgetDidCancel(_ widget: CLDUploaderWidget) {} func uploadWidgetDidDismiss() {} // MARK: - test init func test_init_cloudinary_shouldCreateObject() { // Given let cloudinaryObject = cloudinary! // When sut = CLDUploaderWidget(cloudinary: cloudinaryObject, configuration: nil, images: nil, videos: nil, delegate: nil) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertEqual (sut.cloudinaryObject, cloudinaryObject, "objects should be equal") } func test_init_cloudinaryConfiguration_shouldCreateObject() { // Given let cloudinaryObject = cloudinary! let configuration = CLDWidgetConfiguration() // When sut = CLDUploaderWidget(cloudinary: cloudinaryObject, configuration: configuration, images: nil, videos: nil, delegate: nil) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertEqual (sut.cloudinaryObject, cloudinaryObject, "objects should be equal") XCTAssertEqual (sut.configuration, configuration, "objects should be equal") } func test_init_cloudinaryConfigurationImages_shouldCreateObject() { // Given let images = createImages() let cloudinaryObject = cloudinary! let configuration = CLDWidgetConfiguration() // When sut = CLDUploaderWidget(cloudinary: cloudinaryObject, configuration: configuration, images: images, videos: nil, delegate: nil) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertEqual (sut.cloudinaryObject, cloudinaryObject, "objects should be equal") XCTAssertEqual (sut.configuration, configuration, "objects should be equal") XCTAssertEqual (sut.images, images, "objects should be equal") } func test_convenienceInit_cloudinaryConfigurationImages_shouldCreateObject() { // Given let images = createImages() let cloudinaryObject = cloudinary! let configuration = CLDWidgetConfiguration() // When sut = CLDUploaderWidget(cloudinary: cloudinaryObject, configuration: configuration, images: images, delegate: nil) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertEqual (sut.cloudinaryObject, cloudinaryObject, "objects should be equal") XCTAssertEqual (sut.configuration, configuration, "objects should be equal") XCTAssertEqual (sut.images, images, "objects should be equal") } func test_init_cloudinaryConfigurationImagesVideos_shouldCreateObject() { // Given let images = createImages() let videos = createVideos() let cloudinaryObject = cloudinary! let configuration = CLDWidgetConfiguration() // When sut = CLDUploaderWidget(cloudinary: cloudinaryObject, configuration: configuration, images: images, videos: videos, delegate: nil) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertEqual (sut.cloudinaryObject, cloudinaryObject, "objects should be equal") XCTAssertEqual (sut.configuration, configuration, "objects should be equal") XCTAssertEqual (sut.images, images, "objects should be equal") XCTAssertEqual (sut.videos, videos, "objects should be equal") } func test_init_allProperties_shouldCreateObject() { // Given let images = createImages() let videos = createVideos() let cloudinaryObject = cloudinary! let configuration = CLDWidgetConfiguration() // When sut = CLDUploaderWidget(cloudinary: cloudinaryObject, configuration: configuration, images: images, videos: videos, delegate: self) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "object should be set") XCTAssertEqual (sut.cloudinaryObject, cloudinaryObject, "objects should be equal") XCTAssertEqual (sut.configuration, configuration, "objects should be equal") XCTAssertEqual (sut.images, images, "objects should be equal") XCTAssertEqual (sut.videos, videos, "objects should be equal") } // MARK: - update func test_update_allProperties_shouldCreateObject() { // Given let images = createImages() let videos = createVideos() let initialCloudinaryObject = cloudinary! let updatedCloudinaryObject = cloudinarySecured! let configuration = CLDWidgetConfiguration() // When sut = CLDUploaderWidget(cloudinary: initialCloudinaryObject, configuration: nil, images: nil, videos: nil, delegate: nil) sut.setCloudinary(updatedCloudinaryObject).setConfiguration(configuration).setImages(images).setVideos(videos).setDelegate(self) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "object should be set") XCTAssertEqual (sut.cloudinaryObject, updatedCloudinaryObject, "objects should be equal") XCTAssertEqual (sut.configuration, configuration, "objects should be equal") XCTAssertEqual (sut.images, images, "objects should be equal") XCTAssertEqual (sut.videos, videos, "objects should be equal") } } ================================================ FILE: Example/Tests/UploadWidgetTests/UploaderWidgetViewControllerTests/UploaderWidgetViewControllerTests.swift ================================================ // // UploaderWidgetViewControllerTests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import Foundation import XCTest @testable import Cloudinary class UploaderWidgetViewControllerTests: WidgetBaseTest, CLDWidgetViewControllerDelegate { var sut: CLDWidgetViewController! // MARK: - setup and teardown override func setUp() { super.setUp() } override func tearDown() { sut = nil super.tearDown() } // MARK: - delegate func widgetViewController(_ controller: CLDWidgetViewController, didFinishEditing editedAssets: [CLDWidgetAssetContainer]) { print("didFinishEditing") } func widgetViewControllerDidCancel(_ controller: CLDWidgetViewController) { print("did cancel") } // MARK: - test init func test_init_emptyArray_shouldCreateObject() { // Given let assets: [CLDWidgetAssetContainer] = [] // When sut = CLDWidgetViewController(assets: assets) // Then XCTAssertNotNil(sut, "object should be initialized") } func test_init_emptyDelegate_shouldCreateObject() { // Given let assets = createMixAssetContainers() // When sut = CLDWidgetViewController(assets: assets, delegate: nil) // Then XCTAssertNotNil(sut, "object should be initialized") } func test_init_mixAssetsAllProperties_shouldCreateObjectWithProperties() { // Given let assets = createMixAssetContainers() let configuration = CLDWidgetConfiguration(allowRotate: true, initialAspectLockState: .enabledAndOn) // When sut = CLDWidgetViewController(assets: assets, configuration: configuration, delegate: self) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "delegate should be initialized") XCTAssertEqual (sut.assets, assets, "objects should be equal") XCTAssertEqual (sut.configuration, configuration, "objects should be equal") } func test_init_imageAssetsAllProperties_shouldCreateObjectWithProperties() { // Given let assets = createImageOnlyAssetContainers() let configuration = CLDWidgetConfiguration(allowRotate: true, initialAspectLockState: .enabledAndOn) // When sut = CLDWidgetViewController(assets: assets, configuration: configuration, delegate: self) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "delegate should be initialized") XCTAssertEqual (sut.assets, assets, "objects should be equal") XCTAssertEqual (sut.configuration, configuration, "objects should be equal") } func test_init_videoAssetsAllProperties_shouldCreateObjectWithProperties() { // Given let assets = createVideoOnlyAssetContainers() let configuration = CLDWidgetConfiguration(allowRotate: true, initialAspectLockState: .enabledAndOn) // When sut = CLDWidgetViewController(assets: assets, configuration: configuration, delegate: self) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "delegate should be initialized") XCTAssertEqual (sut.assets, assets, "objects should be equal") XCTAssertEqual (sut.configuration, configuration, "objects should be equal") } func test_init_updateAfterInit_shouldCreateObjectWithDelegate() { // Given let assets = createMixAssetContainers() // When sut = CLDWidgetViewController(assets: assets, delegate: nil) sut.delegate = self // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "delegate should be initialized") } func test_createView_shouldCreateObjectWithUIElements() { // Given let assets = createMixAssetContainers() // When sut = CLDWidgetViewController(assets: assets, delegate: self) let _ = sut.view // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.topButtonsView, "object should be initialized") XCTAssertNotNil(sut.backButton, "object should be initialized") XCTAssertNotNil(sut.actionButton, "object should be initialized") XCTAssertNotNil(sut.containerView, "object should be initialized") } func test_initAndCreateView_shouldCreateTopButtonsWithSpecificType() { // Given let assets = createMixAssetContainers() // When sut = CLDWidgetViewController(assets: assets, delegate: self) let _ = sut.view // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.delegate, "object should be initialized") XCTAssertEqual (sut.backButton.buttonType, .custom, "objects should be equal") XCTAssertEqual (sut.actionButton.buttonType, .custom, "objects should be equal") } func test_init_shouldCreatePreviewViewController() { // Given let assets = createMixAssetContainers() // When sut = CLDWidgetViewController(assets: assets, delegate: self) // Then XCTAssertNotNil(sut, "object should be initialized") XCTAssertNotNil(sut.previewViewController, "object should be initialized") } } ================================================ FILE: Example/Tests/UploadWidgetTests/WidgetBaseTest.swift ================================================ // // WidgetBaseTest.swift // // Copyright (c) 2016 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // @testable import Cloudinary import AVKit class WidgetBaseTest: NetworkBaseTest { func createMixAssetContainers() -> [CLDWidgetAssetContainer] { var assetContainers = [CLDWidgetAssetContainer]() for _ in 1...10 { let imageContainer = CLDWidgetAssetContainer(originalImage: getImage(.logo), editedImage: getImage(.logo)) assetContainers.append(imageContainer) } for _ in 1...10 { let videoContainer = CLDWidgetAssetContainer.init(videoItem: getVideo(.dog)) assetContainers.append(videoContainer) } return assetContainers } func createImageOnlyAssetContainers() -> [CLDWidgetAssetContainer] { var assetContainers = [CLDWidgetAssetContainer]() for _ in 1...10 { let imageContainer = CLDWidgetAssetContainer(originalImage: getImage(.logo), editedImage: getImage(.logo)) assetContainers.append(imageContainer) } return assetContainers } func createVideoOnlyAssetContainers() -> [CLDWidgetAssetContainer] { var assetContainers = [CLDWidgetAssetContainer]() for _ in 1...10 { let videoContainer = CLDWidgetAssetContainer.init(videoItem: getVideo(.dog)) assetContainers.append(videoContainer) } return assetContainers } func createImages() -> [UIImage] { var images = [UIImage]() for _ in 1...10 { let image = getImage(.logo) images.append(image) } return images } func createVideos() -> [AVPlayerItem] { var videos = [AVPlayerItem]() for _ in 1...10 { let video = getVideo(.dog) videos.append(video) } return videos } } ================================================ FILE: Example/WidgetUITests/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 ================================================ FILE: Example/WidgetUITests/WidgetUITests.swift ================================================ // // WidgetUITests.swift // // Copyright (c) 2020 Cloudinary (http://cloudinary.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest class WidgetUITests: XCTestCase { var app: XCUIApplication! override func setUpWithError() throws { try super.setUpWithError() app = XCUIApplication() app.launch() continueAfterFailure = false } override func tearDownWithError() throws { try super.tearDownWithError() app = nil } // MARK: - rotate button func test_rotateStates_on_shouldShowRotateButton() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 2).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() app.buttons[UITestConstants.widgetViewControllerActionButton].tap() let rotateButton = app.buttons[UITestConstants.editViewControllerRotateButton] // Then XCTAssertTrue(rotateButton.exists, "button should exist") XCTAssertTrue(rotateButton.isEnabled, "button should be enabled") } func test_rotateStates_off_shouldNotShowRotateButton() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.switches[UITestConstants.rotateStateSwitch].swipeLeft() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() app.buttons[UITestConstants.widgetViewControllerActionButton].tap() let rotateButton = app.buttons[UITestConstants.editViewControllerRotateButton] // Then XCTAssertFalse(rotateButton.exists, "button should not exist") } // MARK: - action button func test_actionButtonStates_noVideoEnabledAndOff_shouldShowActionButtonWithText() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 2).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let actionButton = app.buttons[UITestConstants.widgetViewControllerActionButton] actionButton.tap() // Then XCTAssertTrue (actionButton.exists, "button should exist") XCTAssertTrue (actionButton.isEnabled, "button should be enabled") XCTAssertEqual(actionButton.label, "Aspect ratio unlocked ", "button label should be set to string") } func test_actionButtonStates_noVideoEnabledAndOn_shouldShowActionButtonWithText() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 2).tap() app.segmentedControls[UITestConstants.aspectLockSegmented].children(matching: .button).element(boundBy: 1).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let actionButton = app.buttons[UITestConstants.widgetViewControllerActionButton] actionButton.tap() // Then XCTAssertTrue (actionButton.exists, "button should exist") XCTAssertTrue (actionButton.isEnabled, "button should be enabled") XCTAssertEqual(actionButton.label, "Aspect ratio locked ", "button label should be set to string") } func test_actionButtonStates_noVideoDisabled_shouldHideActionButton() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 2).tap() app.segmentedControls[UITestConstants.aspectLockSegmented].children(matching: .button).element(boundBy: 2).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() app.buttons[UITestConstants.widgetViewControllerActionButton].tap() let actionButton = app.buttons[UITestConstants.widgetViewControllerActionButton] // Then XCTAssertTrue (actionButton.exists, "button should exist") XCTAssertFalse(actionButton.isEnabled, "button should be disabled") XCTAssertEqual(actionButton.label, String(), "button label should be set to empty string") } func test_actionButtonStates_withVideoEnabledAndOff_shouldShowActionButtonWithText() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let actionButton = app.buttons[UITestConstants.widgetViewControllerActionButton] actionButton.tap() // Then XCTAssertTrue (actionButton.exists, "button should exist") XCTAssertFalse(actionButton.isEnabled, "button should be disabled") XCTAssertEqual(actionButton.label, String(), "button label should be set to empty string") } func test_actionButtonStates_withVideoEnabledAndOn_shouldShowActionButtonWithText() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.aspectLockSegmented].children(matching: .button).element(boundBy: 1).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let actionButton = app.buttons[UITestConstants.widgetViewControllerActionButton] actionButton.tap() // Then XCTAssertTrue (actionButton.exists, "button should exist") XCTAssertFalse(actionButton.isEnabled, "button should be disabled") XCTAssertEqual(actionButton.label, String(), "button label should be set to empty string") } func test_actionButtonStates_withVideoDisabled_shouldHideActionButton() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.aspectLockSegmented].children(matching: .button).element(boundBy: 2).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() app.buttons[UITestConstants.widgetViewControllerActionButton].tap() let actionButton = app.buttons[UITestConstants.widgetViewControllerActionButton] // Then XCTAssertTrue (actionButton.exists, "button should exist") XCTAssertFalse(actionButton.isEnabled, "button should be disabled") XCTAssertEqual(actionButton.label, String(), "button label should be set to empty string") } // MARK: - collection view func test_collectionView_many_shouldHaveManyCells() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let secondCell = app.collectionViews.children(matching: .cell).element(boundBy: 1) // Then XCTAssertTrue(secondCell.exists, "second cell should exist") } func test_collectionView_oneImage_shouldHaveOneCell() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 1).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 2).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let firstCell = app.collectionViews.children(matching: .cell).element(boundBy: 0) let secondCell = app.collectionViews.children(matching: .cell).element(boundBy: 1) // Then XCTAssertTrue(firstCell.exists, "one cell should exist") XCTAssertFalse(secondCell.exists, "only one cell should exist") } func test_collectionView_oneVideo_shouldHaveOneCell() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 2).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 1).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let firstCell = app.collectionViews.children(matching: .cell).element(boundBy: 0) let secondCell = app.collectionViews.children(matching: .cell).element(boundBy: 1) // Then XCTAssertTrue(firstCell.exists, "one cell should exist") XCTAssertFalse(secondCell.exists, "only one cell should exist") } func test_collectionView_none_shouldPresentImagePicker() { // Given let existsPredicate = NSPredicate(format: "exists == true") // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 2).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 2).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let imagePicker = app.navigationBars[UITestConstants.photos] // wait for image picker to load expectation(for: existsPredicate, evaluatedWith: imagePicker, handler: nil) waitForExpectations(timeout: 15, handler: nil) // Then XCTAssertTrue(imagePicker.exists, "if no assets added to the widget, image picker should be presented") } // MARK: - video view func test_videoView_oneVideo_shouldBePresented() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 2).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 1).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let isHittable = app.otherElements[UITestConstants.previewViewControllerVideoView].isHittable // Then XCTAssertTrue(isHittable, "video view should be presented when video is selected") } func test_videoView_mixAssets_shouldBePresented() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 0).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 0).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let isHittable = app.otherElements[UITestConstants.previewViewControllerVideoView].isHittable // Then XCTAssertTrue(isHittable, "video view should be presented when video is selected") } func test_videoView_noVideo_shouldNotBePresented() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 1).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 2).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let presented = app.otherElements[UITestConstants.previewViewControllerVideoView].exists // Then XCTAssertFalse(presented, "video view should not be presented when video is not selected") } // MARK: - video controls view func test_videoControlView_oneVideo_shouldExist() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 2).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 1).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let videoControlExists = app.otherElements[UITestConstants.videoControlsView].exists // Then XCTAssertTrue(videoControlExists, "video control should exist in this situation") } func test_videoControlViewButton_oneVideo_shouldBeHittble() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 2).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 1).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let isHittable = app.buttons[UITestConstants.videoViewPausePlayButton].isHittable // Then XCTAssertTrue(isHittable, "pausePlayButton should be presented when video is selected") } func test_videoControlViewButton_oneVideo_shouldUpdateTextOnPress() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 2).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 1).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let initialButtonText = app.buttons[UITestConstants.videoViewPausePlayButton].label app.buttons[UITestConstants.videoViewPausePlayButton].tap() let updatedButtonText = app.buttons[UITestConstants.videoViewPausePlayButton].label // Then XCTAssertNotEqual(initialButtonText, updatedButtonText, "pressing pausePlayButton should change its state and text") } func test_videoControlViewTimer_oneVideo_videoControlViewShouldDisappearAfterShortTime() { // Given let hittablePredicate = NSPredicate(format: "isHittable == false") // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 2).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 1).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let pausePlayButton = app.buttons[UITestConstants.videoViewPausePlayButton] // wait for pausePlayButton to disappear expectation(for: hittablePredicate, evaluatedWith: pausePlayButton, handler: nil) waitForExpectations(timeout: 10, handler: nil) // Then XCTAssertFalse(pausePlayButton.exists, "playPauseButton should disappear after a few seconds when the video is playing and showing the controls") } func test_videoControlView_backgroundPressed_videoControlViewShouldDisappear() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 2).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 1).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() // tap() will tap in the middle (on the button!) this code will force background press let videoControls = app.otherElements[UITestConstants.videoControlsView] let normalized = videoControls.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0)) let coordinate = normalized.withOffset(CGVector(dx: 10, dy: 10)) coordinate.tap() let pausePlayButton = app.buttons[UITestConstants.videoViewPausePlayButton] // Then XCTAssertFalse(pausePlayButton.exists, "playPauseButton should disappear after a few seconds when the video is playing and showing the controls") } // MARK: - video player view func test_videoPlayerView_oneVideo_shouldExist() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 2).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 1).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() let videoPlayerExists = app.otherElements[UITestConstants.videoPlayerView].exists // Then XCTAssertTrue(videoPlayerExists, "video player should exist when video view is presented") } // MARK: - image video transitions func test_imageVideoTransitions_videoToImage_shouldNotPresentVideoView() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 1).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 1).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() app.collectionViews.children(matching: .cell).element(boundBy: 1).tap() let presented = app.otherElements[UITestConstants.previewViewControllerVideoView].exists // Then XCTAssertFalse(presented, "video view should not be presented when video is not selected") } func test_imageVideoTransitions_videoToImageToVideo_shouldPresentVideoView() { // When app.buttons[UITestConstants.goToWidgetBarButton].tap() app.segmentedControls[UITestConstants.initialImagesSegmented].children(matching: .button).element(boundBy: 1).tap() app.segmentedControls[UITestConstants.initialVideosSegmented].children(matching: .button).element(boundBy: 1).tap() app.buttons[UITestConstants.widgetSettingsPresentBarButton].tap() app.collectionViews.children(matching: .cell).element(boundBy: 1).tap() app.collectionViews.children(matching: .cell).element(boundBy: 0).tap() let presented = app.otherElements[UITestConstants.previewViewControllerVideoView].exists // Then XCTAssertTrue(presented, "video view should be presented when video is selected") } } class UITestConstants { static let goToWidgetBarButton = "goToWidgetBarButton" static let widgetSettingsPresentBarButton = "widgetSettingsPresentBarButton" static let widgetViewControllerActionButton = "widgetViewControllerActionButton" static let editViewControllerRotateButton = "editViewControllerRotateButton" static let rotateStateSwitch = "rotateStateSwitch" static let aspectLockSegmented = "aspectLockSegmented" static let initialImagesSegmented = "initialImagesSegmented" static let initialVideosSegmented = "initialVideosSegmented" static let allPhotos = "All Photos" static let photos = "Photos" static let previewViewControllerVideoView = "previewViewControllerVideoView" static let videoViewPausePlayButton = "videoViewPausePlayButton" static let videoControlsView = "videoControlsView" static let videoPlayerView = "videoPlayerView" } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 Cloudinary (http://cloudinary.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Cloudinary", platforms: [ .iOS(.v9)], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "Cloudinary", targets: ["Cloudinary"]), ], dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "Cloudinary", path: "Cloudinary/Classes"), ] ) ================================================ FILE: Package@swift-5.3.swift ================================================ // swift-tools-version:5.3 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Cloudinary", platforms: [ .iOS(.v9)], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "Cloudinary", targets: ["Cloudinary"]), ], dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "Cloudinary", path: "Cloudinary/Classes", resources: [ .copy("Core/Network/PrivacyInfo.xcprivacy") ] ), ] ) ================================================ FILE: README.md ================================================ Cloudinary iOS SDK ========================= [![Build Status](https://api.travis-ci.com/cloudinary/cloudinary_ios.svg?branch=master)](https://app.travis-ci.com/github/cloudinary/cloudinary_ios) ## About The Cloudinary iOS SDK allows you to quickly and easily integrate your application with Cloudinary. Effortlessly optimize and transform your cloud's assets. ### Additional documentation This Readme provides basic installation and usage information. For the complete documentation, see the [iOS SDK Guide](https://cloudinary.com/documentation/ios_integration). ## Table of Contents - [Key Features](#key-features) - [Version Support](#Version-Support) - [Installation](#installation) - [Usage](#usage) - [Setup](#Setup) - [Transform and Optimize Assets](#Transform-and-Optimize-Assets) - [File Upload](#File-Upload) - [File Download](#File-Download) ## Key Features - [Transform](https://cloudinary.com/documentation/ios_video_manipulation#video_transformation_examples) and [optimize](https://cloudinary.com/documentation/ios_image_manipulation#image_optimizations) assets. ## Version Support | SDK Version | iOS 9+ | iOS 8 | |----------------|-----------|-----------| | 2.0.0 - 2.10.1 | V | V | | 3.0.0 - 5.x.x | V | X | ## Installation ### CocoaPods [CocoaPods](http://cocoapods.org) is a dependency manager for Swift and Objective-C Cocoa projects. To install CocoaPods: ```bash sudo gem install cocoapods ``` If you don't have a `Podfile` in your project yet, add it by running the command: ```bash pod init ``` Add the Cloudinary SDK to your `Podfile`: ```ruby source 'https://github.com/CocoaPods/Specs.git' platform :ios, '9.0' use_frameworks! target 'MyApp' do pod 'Cloudinary', '~> 5.0' end ``` Then, run the command: ```bash pod install ``` ### Carthage Create `Cartfile` ```bash touch Cartfile ``` Open `Cartfile` and enter the following line ```bash github "cloudinary/cloudinary_ios" ~> 5.0 ``` Then, run the command: ```bash carthage update --use-xcframeworks ``` A `Cartfile.resolved` file and a `Carthage` directory will appear in the same directory where your `.xcodeproj` or `.xcworkspace` is. Drag the built `.xcframework` bundles from `Carthage/Build` into the `Frameworks and Libraries` section of your application’s Xcode project. ### Swift Package Manager * File > Add Packages... > * Add https://github.com/cloudinary/cloudinary_ios.git * Select "Up to Next Major" with "5.0.0" ### Working with the Cloudinary iOS SDK Manually If you prefer not use a dependency manager, you can add Cloudinary manually by adding it as a submodule to your project: Open Terminal and navigate to your project's top level directory. If your project is not initialized as a git repository, run the command: ```bash git init ``` To add cloudinary as a git submodule, run the command: ```bash git submodule add https://github.com/cloudinary/cloudinary_ios.git ``` #### Embedded Framework 1. Drag `Cloudinary.xcodeproj` into the Project Navigator of your application's Xcode project. It should appear under your application's blue project icon. 2. Select `Cloudinary.xcodeproj` and make sure the deployment target matches that of your application target. 3. Select your application project. Under 'TARGETS' select your application, open the 'General' tab, click on the `+` button under the 'Embedded Binaries' and Select 'Cloudinary.framework'. ## Usage ### Setup To use the API, you will need a CLDCloudinary instance, which is initialized with an instance of CLDConfiguration. The CLDConfiguration must have its `cloudName` and `apiKey` properties set. Other properties are optional. See [API, URLs and access identifiers](https://cloudinary.com/documentation/api_and_access_identifiers) for more details. There are several ways to initialize CLDConfiguration. You can simply call its constructor with the desired params: ```swift let config = CLDConfiguration(cloudName: "CLOUD_NAME", apiKey: "API_KEY") ``` Another way is by passing a URL of the form: cloudinary://API_KEY:API_SECRET@CLOUD_NAME ```swift let config = CLDConfiguration(cloudinaryUrl: "cloudinary://:@") ``` You can also add the same URL as an environment parameters under `CLOUDINARY_URL`, then initialize CLDConfiguration using its static initializer ```swift let config = CLDConfiguration.initWithEnvParams() ``` Now you can create a CLDCloudinary instance to work with ```swift let cloudinary = CLDCloudinary(configuration: config) ``` ### Transform and Optimize Assets The following example generates a URL on an uploaded `sample` image: ```swift cloudinary.createUrl().generate("sample.jpg") // http://res.cloudinary.com/CLOUD_NAME/image/upload/sample.jpg ``` The following example generates an image URL of an uploaded `sample` image while transforming it to fill a 100x150 rectangle: ```swift let transformation = CLDTransformation().setWidth(100).setHeight(150).setCrop(.crop) cloudinary.createUrl().setTransformation(transformation).generate("sample.jpg") // http://res.cloudinary.com/CLOUD_NAME/image/upload/c_fill,h_150,w_100/sample.jpg ``` Another example, embedding a smaller version of an uploaded image while generating a 90x90 face detection based thumbnail: ```swift let transformation = CLDTransformation().setWidth(90).setHeight(90).setCrop(.Thumb).setGravity(.Face) cloudinary.createUrl().setTransformation(transformation).generate("sample.jpg") // http://res.cloudinary.com/CLOUD_NAME/image/upload/c_thumb,g_face,h_90,w_90/sample.jpg ``` You can provide either a Facebook name or a numeric ID of a Facebook profile or a fan page. Embedding a Facebook profile to match your graphic design is very simple: ```swift let url = cloudinary.createUrl().setTransformation(CLDTransformation().setWidth(90).setHeight(130).setGravity(.Face).setCrop(.Fill)).setType(.Facebook).generate("billclinton.jpg") // http://res.cloudinary.com/CLOUD_NAME/image/facebook/c_fill,g_face,h_130,w_90/billclinton.jpg ``` You can also chain transformations together: ```swift let transformation = CLDTransformation().setWidth(100).setHeight(150).chain().setCrop(.Fit) let url = cloudinary.createUrl().setTransformation().generate("sample.jpg") // http://res.cloudinary.com/CLOUD_NAME/image/facebook/h_150,w_100/c_fit/sample.jpg ``` ### File Upload ##### 1. Signed upload Uploading to your cloud is very straightforward. In the following example the file located at `fileUrl` is uploaded to your cloud: ```swift cloudinary.createUploader().upload(file: fileUrl) ``` `fileUrl` can point to either a local or a remote file. You can also upload data: ```swift cloudinary.createUploader().upload(data: data) ``` The uploaded image is assigned a randomly generated public ID, which is returned as part of the response. You can pass an instance of `CLDUploadRequestParams` for extra parameters you'd want to pass as part of the upload request. For example, you can specify your own public ID instead of a randomly generated one. For a full list of available upload parameters, see [the Upload API Reference](https://cloudinary.com/documentation/image_upload_api_reference#upload) documentation. You can also pass a `progress` closure that is called periodically during the data transfer, and a `completionHandler` closure to be called once the request has finished, holding either the response object or the error. In the following example, we apply an incoming transformation as part of the upload request, the transformation is applied before saving the image in the cloud. We also specify a public ID and pass closures for the upload progress and completion handler. ```swift let params = CLDUploadRequestParams() params.setTransformation(CLDTransformation().setGravity(.NorthWest)) params.setPublicId("my_public_id") let request = cloudinary.createUploader().upload(file: fileUrl, params: params, progress: { (bytes, totalBytes, totalBytesExpected) in // Handle progress }) { (response, error) in // Handle response } ``` ##### 2. Unsigned uploads using [Upload Presets.](https://cloudinary.com/documentation/ios_image_and_video_upload#unsigned_upload) You can create an upload preset in your Cloudinary account console, defining rules that limit the formats, transformations, dimensions and more. Once the preset is defined, it's name is supplied when calling upload. An upload call will only succeed if the preset name is used and the resource is within the preset's pre-defined limits. The following example uploads a local resource, assuming a preset named 'sample_preset' already exists in the account: ```swift let request = cloudinary.createUploader().upload(url: file, uploadPreset: "sample_preset", params: CLDUploadRequestParams()).response({ (response, error) in // Handle response }) ``` Every upload request returns a CLDUploadRequest instance, allowing options such as cancelling, suspending or resuming it. ### File Download The SDK provides some convenient methods for downloading files from your cloud: ```swift cloudinary.createDownloader().fetchImage(url) ``` You can also pass a `progress` closure that is called periodically during the data transfer, and a `completionHandler` closure to be called once the request has finished, holding either the fetched UIImage or an error. ```swift let request = cloudinary.createDownloader().fetchImage(url, progress: { (bytes, totalBytes, totalBytesExpected) in // Handle progress }) { (responseImage, error) in // Handle response } ``` Every download request returns an instance implementing CLDNetworkDataRequest, allowing options such as cancelling, suspending or resuming it. The downloaded image is cached both to the memory and the disk (customizable). The disk cache size is limited and can be changed. ## Contributions See [contributing guidelines](/CONTRIBUTING.md). ## Get Help If you run into an issue or have a question, you can either: - [Open a Github issue](https://github.com/cloudinary/cloudinary_ios/issues) (for issues related to the SDK) - [Open a support ticket](https://cloudinary.com/contact) (for issues related to your account) ## About Cloudinary Cloudinary is a powerful media API for websites and mobile apps alike, Cloudinary enables developers to efficiently manage, transform, optimize, and deliver images and videos through multiple CDNs. Ultimately, viewers enjoy responsive and personalized visual-media experiences—irrespective of the viewing device. ## Additional Resources - [Cloudinary Transformation and REST API References](https://cloudinary.com/documentation/cloudinary_references): Comprehensive references, including syntax and examples for all SDKs. - [DevJams](https://www.youtube.com/playlist?list=PL8dVGjLA2oMr09amgERARsZyrOz_sPvqw): Cloudinary developer podcasts on YouTube. - [Cloudinary Academy](https://training.cloudinary.com/): Free self-paced courses, instructor-led virtual courses, and on-site courses. - [Code Explorers and Feature Demos](https://cloudinary.com/documentation/code_explorers_demos_index): A one-stop shop for all code explorers, Postman collections, and feature demos found in the docs. - [Cloudinary Roadmap](https://cloudinary.com/roadmap): Your chance to follow, vote, or suggest what Cloudinary should develop next. - [Cloudinary Facebook Community](https://www.facebook.com/groups/CloudinaryCommunity): Learn from and offer help to other Cloudinary developers. - [Cloudinary Account Registration](https://cloudinary.com/users/register/free): Free Cloudinary account registration. - [Cloudinary Website](https://cloudinary.com) ## Licence Released under the MIT license. ================================================ FILE: tools/get_test_cloud.sh ================================================ #!/usr/bin/env bash function test_cloud { CLOUD_DETAILS=$(curl -X POST \-H 'Content-type:application/json' \https://sub-account-testing.cloudinary.com/create_sub_account \--data '{"prefix" : "ios-test-cloud"}') echo ${CLOUD_DETAILS} | python -c 'import json,sys;c=json.load(sys.stdin)["payload"];print("cloudinary://%s:%s@%s" % (c["cloudApiKey"], c["cloudApiSecret"], c["cloudName"]))' } test_cloud