Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/cron-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
strategy:
matrix:
include:
- ios: "26.0"
device: "iPhone 17 Pro"
setup_runtime: false
- ios: "18.5"
device: "iPhone 16 Pro"
setup_runtime: false
Expand All @@ -36,7 +39,7 @@ jobs:
fail-fast: false
runs-on: macos-15
env:
XCODE_VERSION: "16.4"
XCODE_VERSION: "26.0"
steps:
- uses: actions/checkout@v4.1.1
- uses: ./.github/actions/bootstrap
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/smoke-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ concurrency:

env:
HOMEBREW_NO_INSTALL_CLEANUP: 1 # Disable cleanup for homebrew, we don't need it on CI
IOS_SIMULATOR_DEVICE: "iPhone 16 Pro (18.5)"
IOS_SIMULATOR_DEVICE: "iPhone 17 Pro (26.0)"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_PR_NUM: ${{ github.event.pull_request.number }}

Expand Down Expand Up @@ -71,7 +71,6 @@ jobs:
name: test-coverage-${{ github.event.pull_request.number }}
path: reports/sonarqube-generic-coverage.xml


test_ui:
name: Test UI
runs-on: macos-15
Expand Down
71 changes: 57 additions & 14 deletions .swiftformat
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,65 @@
--header "\nCopyright © {year} Stream.io Inc. All rights reserved.\n"
--swiftversion 6.0

--ifdef no-indent
--disable redundantType
--disable extensionAccessControl
--disable andOperator
--disable hoistPatternLet
--disable typeSugar
--disable opaqueGenericParameters
--disable genericExtensions
--disable preferForLoop
--disable redundantGet # it removes get async throws from getters
--rules blankLinesAroundMark
--rules blankLinesAtEndOfScope
--rules blankLinesAtStartOfScope
--rules blankLinesBetweenScopes
--rules braces
--rules consecutiveBlankLines
--rules consecutiveSpaces
--rules duplicateImports
--rules elseOnSameLine
--rules emptyBraces
--rules enumNamespaces
--rules fileHeader
--rules indent
--rules initCoderUnavailable
--rules isEmpty
--rules leadingDelimiters
--rules linebreakAtEndOfFile
--rules linebreaks
--rules modifierOrder
--rules numberFormatting
--rules redundantBackticks
--rules redundantBreak
--rules redundantExtensionACL
--rules redundantFileprivate
--rules redundantLet
--rules redundantLetError
--rules redundantNilInit
--rules redundantObjc
--rules redundantPattern
--rules redundantRawValues
--rules redundantVoidReturnType
--rules semicolons
--rules sortedImports
--rules spaceAroundBraces
--rules spaceAroundBrackets
--rules spaceAroundComments
--rules spaceAroundGenerics
--rules spaceAroundOperators
--rules spaceAroundParens
--rules spaceInsideBraces
--rules spaceInsideBrackets
--rules spaceInsideComments
--rules spaceInsideGenerics
--rules spaceInsideParens
--rules strongOutlets
--rules strongifiedSelf
--rules todos
--rules trailingCommas
--rules trailingSpace
--rules unusedArguments
--rules void
--rules wrap
--rules wrapArguments
--rules wrapAttributes
--rules yodaConditions

# Rules inferred from Swift Standard Library:
--disable anyObjectProtocol, wrapMultilineStatementBraces
# Configuration for enabled rules
--ifdef no-indent
--indent 4
--enable isEmpty
--disable redundantParens # it generates mistakes for e.g. "if (a || b), let x = ... {}"
--semicolons inline
--nospaceoperators ..., ..< # what about ==, +=?
--commas inline
Expand Down
10 changes: 8 additions & 2 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ excluded:
- vendor/bundle

only_rules:
# Currently enabled autocorrectable rules
- attribute_name_spacing
- closing_brace
- colon
Expand All @@ -26,14 +25,15 @@ only_rules:
- empty_enum_arguments
- empty_parameters
- empty_parentheses_with_trailing_closure
- explicit_init
- file_name_no_space
- joined_default_parameter
- leading_whitespace
- legacy_cggeometry_functions
- legacy_constant
- legacy_constructor
- legacy_nsgeometry_functions
- mark
- multiline_arguments
- no_space_in_method_call
- prefer_type_checking
- private_over_fileprivate
Expand All @@ -59,5 +59,11 @@ only_rules:
- vertical_whitespace
- void_return

