diff --git a/DXFeedFramework.xcodeproj/project.pbxproj b/DXFeedFramework.xcodeproj/project.pbxproj index ea69d7bb6..c35dbd88c 100644 --- a/DXFeedFramework.xcodeproj/project.pbxproj +++ b/DXFeedFramework.xcodeproj/project.pbxproj @@ -121,6 +121,8 @@ 6447A5ED2A8FCC2200739CCF /* UtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6447A5EC2A8FCC2200739CCF /* UtilsTest.swift */; }; 6447A5EF2A8FD1CD00739CCF /* DayUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6447A5EE2A8FD1CD00739CCF /* DayUtil.swift */; }; 6447A5F12A8FDD1B00739CCF /* BitUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6447A5F02A8FDD1B00739CCF /* BitUtil.swift */; }; + 644B95E72BC542F600E95CB7 /* DXAttachTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644B95E62BC542F600E95CB7 /* DXAttachTest.swift */; }; + 644B95E92BC54E4E00E95CB7 /* MarketEvent+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644B95E82BC54E4E00E95CB7 /* MarketEvent+Ext.swift */; }; 644BD75D2A44726F00A0BF99 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644BD75C2A44726F00A0BF99 /* AppDelegate.swift */; }; 644BD7612A44726F00A0BF99 /* ARQuoteTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644BD7602A44726F00A0BF99 /* ARQuoteTableViewController.swift */; }; 644BD7662A44727000A0BF99 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 644BD7652A44727000A0BF99 /* Assets.xcassets */; }; @@ -673,6 +675,8 @@ 6447A5EC2A8FCC2200739CCF /* UtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilsTest.swift; sourceTree = ""; }; 6447A5EE2A8FD1CD00739CCF /* DayUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayUtil.swift; sourceTree = ""; }; 6447A5F02A8FDD1B00739CCF /* BitUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitUtil.swift; sourceTree = ""; }; + 644B95E62BC542F600E95CB7 /* DXAttachTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXAttachTest.swift; sourceTree = ""; }; + 644B95E82BC54E4E00E95CB7 /* MarketEvent+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MarketEvent+Ext.swift"; sourceTree = ""; }; 644BD75A2A44726F00A0BF99 /* DXARQuoteTableApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DXARQuoteTableApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 644BD75C2A44726F00A0BF99 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 644BD7602A44726F00A0BF99 /* ARQuoteTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARQuoteTableViewController.swift; sourceTree = ""; }; @@ -1297,6 +1301,7 @@ 6447A5DE2A8E56FC00739CCF /* IIndexedEvent.swift */, 6447A5DC2A8E56CF00739CCF /* ITimeSeriesEvent.swift */, 642BE4C92A2E1C640052340A /* MarketEvent.swift */, + 644B95E82BC54E4E00E95CB7 /* MarketEvent+Ext.swift */, 64BA92682A306E6000BE26A0 /* TradeBase.swift */, 6447A5E02A8E5A5400739CCF /* IndexedEventSource.swift */, 64C771F32A94A86E009868C2 /* Side.swift */, @@ -1631,6 +1636,7 @@ 64EAA1A12B7A38F8005087BC /* DXPromiseTest.swift */, 64EAA1A52B838ED3005087BC /* DXSnapshotProcessorTest.swift */, 64820AAE2BB2E26100BDFD0B /* DXOtcMarketOrderTest.swift */, + 644B95E62BC542F600E95CB7 /* DXAttachTest.swift */, ); path = DXFeedFrameworkTests; sourceTree = ""; @@ -2387,6 +2393,7 @@ 640C3FDC2A618B2000555161 /* MarketEventSymbols.swift in Sources */, 641C64AC2B331B160023CFAD /* ObservableSubscriptionChangeListener.swift in Sources */, 642BE4D42A2F5D730052340A /* TimeAndSale+Ext.swift in Sources */, + 644B95E92BC54E4E00E95CB7 /* MarketEvent+Ext.swift in Sources */, 64ACBCD92A279F7900032C53 /* DXEventListener.swift in Sources */, 64ECD6852A9DDF6200B36935 /* DXInstrumentProfileCollector.swift in Sources */, 64C004722BA074500009F7C9 /* OtcMarketsOrder.swift in Sources */, @@ -2577,6 +2584,7 @@ 64098F672ACEB6F70020D741 /* DXConnectionStateTests.swift in Sources */, 648BD56F2AC582AB004A3A95 /* DateTimeParserTest.swift in Sources */, 64ACBCEA2A28DDDA00032C53 /* TestEventListener.swift in Sources */, + 644B95E72BC542F600E95CB7 /* DXAttachTest.swift in Sources */, 6401A5152A582134009BA686 /* IsolateTest.swift in Sources */, 6406F25B2AD987EB00B58C42 /* PublisherTest.swift in Sources */, 64ACBCCF2A27851C00032C53 /* FeedTest.swift in Sources */, diff --git a/DXFeedFramework/Api/DXFeed.swift b/DXFeedFramework/Api/DXFeed.swift index 6c1780850..f1f6f4852 100644 --- a/DXFeedFramework/Api/DXFeed.swift +++ b/DXFeedFramework/Api/DXFeed.swift @@ -12,7 +12,7 @@ import Foundation public class DXFeed { /// Feed native wrapper. private let native: NativeFeed - + internal var nativeFeed: NativeFeed { return native } @@ -267,7 +267,7 @@ public extension DXFeed { } return task } - + /// Requests time series of events for the specified event type, symbol, and a range of time. /// /// This method works only for event types that implement ``ITimeSeriesEvent`` interface. diff --git a/DXFeedFramework/Api/DXFeedSubscription.swift b/DXFeedFramework/Api/DXFeedSubscription.swift index 39b4ba47d..d26b1fcd7 100644 --- a/DXFeedFramework/Api/DXFeedSubscription.swift +++ b/DXFeedFramework/Api/DXFeedSubscription.swift @@ -12,7 +12,7 @@ import Foundation public class DXFeedSubscription { /// Subscription native wrapper. private let native: NativeSubscription - + internal var nativeSubscription: NativeSubscription { return native } @@ -177,6 +177,6 @@ public extension DXFeedSubscription { /// - feed: The ``DXFeed`` to detach from. /// - Throws: GraalException. Rethrows exception from Java. func detach(feed: DXFeed) throws { - try native.detach(feed: feed.nativeFeed) + try native.detach(feed: feed.nativeFeed) } } diff --git a/DXFeedFramework/Api/Osub/TimeSeriesSubscriptionSymbol.swift b/DXFeedFramework/Api/Osub/TimeSeriesSubscriptionSymbol.swift index fae2962e2..8f0dd9702 100644 --- a/DXFeedFramework/Api/Osub/TimeSeriesSubscriptionSymbol.swift +++ b/DXFeedFramework/Api/Osub/TimeSeriesSubscriptionSymbol.swift @@ -52,7 +52,7 @@ public class TimeSeriesSubscriptionSymbol: GenericIndexedEventSubscriptionSymbol /// Custom symbol has to return string representation. public override var stringValue: String { - return "\(symbol){fromTime=\((try? DXTimeFormat.defaultTimeFormat?.withMillis?.format(value: fromTime)) ?? "")}" + return "\(symbol.description){fromTime=\((try? DXTimeFormat.defaultTimeFormat?.withMillis?.format(value: fromTime)) ?? "")}" } } diff --git a/DXFeedFramework/Events/Market/Candles/CandlePriceLevel.swift b/DXFeedFramework/Events/Market/Candles/CandlePriceLevel.swift index 032508f8c..5d5f86e73 100644 --- a/DXFeedFramework/Events/Market/Candles/CandlePriceLevel.swift +++ b/DXFeedFramework/Events/Market/Candles/CandlePriceLevel.swift @@ -114,7 +114,7 @@ public class CandlePriceLevel { public func toString() -> String { return stringDescription } - + /// Returns full string representation of this candle price level attribute. /// /// It is contains attribute key and its value. diff --git a/DXFeedFramework/Events/Market/Candles/CandleSymbol.swift b/DXFeedFramework/Events/Market/Candles/CandleSymbol.swift index 45967fdb2..dca9d8f22 100644 --- a/DXFeedFramework/Events/Market/Candles/CandleSymbol.swift +++ b/DXFeedFramework/Events/Market/Candles/CandleSymbol.swift @@ -11,7 +11,7 @@ import Foundation /// representation of the candle symbol for subscription. /// /// [For more details see](https://docs.dxfeed.com/dxfeed/api/com/dxfeed/event/candle/CandleSymbol.html) -public class CandleSymbol { +public class CandleSymbol: CustomStringConvertible { /// Returns string representation of this symbol. public private(set) var symbol: String? /// Gets base market symbol without attributes. @@ -85,6 +85,11 @@ public class CandleSymbol { func toString() -> String { return symbol ?? "null" } + + public var description: String { + return toString() + } + /// Converts the given string symbol into the candle symbol object. /// /// - Throws: ArgumentException/invalidOperationException(_:) diff --git a/DXFeedFramework/Events/Market/Extra/MarketEvent+Ext.swift b/DXFeedFramework/Events/Market/Extra/MarketEvent+Ext.swift new file mode 100644 index 000000000..4fcdd6888 --- /dev/null +++ b/DXFeedFramework/Events/Market/Extra/MarketEvent+Ext.swift @@ -0,0 +1,32 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation + +public protocol ScopingFunctionSupported { + associatedtype ASelf = Self where ASelf: ScopingFunctionSupported + + func `let`(block: (ASelf) -> T) -> T + func also(block: (ASelf) -> Void) -> ASelf +} + +public extension ScopingFunctionSupported { + @inline(__always) + @discardableResult + func `let`(block: (Self) -> T) -> T { + return block(self) + } + + @inline(__always) + @discardableResult + func also(block: (Self) -> Void) -> Self { + block(self) + return self + } +} + +extension MarketEvent: ScopingFunctionSupported {} diff --git a/DXFeedFrameworkTests/DXAttachTest.swift b/DXFeedFrameworkTests/DXAttachTest.swift new file mode 100644 index 000000000..c56f31c59 --- /dev/null +++ b/DXFeedFrameworkTests/DXAttachTest.swift @@ -0,0 +1,72 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import XCTest +@testable import DXFeedFramework + +final class DXAttachTest: XCTestCase { + let detachedSymbol = "TEST1" + let attachedSymbol = "TEST2" + + func testAttachDetach() throws { + let endpoint = try DXEndpoint.create() + do { + if let feed = endpoint.getFeed(), let publisher = endpoint.getPublisher() { + let subcription = try feed.createSubscription([TimeAndSale.self]) + let expectation1 = expectation(description: "Events received") + let expectation2 = expectation(description: "Events received") + let listener = AnonymousClass { anonymCl in + anonymCl.callback = { events in + events.forEach { event in + switch event.eventSymbol { + case self.detachedSymbol: + XCTFail("Received detached symbol \(event.toString())") + case self.attachedSymbol: + if event.timeAndSale.askPrice == 100 { + expectation1.fulfill() + } else if event.timeAndSale.askPrice == 200 { + expectation2.fulfill() + } + default: + XCTFail("Unexpected symbol \(event.toString())") + } + } + } + return anonymCl + } + try subcription.add(listener: listener) + try subcription.addSymbols(detachedSymbol) + try feed.detach(subscription: subcription) + try publisher.publish(events: [TimeAndSale(detachedSymbol)]) + try feed.attach(subscription: subcription) + try feed.attach(subscription: subcription) + try subcription.addSymbols(attachedSymbol) + + try publisher.publish(events: [TimeAndSale(attachedSymbol).also(block: { tns in + tns.askPrice = 100 + })]) + wait(for: [expectation1], timeout: 1) + try subcription.detach(feed: feed) + try publisher.publish(events: [TimeAndSale(detachedSymbol)]) + try subcription.attach(feed: feed) + try publisher.publish(events: [TimeAndSale(attachedSymbol).also(block: { tns in + tns.askPrice = 200 + })]) + wait(for: [expectation2], timeout: 1) + let symbols = try subcription.getSymbols().map { symbol in + symbol.stringValue + } + XCTAssert(Set(symbols) == Set([attachedSymbol, detachedSymbol])) + } else { + XCTAssert(false, "Subscription returned null") + } + } catch { + XCTAssert(false, "Error during attach/detach \(error)") + } + } + +} diff --git a/DXFeedFrameworkTests/FeedTest.swift b/DXFeedFrameworkTests/FeedTest.swift index 9fe8baa9e..f01e92843 100644 --- a/DXFeedFrameworkTests/FeedTest.swift +++ b/DXFeedFrameworkTests/FeedTest.swift @@ -13,7 +13,7 @@ final class FeedTest: XCTestCase { _ = Isolate.shared } - func testInitializationWithNilNativeSubscription() { + func testAInitializationWithNilNativeSubscription() { XCTAssertThrowsError(try DXFeedSubscription(native: nil, types: [Quote.self])) { error in // Assert XCTAssertTrue(error is ArgumentException) @@ -30,8 +30,12 @@ final class FeedTest: XCTestCase { XCTAssertNotNil((TimeSeriesSubscriptionSymbol(symbol: symbol, fromTime: 0) as Any) as? Symbol, "String is not a symbol") - let symbol1 = try CandleSymbol.valueOf("test") - let testString = TimeSeriesSubscriptionSymbol(symbol: symbol1, fromTime: 10).stringValue + let symbol1 = try CandleSymbol.valueOf("test123") + let testString = TimeSeriesSubscriptionSymbol(symbol: symbol1, fromTime: 30).stringValue + XCTAssertEqual("test123{fromTime", + testString.substring(to: testString.index(of: "=")!)) + XCTAssertEqual(".030}", + testString.substring(from: testString.index(of: ".")!)) } func testSetGetSymbols() throws { @@ -62,65 +66,4 @@ final class FeedTest: XCTestCase { XCTAssert(false, "Subscription returned null") } } - - - func testAttachDetach() throws { - let detachedSymbol = "TEST1" - let attachedSymbol = "TEST2" - let endpoint = try DXEndpoint.create() - do { - if let feed = endpoint.getFeed(), let publisher = endpoint.getPublisher() { - let subcription = try feed.createSubscription([TimeAndSale.self]) - let expectation1 = expectation(description: "Events received") - let expectation2 = expectation(description: "Events received") - let listener = AnonymousClass { anonymCl in - anonymCl.callback = { events in - print(events) - events.forEach { event in - switch event.eventSymbol { - case detachedSymbol: - XCTFail("Received detached symbol \(event.toString())") - case attachedSymbol: - if event.timeAndSale.askPrice == 100 { - expectation1.fulfill() - } else if event.timeAndSale.askPrice == 200 { - expectation2.fulfill() - } - default: - XCTFail("Unexpected symbol \(event.toString())") - } - } - } - return anonymCl - } - try subcription.add(listener: listener) - try subcription.addSymbols(detachedSymbol) - try feed.detach(subscription: subcription) - try publisher.publish(events: [TimeAndSale(detachedSymbol)]) - try feed.attach(subscription: subcription) - try feed.attach(subscription: subcription) - try subcription.addSymbols(attachedSymbol) - - let tns1 = TimeAndSale(attachedSymbol) - tns1.askPrice = 100 - try publisher.publish(events: [tns1]) - wait(for: [expectation1], timeout: 1) - - try subcription.detach(feed: feed) - try publisher.publish(events: [TimeAndSale(detachedSymbol)]) - try subcription.attach(feed: feed) - tns1.askPrice = 200 - try publisher.publish(events: [tns1]) - wait(for: [expectation2], timeout: 1) - let symbols = try subcription.getSymbols().map { symbol in - symbol.stringValue - } - XCTAssert(Set(symbols) == Set([attachedSymbol, detachedSymbol])) - } else { - XCTAssert(false, "Subscription returned null") - } - } catch { - XCTAssert(false, "Error during attach/detach \(error)") - } - } } diff --git a/Samples/QuoteTableApp/AddSymbolsViewController.swift b/Samples/QuoteTableApp/AddSymbolsViewController.swift index 6409c2ccd..358389370 100644 --- a/Samples/QuoteTableApp/AddSymbolsViewController.swift +++ b/Samples/QuoteTableApp/AddSymbolsViewController.swift @@ -42,7 +42,6 @@ class AddSymbolsViewController: UIViewController { dataProvider.allSymbols = symbols } - func changeActivityIndicator() { if symbols.count == 0 { activityIndicator.isHidden = false diff --git a/Samples/QuoteTableApp/QuoteTableViewController.swift b/Samples/QuoteTableApp/QuoteTableViewController.swift index 83fd11187..29877eb9c 100644 --- a/Samples/QuoteTableApp/QuoteTableViewController.swift +++ b/Samples/QuoteTableApp/QuoteTableViewController.swift @@ -29,7 +29,7 @@ class QuoteTableViewController: UIViewController { quoteTableView.separatorStyle = .none - NotificationCenter.default.addObserver(forName: .selectedSymbolsChanged, object: nil, queue: nil) { [weak self] (notification) in + NotificationCenter.default.addObserver(forName: .selectedSymbolsChanged, object: nil, queue: nil) { [weak self] (_) in guard let strongSelf = self else { return } @@ -55,7 +55,7 @@ class QuoteTableViewController: UIViewController { self.subscribe(false) } } - + func subscribe(_ unlimited: Bool) { if endpoint == nil { try? SystemProperty.setProperty(DXEndpoint.ExtraPropery.heartBeatTimeout.rawValue, "15s") @@ -99,7 +99,7 @@ extension QuoteTableViewController: DXEventListener { func receiveEvents(_ events: [MarketEvent]) { events.forEach { event in switch event.type { - case .quote: + case .quote: dataSource[event.eventSymbol]?.update(event.quote) case .profile: dataSource[event.eventSymbol]?.update(event.profile.descriptionStr ?? "") diff --git a/Samples/QuoteTableApp/SymbolCell.swift b/Samples/QuoteTableApp/SymbolCell.swift index 7dfb72f3d..e0026b40c 100644 --- a/Samples/QuoteTableApp/SymbolCell.swift +++ b/Samples/QuoteTableApp/SymbolCell.swift @@ -33,5 +33,4 @@ class SymbolCell: UITableViewCell { enabledSwitch?.isOn = check } - }