multiline_arguments:
only_enforce_after_first_closure_on_first_line: true

trailing_whitespace:
ignores_empty_lines: true

file_name_no_space:
severity: error
4 changes: 2 additions & 2 deletions Githubfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
export YEETD_VERSION='1.0'
export SONAR_VERSION='7.2.0.5079'
export IPSW_VERSION='3.1.592'
export SWIFT_LINT_VERSION='0.55.1'
export SWIFT_FORMAT_VERSION='0.56.4'
export SWIFT_LINT_VERSION='0.59.1'
export SWIFT_FORMAT_VERSION='0.58.2'
export SWIFT_GEN_VERSION='6.5.1'
2 changes: 1 addition & 1 deletion Sources/StreamCore/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>0.2.1</string>
<string>0.3.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
Expand Down
31 changes: 31 additions & 0 deletions Sources/StreamCore/Query/Filter+Local.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,37 @@ extension Filter {
return field.matcher.match(model, to: value, filterOperator: filterOperator)
}
}

/// Checks whether this filter contains a specific field.
///
/// This method recursively traverses compound filters (AND/OR) to determine if any subfilter
/// operates on the specified field.
///
/// - Parameter field: The field to search for in the filter hierarchy.
/// - Returns: `true` if the filter contains the specified field, `false` otherwise.
///
/// ## Example Usage
/// ```swift
/// let nameField = FilterField("name", localValue: { $0.name })
/// let ageField = FilterField("age", localValue: { $0.age })
///
/// let filter = Filter.and([
/// Filter.equal(nameField, "John"),
/// Filter.greater(ageField, 25)
/// ])
///
/// let containsName = filter.contains(nameField) // true
/// let containsEmail = filter.contains(emailField) // false
/// ```
public func contains<Field>(_ field: Field) -> Bool where Field == Self.FilterField {
switch filterOperator {
case .and, .or:
guard let subfilters = value as? [Self] else { return false }
return subfilters.contains(where: { $0.contains(field) })
default:
return self.field.rawValue == field.rawValue
}
}
}

private struct FilterMatcher<Model, Value>: Sendable where Model: Sendable, Value: FilterValue {
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamCore/Utils/SystemEnvironment+Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import Foundation

enum SystemEnvironment {
/// A Stream Core version.
public static let version: String = "0.2.1"
public static let version: String = "0.3.0"
}
2 changes: 1 addition & 1 deletion Sources/StreamCoreUI/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>0.2.1</string>
<string>0.3.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
Expand Down
132 changes: 132 additions & 0 deletions Tests/StreamCoreTests/Query/Filter_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1242,4 +1242,136 @@ struct Filter_Tests {
#expect(exactMatchFilter.matches(TestUser(name: "JOHN"))) // Case-insensitive exact match should work
#expect(exactMatchFilter.matches(TestUser(name: "john"))) // Case-insensitive exact match should work
}

// MARK: - Filter Field Containment Tests

@Test func containsSimpleFilter() {
let nameFilter = TestFilter.equal(.name, "John")
let ageFilter = TestFilter.greater(.age, 25)
let emailFilter = TestFilter.equal(.email, "john@example.com")

// Test that a simple filter contains its own field
#expect(nameFilter.contains(.name))
#expect(ageFilter.contains(.age))
#expect(emailFilter.contains(.email))

// Test that a simple filter does not contain different fields
#expect(!nameFilter.contains(.age))
#expect(!nameFilter.contains(.email))
#expect(!ageFilter.contains(.name))
#expect(!ageFilter.contains(.email))
#expect(!emailFilter.contains(.name))
#expect(!emailFilter.contains(.age))
}

@Test func containsAndFilter() {
let nameFilter = TestFilter.equal(.name, "John")
let ageFilter = TestFilter.greater(.age, 25)
let emailFilter = TestFilter.equal(.email, "john@example.com")

let andFilter = TestFilter.and([nameFilter, ageFilter, emailFilter])

// Test that AND filter contains all its subfilter fields
#expect(andFilter.contains(.name))
#expect(andFilter.contains(.age))
#expect(andFilter.contains(.email))

// Test that AND filter does not contain fields not in its subfilters
#expect(!andFilter.contains(.height))
#expect(!andFilter.contains(.tags))
#expect(!andFilter.contains(.isActive))
}

@Test func containsOrFilter() {
let nameFilter = TestFilter.equal(.name, "John")
let ageFilter = TestFilter.greater(.age, 25)
let emailFilter = TestFilter.equal(.email, "john@example.com")

let orFilter = TestFilter.or([nameFilter, ageFilter, emailFilter])

// Test that OR filter contains all its subfilter fields
#expect(orFilter.contains(.name))
#expect(orFilter.contains(.age))
#expect(orFilter.contains(.email))

// Test that OR filter does not contain fields not in its subfilters
#expect(!orFilter.contains(.height))
#expect(!orFilter.contains(.tags))
#expect(!orFilter.contains(.isActive))
}

@Test func containsNestedCompoundFilters() {
let nameFilter = TestFilter.equal(.name, "John")
let ageFilter = TestFilter.greater(.age, 25)
let emailFilter = TestFilter.equal(.email, "john@example.com")
let heightFilter = TestFilter.less(.height, 200.0)
let tagsFilter = TestFilter.contains(.tags, "developer")

// Create nested AND filter
let innerAndFilter = TestFilter.and([nameFilter, ageFilter])
let outerAndFilter = TestFilter.and([innerAndFilter, emailFilter, heightFilter])

// Test that nested AND filter contains all fields from all levels
#expect(outerAndFilter.contains(.name))
#expect(outerAndFilter.contains(.age))
#expect(outerAndFilter.contains(.email))
#expect(outerAndFilter.contains(.height))

// Test that nested AND filter does not contain fields not in any subfilter
#expect(!outerAndFilter.contains(.tags))
#expect(!outerAndFilter.contains(.isActive))

// Create nested OR filter
let innerOrFilter = TestFilter.or([emailFilter, heightFilter])
let outerOrFilter = TestFilter.or([innerOrFilter, tagsFilter])

// Test that nested OR filter contains all fields from all levels
#expect(outerOrFilter.contains(.email))
#expect(outerOrFilter.contains(.height))
#expect(outerOrFilter.contains(.tags))

// Test that nested OR filter does not contain fields not in any subfilter
#expect(!outerOrFilter.contains(.name))
#expect(!outerOrFilter.contains(.age))
#expect(!outerOrFilter.contains(.isActive))
}

@Test func containsMixedCompoundFilters() {
let nameFilter = TestFilter.equal(.name, "John")
let ageFilter = TestFilter.greater(.age, 25)
let emailFilter = TestFilter.equal(.email, "john@example.com")
let heightFilter = TestFilter.less(.height, 200.0)
let tagsFilter = TestFilter.contains(.tags, "developer")

// Create mixed compound filter: AND containing OR
let orFilter = TestFilter.or([emailFilter, heightFilter])
let mixedFilter = TestFilter.and([nameFilter, ageFilter, orFilter, tagsFilter])

// Test that mixed filter contains all fields from all levels
#expect(mixedFilter.contains(.name))
#expect(mixedFilter.contains(.age))
#expect(mixedFilter.contains(.email))
#expect(mixedFilter.contains(.height))
#expect(mixedFilter.contains(.tags))

// Test that mixed filter does not contain fields not in any subfilter
#expect(!mixedFilter.contains(.isActive))
#expect(!mixedFilter.contains(.homepage))
}

@Test func containsSingleElementCompoundFilters() {
let nameFilter = TestFilter.equal(.name, "John")

// Test AND filter with single element
let singleAndFilter = TestFilter.and([nameFilter])
#expect(singleAndFilter.contains(.name))
#expect(!singleAndFilter.contains(.age))
#expect(!singleAndFilter.contains(.email))

// Test OR filter with single element
let singleOrFilter = TestFilter.or([nameFilter])
#expect(singleOrFilter.contains(.name))
#expect(!singleOrFilter.contains(.age))
#expect(!singleOrFilter.contains(.email))
}
}
2 changes: 1 addition & 1 deletion fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require 'json'
require 'net/http'
import 'Sonarfile'

xcode_version = ENV['XCODE_VERSION'] || '16.4'
xcode_version = ENV['XCODE_VERSION'] || '26.0'
xcode_project = 'StreamCore.xcodeproj'
sdk_names = ['StreamCore', 'StreamCoreUI']
github_repo = ENV['GITHUB_REPOSITORY'] || 'GetStream/stream-core-swift'
Expand Down
Loading