Merge remote-tracking branch 'upstream/main'
Deploy Artifact Proxy Dev / ☁️ Artifact Proxy (push) Has been cancelled
Deploy Discord GCP Alerts / ☁️ Discord GCP Alerts (push) Has been cancelled
ci / 📄 License Check (push) Has been cancelled
ci / ✅ Semantic Pull Request (push) Has been cancelled
ci / 🔤 Check Spelling (push) Has been cancelled
ci / 👀 Detect Changes (push) Has been cancelled
Shorebird CI / changes (push) Has been cancelled
Shorebird CI / CSpell (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (${{ matrix.os }}) (push) Has been cancelled
ci / 🔎 Verify ${{ matrix.package }} (push) Has been cancelled
ci / ci (push) Has been cancelled
Shorebird CI / artifact_proxy (push) Has been cancelled
Shorebird CI / dex (push) Has been cancelled
Shorebird CI / discord_gcp_alerts (push) Has been cancelled
Shorebird CI / flutter_version_resolver (push) Has been cancelled
Shorebird CI / jwt (push) Has been cancelled
Shorebird CI / scoped_deps (push) Has been cancelled
Shorebird CI / shorebird_build_trace (push) Has been cancelled
Shorebird CI / shorebird_ci (push) Has been cancelled
Shorebird CI / shorebird_cli (push) Has been cancelled
Shorebird CI / shorebird_code_push_client (push) Has been cancelled
Shorebird CI / shorebird_code_push_protocol (push) Has been cancelled
Shorebird CI / shorebird_redis_client (push) Has been cancelled
Shorebird CI / stripe_api (push) Has been cancelled
Shorebird CI / required (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (push) Has been cancelled
Deploy Artifact Proxy Dev / ☁️ Artifact Proxy (push) Has been cancelled
Deploy Discord GCP Alerts / ☁️ Discord GCP Alerts (push) Has been cancelled
ci / 📄 License Check (push) Has been cancelled
ci / ✅ Semantic Pull Request (push) Has been cancelled
ci / 🔤 Check Spelling (push) Has been cancelled
ci / 👀 Detect Changes (push) Has been cancelled
Shorebird CI / changes (push) Has been cancelled
Shorebird CI / CSpell (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (${{ matrix.os }}) (push) Has been cancelled
ci / 🔎 Verify ${{ matrix.package }} (push) Has been cancelled
ci / ci (push) Has been cancelled
Shorebird CI / artifact_proxy (push) Has been cancelled
Shorebird CI / dex (push) Has been cancelled
Shorebird CI / discord_gcp_alerts (push) Has been cancelled
Shorebird CI / flutter_version_resolver (push) Has been cancelled
Shorebird CI / jwt (push) Has been cancelled
Shorebird CI / scoped_deps (push) Has been cancelled
Shorebird CI / shorebird_build_trace (push) Has been cancelled
Shorebird CI / shorebird_ci (push) Has been cancelled
Shorebird CI / shorebird_cli (push) Has been cancelled
Shorebird CI / shorebird_code_push_client (push) Has been cancelled
Shorebird CI / shorebird_code_push_protocol (push) Has been cancelled
Shorebird CI / shorebird_redis_client (push) Has been cancelled
Shorebird CI / stripe_api (push) Has been cancelled
Shorebird CI / required (push) Has been cancelled
ci / 🎯 Build ${{ matrix.package }} (push) Has been cancelled
# Conflicts: # packages/artifact_proxy/lib/src/artifact_proxy.dart # packages/scoped_deps/pubspec.yaml # packages/shorebird_cli/pubspec.yaml # packages/shorebird_code_push_client/lib/src/code_push_client.dart
This commit is contained in:
@@ -81,7 +81,7 @@ runs:
|
|||||||
run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT
|
run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: 📊 Upload Coverage
|
- name: 📊 Upload Coverage
|
||||||
uses: codecov/codecov-action@v6
|
uses: codecov/codecov-action@v7
|
||||||
with:
|
with:
|
||||||
flags: ${{ steps.split.outputs.package_name }}
|
flags: ${{ steps.split.outputs.package_name }}
|
||||||
token: ${{ inputs.codecov_token }}
|
token: ${{ inputs.codecov_token }}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ runs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: 🎯 Setup Dart
|
- name: 🎯 Setup Dart
|
||||||
uses: dart-lang/setup-dart@v1
|
uses: dart-lang/setup-dart@v1
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
ci:
|
ci:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: 🐳 Run Redis
|
- name: 🐳 Run Redis
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
dart test --coverage=coverage && \
|
dart test --coverage=coverage && \
|
||||||
dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib --check-ignore
|
dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib --check-ignore
|
||||||
- if: inputs.has_unit_tests
|
- if: inputs.has_unit_tests
|
||||||
uses: codecov/codecov-action@v6
|
uses: codecov/codecov-action@v7
|
||||||
with:
|
with:
|
||||||
flags: ${{ inputs.package_name }}
|
flags: ${{ inputs.package_name }}
|
||||||
working-directory: ${{ inputs.package_path }}
|
working-directory: ${{ inputs.package_path }}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: 🖥️ Add Shorebird to PATH
|
- name: 🖥️ Add Shorebird to PATH
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -98,7 +98,7 @@ jobs:
|
|||||||
sudo udevadm trigger --name-match=kvm
|
sudo udevadm trigger --name-match=kvm
|
||||||
|
|
||||||
- name: 🤖 AVD Cache
|
- name: 🤖 AVD Cache
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v6
|
||||||
id: avd-cache
|
id: avd-cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -143,7 +143,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ matrix.branch }}
|
ref: ${{ matrix.branch }}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: 👀 Check Licenses
|
- name: 👀 Check Licenses
|
||||||
uses: ./.github/actions/license_check
|
uses: ./.github/actions/license_check
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: dorny/paths-filter@v4
|
- uses: dorny/paths-filter@v4
|
||||||
name: Build Detection
|
name: Build Detection
|
||||||
@@ -154,7 +154,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: 🎯 Build ${{ matrix.package }}
|
- name: 🎯 Build ${{ matrix.package }}
|
||||||
uses: ./.github/actions/dart_package
|
uses: ./.github/actions/dart_package
|
||||||
@@ -178,7 +178,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: 🎯 Build ${{ matrix.package }}
|
- name: 🎯 Build ${{ matrix.package }}
|
||||||
uses: ./.github/actions/dart_package
|
uses: ./.github/actions/dart_package
|
||||||
@@ -203,7 +203,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: 🐳 Run Redis
|
- name: 🐳 Run Redis
|
||||||
run: |
|
run: |
|
||||||
@@ -231,7 +231,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: 🔎 Verify ${{ matrix.package }}
|
- name: 🔎 Verify ${{ matrix.package }}
|
||||||
uses: ./.github/actions/verify_version
|
uses: ./.github/actions/verify_version
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📚 Git Checkout
|
- name: 📚 Git Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: 🐳 Run Redis
|
- name: 🐳 Run Redis
|
||||||
if: inputs.needs_redis
|
if: inputs.needs_redis
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
name: Publish scoped_deps
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "scoped_deps-v[0-9]+.[0-9]+.[0-9]+*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
uses: ./.github/workflows/publish_dart_package.yaml
|
|
||||||
with:
|
|
||||||
working_directory: packages/scoped_deps
|
|
||||||
tag_prefix: scoped_deps-v
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
shorebird_redis_client: ${{ steps.filter.outputs.shorebird_redis_client }}
|
shorebird_redis_client: ${{ steps.filter.outputs.shorebird_redis_client }}
|
||||||
stripe_api: ${{ steps.filter.outputs.stripe_api }}
|
stripe_api: ${{ steps.filter.outputs.stripe_api }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
# Full history so dorny/paths-filter can diff on push events.
|
# Full history so dorny/paths-filter can diff on push events.
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -221,7 +221,7 @@ jobs:
|
|||||||
name: CSpell
|
name: CSpell
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- uses: streetsidesoftware/cspell-action@v8
|
- uses: streetsidesoftware/cspell-action@v8
|
||||||
|
|||||||
+60
-1
@@ -1,9 +1,68 @@
|
|||||||
# Release Notes
|
# Release Notes
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
cspell:words pubspec erickzanardo xcframeworks cupertino codesign codecov rkishan appbundle proto tlsv kingdomseed Peetee Aditya serde bipatch
|
cspell:words pubspec erickzanardo xcframeworks cupertino codesign codecov rkishan appbundle proto tlsv kingdomseed Peetee Aditya serde bipatch GLES lipo
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## 1.6.115 (July 28, 2026)
|
||||||
|
|
||||||
|
- 🐦 Flutter 3.44.8 / Dart 3.12.2 support
|
||||||
|
- Android: fix a startup crash on vendor-modified devices caused by the
|
||||||
|
accessibility bridge failing during initialization.
|
||||||
|
- iOS/macOS: fix framework builds failing under Xcode 27 due to a `lipo`
|
||||||
|
architecture verification error.
|
||||||
|
|
||||||
|
## 1.6.114 (July 20, 2026)
|
||||||
|
|
||||||
|
- 🐦 Flutter 3.44.7 / Dart 3.12.2 support
|
||||||
|
- Android: fix a file descriptor leak that could crash apps using external
|
||||||
|
textures (e.g. video) on some GPUs such as Arm Mali.
|
||||||
|
|
||||||
|
## 1.6.113 (July 10, 2026)
|
||||||
|
|
||||||
|
- 🐛 Fix `--shorebird-trace` build tracing being silently disabled even on
|
||||||
|
Flutter versions that support it, caused by probing plain `flutter build -h`
|
||||||
|
output where the flag is hidden.
|
||||||
|
|
||||||
|
## 1.6.112 (July 10, 2026)
|
||||||
|
|
||||||
|
- 🐦 Flutter 3.44.6 / Dart 3.12.2 support
|
||||||
|
- Linux: fix a crash when building asset bundles with native assets enabled
|
||||||
|
but without a native app build directory (missing `CMakeCache.txt`).
|
||||||
|
- Android: fix a crash when running instrumented tests.
|
||||||
|
|
||||||
|
## 1.6.111 (July 7, 2026)
|
||||||
|
|
||||||
|
- 🐦 Flutter 3.44.5 / Dart 3.12.2 support
|
||||||
|
- Android: fix `libapp.so` sometimes missing from the APK or app bundle when
|
||||||
|
building with flavors, or with older app templates combined with certain
|
||||||
|
plugins.
|
||||||
|
- Impeller: fix text shadows rendering in the wrong position, separate from
|
||||||
|
the text.
|
||||||
|
- Impeller (Vulkan): fix a crash during app shutdown or rotation on some
|
||||||
|
Android devices.
|
||||||
|
- Android: silence spurious Built-in Kotlin migration warnings when building
|
||||||
|
with AGP below 9.
|
||||||
|
- 🐛 Updater: report the currently running patch number instead of the last
|
||||||
|
booted patch.
|
||||||
|
|
||||||
|
## 1.6.110 (June 25, 2026)
|
||||||
|
|
||||||
|
- 🐦 Flutter 3.44.4 / Dart 3.12.2 support
|
||||||
|
- Linux: fix bounds checking in `FlAccessibleTextField`.
|
||||||
|
|
||||||
|
## 1.6.109 (June 22, 2026)
|
||||||
|
|
||||||
|
- 🐦 Flutter 3.44.3 / Dart 3.12.2 support
|
||||||
|
- Fix a potential buffer overflow in the animated PNG decoder when parsing
|
||||||
|
malformed fdAT chunks.
|
||||||
|
- Swift Package Manager: fix crashes from concurrent directory/file/symlink
|
||||||
|
creation when building iOS or macOS apps.
|
||||||
|
- Impeller (OpenGL ES): fix a texture resource leak by ensuring the
|
||||||
|
TextureGLES destructor releases all objects it holds, including its sync
|
||||||
|
fence.
|
||||||
|
- Print a trace when skipping flavor-specific and platform-specific assets.
|
||||||
|
|
||||||
## 1.6.108 (June 12, 2026)
|
## 1.6.108 (June 12, 2026)
|
||||||
|
|
||||||
- 🐦 Flutter 3.44.2 / Dart 3.12.2 support
|
- 🐦 Flutter 3.44.2 / Dart 3.12.2 support
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
65b1e4368e98a85ea11e8ef2cffc286c3b2ad8b2
|
c15ef6379403a0a55531a058bdb2c8e55bc05c98
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ dev_dependencies:
|
|||||||
build_runner: ^2.15.0
|
build_runner: ^2.15.0
|
||||||
json_serializable: ^6.13.2
|
json_serializable: ^6.13.2
|
||||||
mocktail: ^1.0.5
|
mocktail: ^1.0.5
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ environment:
|
|||||||
sdk: ">=3.9.0 <4.0.0"
|
sdk: ">=3.9.0 <4.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
meta: ^1.16.0
|
meta: ^1.18.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
path: ^1.9.1
|
path: ^1.9.1
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ dev_dependencies:
|
|||||||
build_runner: ^2.15.0
|
build_runner: ^2.15.0
|
||||||
json_serializable: ^6.13.2
|
json_serializable: ^6.13.2
|
||||||
mocktail: ^1.0.5
|
mocktail: ^1.0.5
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
|
|||||||
@@ -15,4 +15,4 @@ dependencies:
|
|||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
mocktail: ^1.0.5
|
mocktail: ^1.0.5
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ dependencies:
|
|||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
http: ^1.5.0
|
http: ^1.5.0
|
||||||
json_annotation: ^4.12.0
|
json_annotation: ^4.12.0
|
||||||
meta: ^1.7.0
|
meta: ^1.18.3
|
||||||
pointycastle: ^4.0.0
|
pointycastle: ^4.0.0
|
||||||
rsa_pkcs: ^2.0.0
|
rsa_pkcs: ^2.0.0
|
||||||
ttl_cache: ^0.3.0
|
ttl_cache: ^0.3.0
|
||||||
@@ -21,4 +21,4 @@ dev_dependencies:
|
|||||||
build_runner: ^2.15.0
|
build_runner: ^2.15.0
|
||||||
json_serializable: ^6.13.2
|
json_serializable: ^6.13.2
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ dependencies:
|
|||||||
resp_client: ^1.2.0
|
resp_client: ^1.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
|
|||||||
@@ -1,7 +1,30 @@
|
|||||||
|
<!-- cspell:words Angelov mrgnhnt -->
|
||||||
|
|
||||||
# Scoped Deps
|
# Scoped Deps
|
||||||
|
|
||||||
A simple dependency injection library built on Zones.
|
A simple dependency injection library built on Zones.
|
||||||
|
|
||||||
|
## Status: vendored, no longer published by Shorebird
|
||||||
|
|
||||||
|
Felix Angelov wrote this package in 2023 (originally `package:scoped`) so that
|
||||||
|
our packages could share one dependency-injection mechanism. We published it to
|
||||||
|
pub.dev in 2024 in case it was useful to others, then never needed to change it
|
||||||
|
again.
|
||||||
|
|
||||||
|
In 2026, Morgan Hunt ([`mrgnhnt96/scoped_deps`][mrgnhnt_repo]) approached us
|
||||||
|
about taking over the pub.dev package and continuing to improve it, and we
|
||||||
|
accepted. **[`scoped_deps` on pub.dev][pub_package] is now his**, and its
|
||||||
|
versions have diverged from this directory.
|
||||||
|
|
||||||
|
Shorebird packages keep using this vendored copy, resolved through the Dart
|
||||||
|
workspace rather than from pub.dev. The code hasn't meaningfully changed in two
|
||||||
|
years, so there's nothing to gain from moving right now. At some point we should
|
||||||
|
look at where the pub.dev package has evolved to and consider deleting this copy
|
||||||
|
in favor of it.
|
||||||
|
|
||||||
|
[mrgnhnt_repo]: https://github.com/mrgnhnt96/scoped_deps
|
||||||
|
[pub_package]: https://pub.dev/packages/scoped_deps
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
|
# cspell:words mrgnhnt
|
||||||
name: scoped_deps
|
name: scoped_deps
|
||||||
description: A simple Dart library for managing scoped dependencies built on top of Zones from dart:async.
|
description: A simple Dart library for managing scoped dependencies built on top of Zones from dart:async.
|
||||||
version: 0.1.0+2
|
version: 0.1.0+2
|
||||||
homepage: https://git.tonycloud.org/flutter/shorebird-workspace
|
homepage: https://git.tonycloud.org/flutter/shorebird-workspace
|
||||||
repository: https://git.tonycloud.org/flutter/shorebird-workspace
|
repository: https://git.tonycloud.org/flutter/shorebird-workspace
|
||||||
resolution: workspace
|
resolution: workspace
|
||||||
|
# pub.dev/packages/scoped_deps is now owned by mrgnhnt96; see README.md.
|
||||||
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.9.0 <4.0.0"
|
sdk: ">=3.9.0 <4.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
meta: ^1.0.0
|
meta: ^1.18.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
mocktail: ^1.0.5
|
mocktail: ^1.0.5
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ environment:
|
|||||||
sdk: ^3.9.0
|
sdk: ^3.9.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
very_good_analysis: ^10.0.0
|
very_good_analysis: ^10.3.0
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ environment:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
args: ^2.6.0
|
args: ^2.6.0
|
||||||
meta: ^1.16.0
|
meta: ^1.18.3
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
pub_semver: ^2.1.0
|
pub_semver: ^2.1.0
|
||||||
yaml: ^3.1.0
|
yaml: ^3.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
very_good_analysis: ^10.2.0
|
very_good_analysis: ^10.3.0
|
||||||
|
|||||||
@@ -125,13 +125,20 @@ ${link(uri: Uri.parse(openShorebirdIssueUrl))}
|
|||||||
/// Probes the command's help output and caches the result per [command]
|
/// Probes the command's help output and caches the result per [command]
|
||||||
/// so that subsequent calls for the same command are free.
|
/// so that subsequent calls for the same command are free.
|
||||||
/// Returns `false` if the help check fails for any reason.
|
/// Returns `false` if the help check fails for any reason.
|
||||||
|
///
|
||||||
|
/// The probe uses verbose help (`-h -v`): `--shorebird-trace` is registered
|
||||||
|
/// with `hide: !verboseHelp` in Flutter, so it does not appear in plain
|
||||||
|
/// `-h` output. Probing with `-h` alone therefore returned `false` even on
|
||||||
|
/// Flutter versions that fully support the flag, silently disabling build
|
||||||
|
/// tracing for everyone. `-h -v` lists hidden options so support is
|
||||||
|
/// detected correctly.
|
||||||
Future<bool> _supportsTraceFlag(String command) async {
|
Future<bool> _supportsTraceFlag(String command) async {
|
||||||
if (_traceSupport.containsKey(command)) return _traceSupport[command]!;
|
if (_traceSupport.containsKey(command)) return _traceSupport[command]!;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = await process.run(
|
final result = await process.run(
|
||||||
'flutter',
|
'flutter',
|
||||||
['build', command, '-h'],
|
['build', command, '-h', '-v'],
|
||||||
runInShell: false,
|
runInShell: false,
|
||||||
);
|
);
|
||||||
final supported = result.stdout.toString().contains('--shorebird-trace');
|
final supported = result.stdout.toString().contains('--shorebird-trace');
|
||||||
|
|||||||
@@ -129,6 +129,22 @@ If left checked, Xcode will rewrite the build number in the uploaded IPA, so the
|
|||||||
throw ProcessExit(ExitCode.software.code);
|
throw ProcessExit(ExitCode.software.code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When code signing is requested (the default), `flutter build ipa` is
|
||||||
|
// expected to export a signed .ipa. Flutter treats the export step as
|
||||||
|
// optional and exits 0 even when it fails (e.g. no signing certificate),
|
||||||
|
// so we must verify the .ipa was actually produced. Otherwise we would
|
||||||
|
// report a successful release and point the user at an .ipa that does not
|
||||||
|
// exist. See https://github.com/shorebirdtech/shorebird/issues/3807.
|
||||||
|
if (codesign && artifactManager.getIpa() == null) {
|
||||||
|
logger.err(
|
||||||
|
'''
|
||||||
|
Unable to find generated IPA. This usually means that the IPA export step of "flutter build ipa" failed (for example, due to a missing or invalid code signing certificate). Review the build output above for the underlying error.
|
||||||
|
|
||||||
|
If you do not need a signed IPA (for example, you will sign the .xcarchive in Xcode), re-run this command with --no-codesign.''',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.software.code);
|
||||||
|
}
|
||||||
|
|
||||||
return xcarchiveDirectory;
|
return xcarchiveDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// Generated code. Do not modify.
|
// Generated code. Do not modify.
|
||||||
const packageVersion = '1.6.108';
|
const packageVersion = '1.6.115';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: shorebird_cli
|
name: shorebird_cli
|
||||||
description: Command-line tool to interact with Shorebird's services.
|
description: Command-line tool to interact with Shorebird's services.
|
||||||
version: 1.6.108
|
version: 1.6.115
|
||||||
repository: https://git.tonycloud.org/flutter/shorebird-workspace
|
repository: https://git.tonycloud.org/flutter/shorebird-workspace
|
||||||
resolution: workspace
|
resolution: workspace
|
||||||
|
|
||||||
@@ -20,24 +20,24 @@ dependencies:
|
|||||||
crypto: ^3.0.6
|
crypto: ^3.0.6
|
||||||
dex:
|
dex:
|
||||||
path: ../dex
|
path: ../dex
|
||||||
equatable: ^2.0.8
|
equatable: ^2.1.0
|
||||||
googleapis_auth: ^2.3.1
|
googleapis_auth: ^2.3.2
|
||||||
http: ^1.5.0
|
http: ^1.5.0
|
||||||
intl: ^0.20.2
|
intl: ^0.20.3
|
||||||
io: ^1.0.5
|
io: ^1.0.5
|
||||||
json_annotation: ^4.12.0
|
json_annotation: ^4.12.0
|
||||||
json_path: ^0.9.0
|
json_path: ^0.9.0
|
||||||
jwt:
|
jwt:
|
||||||
path: ../jwt
|
path: ../jwt
|
||||||
mason_logger: ^0.3.5
|
mason_logger: ^0.3.5
|
||||||
meta: ^1.16.0
|
meta: ^1.18.3
|
||||||
open_aot_patch_tools:
|
open_aot_patch_tools:
|
||||||
path: ../open_aot_patch_tools
|
path: ../open_aot_patch_tools
|
||||||
path: ^1.9.1
|
path: ^1.9.1
|
||||||
pem: ^2.0.5
|
pem: ^2.0.5
|
||||||
platform: ^3.1.6
|
platform: ^3.1.6
|
||||||
pointycastle: ^4.0.0
|
pointycastle: ^4.0.0
|
||||||
propertylistserialization: ^1.4.1
|
propertylistserialization: ^1.5.0
|
||||||
pub_semver: ^2.2.0
|
pub_semver: ^2.2.0
|
||||||
pubspec_parse: ^1.5.0
|
pubspec_parse: ^1.5.0
|
||||||
retry: ^3.1.2
|
retry: ^3.1.2
|
||||||
@@ -50,7 +50,7 @@ dependencies:
|
|||||||
path: ../shorebird_code_push_protocol
|
path: ../shorebird_code_push_protocol
|
||||||
stream_transform: ^2.1.1
|
stream_transform: ^2.1.1
|
||||||
uuid: ^4.5.3
|
uuid: ^4.5.3
|
||||||
xml: ^6.5.0
|
xml: ^7.0.1
|
||||||
yaml: ^3.1.3
|
yaml: ^3.1.3
|
||||||
yaml_edit: ^2.2.4
|
yaml_edit: ^2.2.4
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ dev_dependencies:
|
|||||||
build_version: ^2.1.1
|
build_version: ^2.1.1
|
||||||
json_serializable: ^6.13.2
|
json_serializable: ^6.13.2
|
||||||
mocktail: ^1.0.5
|
mocktail: ^1.0.5
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
|
|
||||||
executables:
|
executables:
|
||||||
shorebird:
|
shorebird:
|
||||||
|
|||||||
@@ -347,12 +347,78 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
|
|||||||
verify(
|
verify(
|
||||||
() => shorebirdProcess.run(
|
() => shorebirdProcess.run(
|
||||||
'flutter',
|
'flutter',
|
||||||
['build', 'appbundle', '-h'],
|
['build', 'appbundle', '-h', '-v'],
|
||||||
runInShell: false,
|
runInShell: false,
|
||||||
),
|
),
|
||||||
).called(1);
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'detects --shorebird-trace even though it is hidden from -h',
|
||||||
|
() async {
|
||||||
|
when(
|
||||||
|
() => shorebirdFlutter.resolveFlutterVersion(any()),
|
||||||
|
).thenAnswer((_) async => Version(3, 41, 7));
|
||||||
|
// Flutter registers --shorebird-trace with `hide: !verboseHelp`, so it
|
||||||
|
// only appears in verbose help. Model that: plain `-h` omits the flag,
|
||||||
|
// `-h -v` includes it. Probing without `-v` (the original bug) would
|
||||||
|
// miss it and silently disable tracing on every supported build.
|
||||||
|
when(
|
||||||
|
() => shorebirdProcess.run(
|
||||||
|
'flutter',
|
||||||
|
['build', 'appbundle', '-h'],
|
||||||
|
runInShell: false,
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => ShorebirdProcessResult(
|
||||||
|
exitCode: ExitCode.success.code,
|
||||||
|
stdout: '--release',
|
||||||
|
stderr: '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => shorebirdProcess.run(
|
||||||
|
'flutter',
|
||||||
|
['build', 'appbundle', '-h', '-v'],
|
||||||
|
runInShell: false,
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => ShorebirdProcessResult(
|
||||||
|
exitCode: ExitCode.success.code,
|
||||||
|
stdout: '--release\n--shorebird-trace',
|
||||||
|
stderr: '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await runWithOverrides(() async {
|
||||||
|
await builder.prepareBuildTrace(platform: 'android');
|
||||||
|
await builder.buildAppBundle();
|
||||||
|
});
|
||||||
|
|
||||||
|
final expectedTracePath = p.join(
|
||||||
|
projectRoot.path,
|
||||||
|
'build',
|
||||||
|
'shorebird',
|
||||||
|
'debug',
|
||||||
|
'build-trace-android.json',
|
||||||
|
);
|
||||||
|
verify(
|
||||||
|
() => shorebirdProcess.stream(
|
||||||
|
'flutter',
|
||||||
|
[
|
||||||
|
'build',
|
||||||
|
'appbundle',
|
||||||
|
'--release',
|
||||||
|
'--shorebird-trace=$expectedTracePath',
|
||||||
|
],
|
||||||
|
environment: any(named: 'environment'),
|
||||||
|
runInShell: false,
|
||||||
|
onStart: any(named: 'onStart'),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('skips trace when help probe throws', () async {
|
test('skips trace when help probe throws', () async {
|
||||||
when(
|
when(
|
||||||
() => shorebirdFlutter.resolveFlutterVersion(any()),
|
() => shorebirdFlutter.resolveFlutterVersion(any()),
|
||||||
@@ -360,7 +426,7 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
|
|||||||
when(
|
when(
|
||||||
() => shorebirdProcess.run(
|
() => shorebirdProcess.run(
|
||||||
'flutter',
|
'flutter',
|
||||||
['build', 'appbundle', '-h'],
|
['build', 'appbundle', '-h', '-v'],
|
||||||
runInShell: false,
|
runInShell: false,
|
||||||
),
|
),
|
||||||
).thenThrow(Exception('process failed'));
|
).thenThrow(Exception('process failed'));
|
||||||
@@ -921,7 +987,7 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
|
|||||||
when(
|
when(
|
||||||
() => shorebirdProcess.run(
|
() => shorebirdProcess.run(
|
||||||
'flutter',
|
'flutter',
|
||||||
['build', 'aar', '-h'],
|
['build', 'aar', '-h', '-v'],
|
||||||
runInShell: false,
|
runInShell: false,
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
@@ -1123,7 +1189,7 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
|
|||||||
when(
|
when(
|
||||||
() => shorebirdProcess.run(
|
() => shorebirdProcess.run(
|
||||||
'flutter',
|
'flutter',
|
||||||
['build', 'linux', '-h'],
|
['build', 'linux', '-h', '-v'],
|
||||||
runInShell: false,
|
runInShell: false,
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
@@ -1300,7 +1366,7 @@ Reason: Exited with code 70.'''),
|
|||||||
when(
|
when(
|
||||||
() => shorebirdProcess.run(
|
() => shorebirdProcess.run(
|
||||||
'flutter',
|
'flutter',
|
||||||
['build', 'macos', '-h'],
|
['build', 'macos', '-h', '-v'],
|
||||||
runInShell: false,
|
runInShell: false,
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
@@ -1789,7 +1855,7 @@ Reason: Exited with code 70.'''),
|
|||||||
when(
|
when(
|
||||||
() => shorebirdProcess.run(
|
() => shorebirdProcess.run(
|
||||||
'flutter',
|
'flutter',
|
||||||
['build', 'ios-framework', '-h'],
|
['build', 'ios-framework', '-h', '-v'],
|
||||||
runInShell: false,
|
runInShell: false,
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
@@ -2181,7 +2247,7 @@ Reason: Exited with code 70.'''),
|
|||||||
when(
|
when(
|
||||||
() => shorebirdProcess.run(
|
() => shorebirdProcess.run(
|
||||||
'flutter',
|
'flutter',
|
||||||
['build', 'windows', '-h'],
|
['build', 'windows', '-h', '-v'],
|
||||||
runInShell: false,
|
runInShell: false,
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
|
|||||||
@@ -423,6 +423,9 @@ $body
|
|||||||
when(
|
when(
|
||||||
() => artifactManager.getXcarchiveDirectory(),
|
() => artifactManager.getXcarchiveDirectory(),
|
||||||
).thenReturn(xcarchiveDirectory);
|
).thenReturn(xcarchiveDirectory);
|
||||||
|
when(
|
||||||
|
() => artifactManager.getIpa(),
|
||||||
|
).thenReturn(File(p.join(Directory.systemTemp.path, 'app.ipa')));
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => codeSigner.base64PublicKeyFromPem(any()),
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
@@ -660,6 +663,42 @@ $body
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('when codesigning and ipa not found after build', () {
|
||||||
|
setUp(() {
|
||||||
|
when(() => argResults['codesign']).thenReturn(true);
|
||||||
|
when(() => artifactManager.getIpa()).thenReturn(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logs message and exits with code 70', () async {
|
||||||
|
await expectLater(
|
||||||
|
() => runWithOverrides(iosReleaser.buildReleaseArtifacts),
|
||||||
|
exitsWithCode(ExitCode.software),
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => logger.err(
|
||||||
|
any(that: contains('Unable to find generated IPA')),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('when not codesigning and ipa not found after build', () {
|
||||||
|
setUp(() {
|
||||||
|
when(() => argResults['codesign']).thenReturn(false);
|
||||||
|
when(() => artifactManager.getIpa()).thenReturn(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not check for the ipa and returns xcarchive path', () async {
|
||||||
|
expect(
|
||||||
|
await runWithOverrides(iosReleaser.buildReleaseArtifacts),
|
||||||
|
equals(xcarchiveDirectory),
|
||||||
|
);
|
||||||
|
|
||||||
|
verifyNever(() => artifactManager.getIpa());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('when --obfuscate is passed', () {
|
group('when --obfuscate is passed', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
when(() => argResults['obfuscate']).thenReturn(true);
|
when(() => argResults['obfuscate']).thenReturn(true);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||||
import 'package:shorebird_code_push_client/src/version.dart';
|
import 'package:shorebird_code_push_client/src/version.dart';
|
||||||
@@ -91,23 +92,39 @@ class CodePushClient {
|
|||||||
http.Client? httpClient,
|
http.Client? httpClient,
|
||||||
Uri? hostedUri,
|
Uri? hostedUri,
|
||||||
Map<String, String>? customHeaders,
|
Map<String, String>? customHeaders,
|
||||||
|
@visibleForTesting
|
||||||
|
Duration uploadRetryBaseDelay = const Duration(seconds: 1),
|
||||||
}) : _httpClient = _CodePushHttpClient(httpClient ?? http.Client(), {
|
}) : _httpClient = _CodePushHttpClient(httpClient ?? http.Client(), {
|
||||||
...standardHeaders,
|
...standardHeaders,
|
||||||
...?customHeaders,
|
...?customHeaders,
|
||||||
}),
|
}),
|
||||||
|
_uploadRetryBaseDelay = uploadRetryBaseDelay,
|
||||||
hostedUri = hostedUri ?? defaultHostedUri;
|
hostedUri = hostedUri ?? defaultHostedUri;
|
||||||
|
|
||||||
/// Default URL for the open self-hosted Shorebird server.
|
/// Default URL for the open self-hosted Shorebird server.
|
||||||
static final Uri defaultHostedUri = Uri.parse('http://localhost:8080');
|
static final Uri defaultHostedUri = Uri.parse('http://localhost:8080');
|
||||||
|
|
||||||
/// The standard headers applied to all requests.
|
/// The standard headers applied to all requests.
|
||||||
|
@visibleForTesting
|
||||||
static const standardHeaders = <String, String>{'x-version': packageVersion};
|
static const standardHeaders = <String, String>{'x-version': packageVersion};
|
||||||
|
|
||||||
/// The default error message to use when an unknown error occurs.
|
/// The default error message to use when an unknown error occurs.
|
||||||
|
@visibleForTesting
|
||||||
static const unknownErrorMessage = 'An unknown error occurred.';
|
static const unknownErrorMessage = 'An unknown error occurred.';
|
||||||
|
|
||||||
|
/// The status GCS returns ("Resume Incomplete") between resumable chunks.
|
||||||
|
static const _resumeIncompleteStatus = 308;
|
||||||
|
|
||||||
|
/// The maximum number of consecutive failures tolerated while uploading a
|
||||||
|
/// single resumable session before the upload is abandoned.
|
||||||
|
static const _maxUploadFailures = 5;
|
||||||
|
|
||||||
final http.Client _httpClient;
|
final http.Client _httpClient;
|
||||||
|
|
||||||
|
/// The base delay for exponential backoff between resumable upload retries.
|
||||||
|
/// Doubles with each consecutive failure.
|
||||||
|
final Duration _uploadRetryBaseDelay;
|
||||||
|
|
||||||
/// The hosted uri for the Shorebird CodePush API.
|
/// The hosted uri for the Shorebird CodePush API.
|
||||||
final Uri hostedUri;
|
final Uri hostedUri;
|
||||||
|
|
||||||
@@ -163,17 +180,12 @@ class CodePushClient {
|
|||||||
json.decode(body) as Map<String, dynamic>,
|
json.decode(body) as Map<String, dynamic>,
|
||||||
);
|
);
|
||||||
|
|
||||||
final uploadRequest = http.MultipartRequest('POST', Uri.parse(decoded.url))
|
await _uploadArtifact(
|
||||||
..files.add(file);
|
artifactPath: artifactPath,
|
||||||
|
multipartFile: file,
|
||||||
final uploadResponse = await _httpClient.send(uploadRequest);
|
url: decoded.url,
|
||||||
|
uploadMethod: decoded.uploadMethod,
|
||||||
if (!uploadResponse.isSuccess) {
|
);
|
||||||
throw CodePushException(
|
|
||||||
message:
|
|
||||||
'''Failed to upload artifact (${uploadResponse.reasonPhrase} '${uploadResponse.statusCode})''',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new artifact for a specific [releaseId].
|
/// Create a new artifact for a specific [releaseId].
|
||||||
@@ -222,17 +234,152 @@ class CodePushClient {
|
|||||||
json.decode(body) as Map<String, dynamic>,
|
json.decode(body) as Map<String, dynamic>,
|
||||||
);
|
);
|
||||||
|
|
||||||
final uploadRequest = http.MultipartRequest('POST', Uri.parse(decoded.url))
|
await _uploadArtifact(
|
||||||
..files.add(file);
|
artifactPath: artifactPath,
|
||||||
|
multipartFile: file,
|
||||||
|
url: decoded.url,
|
||||||
|
uploadMethod: decoded.uploadMethod,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final uploadResponse = await _httpClient.send(uploadRequest);
|
/// Uploads an artifact's bytes to storage using the method the server
|
||||||
|
/// selected in the create response.
|
||||||
if (!uploadResponse.isSuccess) {
|
Future<void> _uploadArtifact({
|
||||||
throw CodePushException(
|
required String artifactPath,
|
||||||
message:
|
required http.MultipartFile multipartFile,
|
||||||
'''Failed to upload artifact (${uploadResponse.reasonPhrase} '${uploadResponse.statusCode})''',
|
required String url,
|
||||||
|
required ArtifactUploadMethod? uploadMethod,
|
||||||
|
}) async {
|
||||||
|
if (uploadMethod == ArtifactUploadMethod.resumable) {
|
||||||
|
await _resumableUpload(
|
||||||
|
sessionUri: Uri.parse(url),
|
||||||
|
artifactPath: artifactPath,
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy single multipart POST. Remove once the server no longer returns
|
||||||
|
// ArtifactUploadMethod.multipart (i.e. all supported clients are new
|
||||||
|
// enough to receive a resumable session).
|
||||||
|
final uploadRequest = http.MultipartRequest('POST', Uri.parse(url))
|
||||||
|
..files.add(multipartFile);
|
||||||
|
final uploadResponse = await _httpClient.send(uploadRequest);
|
||||||
|
if (!uploadResponse.isSuccess) {
|
||||||
|
throw _uploadFailed(uploadResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uploads [artifactPath] to a GCS resumable session at [sessionUri] by
|
||||||
|
/// PUTing the bytes in fixed-size chunks with a `Content-Range` header,
|
||||||
|
/// resuming from the last byte GCS acknowledged if a chunk fails. The
|
||||||
|
/// session was initiated (and size-bound) server-side.
|
||||||
|
Future<void> _resumableUpload({
|
||||||
|
required Uri sessionUri,
|
||||||
|
required String artifactPath,
|
||||||
|
}) async {
|
||||||
|
// GCS requires chunk sizes to be a multiple of 256 KiB (except the last).
|
||||||
|
const chunkSize = 8 * 1024 * 1024;
|
||||||
|
|
||||||
|
final file = File(artifactPath);
|
||||||
|
final total = await file.length();
|
||||||
|
final raf = await file.open();
|
||||||
|
var failures = 0;
|
||||||
|
try {
|
||||||
|
var offset = 0;
|
||||||
|
while (offset < total) {
|
||||||
|
final end = offset + chunkSize < total ? offset + chunkSize : total;
|
||||||
|
await raf.setPosition(offset);
|
||||||
|
final chunk = await raf.read(end - offset);
|
||||||
|
|
||||||
|
final http.StreamedResponse response;
|
||||||
|
try {
|
||||||
|
response = await _httpClient.send(
|
||||||
|
http.Request('PUT', sessionUri)
|
||||||
|
..bodyBytes = chunk
|
||||||
|
..headers['content-range'] = 'bytes $offset-${end - 1}/$total',
|
||||||
|
);
|
||||||
|
} on Exception {
|
||||||
|
// Network failure mid-chunk: back off, ask GCS how far it got, and
|
||||||
|
// resume from there.
|
||||||
|
if (++failures > _maxUploadFailures) rethrow;
|
||||||
|
await _backoff(failures);
|
||||||
|
offset = await _queryResumeOffset(sessionUri, total);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final status = response.statusCode;
|
||||||
|
await response.stream.drain<void>();
|
||||||
|
|
||||||
|
if (status == HttpStatus.ok || status == HttpStatus.created) return;
|
||||||
|
|
||||||
|
if (status == _resumeIncompleteStatus) {
|
||||||
|
// A 308 reports GCS's stored byte count in the `range` header. Its
|
||||||
|
// absence means GCS has no bytes yet, so we must restart from 0
|
||||||
|
// (per the resumable upload status-check docs). Treat a lack of
|
||||||
|
// forward progress as a failure so a stuck session can't spin
|
||||||
|
// forever.
|
||||||
|
final next = _parseRangeEnd(response.headers['range']) ?? 0;
|
||||||
|
if (next > offset) {
|
||||||
|
failures = 0;
|
||||||
|
} else if (++failures > _maxUploadFailures) {
|
||||||
|
throw _uploadFailed(response);
|
||||||
|
} else {
|
||||||
|
await _backoff(failures);
|
||||||
|
}
|
||||||
|
offset = next;
|
||||||
|
} else if (status >= HttpStatus.internalServerError) {
|
||||||
|
// Transient server error (5xx): recover the same way as a mid-chunk
|
||||||
|
// network failure — back off, then resume from where GCS left off
|
||||||
|
// rather than re-sending bytes it has already persisted.
|
||||||
|
if (++failures > _maxUploadFailures) throw _uploadFailed(response);
|
||||||
|
await _backoff(failures);
|
||||||
|
offset = await _queryResumeOffset(sessionUri, total);
|
||||||
|
} else {
|
||||||
|
throw _uploadFailed(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await raf.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits with exponential backoff before retrying a resumable upload,
|
||||||
|
/// doubling [_uploadRetryBaseDelay] with each consecutive [failures].
|
||||||
|
Future<void> _backoff(int failures) =>
|
||||||
|
Future<void>.delayed(_uploadRetryBaseDelay * (1 << (failures - 1)));
|
||||||
|
|
||||||
|
/// Queries a resumable [sessionUri] for the number of bytes GCS has received
|
||||||
|
/// so far, returning the offset to resume from.
|
||||||
|
Future<int> _queryResumeOffset(Uri sessionUri, int total) async {
|
||||||
|
final response = await _httpClient.send(
|
||||||
|
http.Request('PUT', sessionUri)
|
||||||
|
..headers['content-range'] = 'bytes */$total',
|
||||||
|
);
|
||||||
|
final status = response.statusCode;
|
||||||
|
await response.stream.drain<void>();
|
||||||
|
if (status == HttpStatus.ok || status == HttpStatus.created) return total;
|
||||||
|
if (status == _resumeIncompleteStatus) {
|
||||||
|
return _parseRangeEnd(response.headers['range']) ?? 0;
|
||||||
|
}
|
||||||
|
throw _uploadFailed(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the next byte offset from a GCS `Range: bytes=0-X` header,
|
||||||
|
/// returning `X + 1`, or null if the header is absent/malformed.
|
||||||
|
int? _parseRangeEnd(String? range) {
|
||||||
|
if (range == null) return null;
|
||||||
|
final dash = range.lastIndexOf('-');
|
||||||
|
if (dash == -1) return null;
|
||||||
|
final last = int.tryParse(range.substring(dash + 1));
|
||||||
|
return last == null ? null : last + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The exception thrown when an artifact upload request fails.
|
||||||
|
CodePushException _uploadFailed(http.BaseResponse response) {
|
||||||
|
final reason = response.reasonPhrase;
|
||||||
|
return CodePushException(
|
||||||
|
message: 'Failed to upload artifact ($reason ${response.statusCode})',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new app with the provided [displayName].
|
/// Create a new app with the provided [displayName].
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
http: ^1.5.0
|
http: ^1.5.0
|
||||||
|
meta: ^1.18.3
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
shorebird_code_push_protocol:
|
shorebird_code_push_protocol:
|
||||||
path: ../shorebird_code_push_protocol
|
path: ../shorebird_code_push_protocol
|
||||||
@@ -21,4 +22,4 @@ dev_dependencies:
|
|||||||
build_verify: ^3.1.1
|
build_verify: ^3.1.1
|
||||||
build_version: ^2.0.0
|
build_version: ^2.0.0
|
||||||
mocktail: ^1.0.5
|
mocktail: ^1.0.5
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ void main() {
|
|||||||
codePushClient = CodePushClient(
|
codePushClient = CodePushClient(
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
customHeaders: customHeaders,
|
customHeaders: customHeaders,
|
||||||
|
// Disable backoff delays so retry paths run instantly under test.
|
||||||
|
uploadRetryBaseDelay: Duration.zero,
|
||||||
);
|
);
|
||||||
when(() => httpClient.send(any())).thenAnswer(
|
when(() => httpClient.send(any())).thenAnswer(
|
||||||
(_) async => http.StreamedResponse(const Stream.empty(), HttpStatus.ok),
|
(_) async => http.StreamedResponse(const Stream.empty(), HttpStatus.ok),
|
||||||
@@ -460,6 +462,62 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('uploads via a resumable session when the server '
|
||||||
|
'selects resumable', () async {
|
||||||
|
const artifactId = 42;
|
||||||
|
const sessionUrl = 'https://storage.googleapis.com/session?upload_id=a';
|
||||||
|
final responses = [
|
||||||
|
http.StreamedResponse(
|
||||||
|
Stream.value(
|
||||||
|
utf8.encode(
|
||||||
|
json.encode(
|
||||||
|
const CreatePatchArtifactResponse(
|
||||||
|
id: artifactId,
|
||||||
|
patchId: patchId,
|
||||||
|
arch: arch,
|
||||||
|
platform: platform,
|
||||||
|
hash: hash,
|
||||||
|
size: size,
|
||||||
|
url: sessionUrl,
|
||||||
|
uploadMethod: ArtifactUploadMethod.resumable,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
HttpStatus.ok,
|
||||||
|
),
|
||||||
|
http.StreamedResponse(const Stream.empty(), HttpStatus.ok),
|
||||||
|
];
|
||||||
|
when(
|
||||||
|
() => httpClient.send(any()),
|
||||||
|
).thenAnswer((_) async => responses.removeAt(0));
|
||||||
|
|
||||||
|
final tempDir = Directory.systemTemp.createTempSync();
|
||||||
|
final fixture = File(path.join(tempDir.path, 'patch.txt'))
|
||||||
|
..writeAsBytesSync([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
codePushClient.createPatchArtifact(
|
||||||
|
appId: appId,
|
||||||
|
artifactPath: fixture.path,
|
||||||
|
patchId: patchId,
|
||||||
|
arch: arch,
|
||||||
|
platform: platform,
|
||||||
|
hash: hash,
|
||||||
|
),
|
||||||
|
completes,
|
||||||
|
);
|
||||||
|
|
||||||
|
final requests = verify(
|
||||||
|
() => httpClient.send(captureAny()),
|
||||||
|
).captured.cast<http.BaseRequest>();
|
||||||
|
expect(requests, hasLength(2));
|
||||||
|
final uploadRequest = requests.last;
|
||||||
|
expect(uploadRequest.method, equals('PUT'));
|
||||||
|
expect(uploadRequest.url, equals(Uri.parse(sessionUrl)));
|
||||||
|
expect(uploadRequest.headers['content-range'], equals('bytes 0-4/5'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('createReleaseArtifact', () {
|
group('createReleaseArtifact', () {
|
||||||
@@ -807,6 +865,314 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('uploads via a resumable session when the server '
|
||||||
|
'selects resumable', () async {
|
||||||
|
const artifactId = 42;
|
||||||
|
const sessionUrl = 'https://storage.googleapis.com/session?upload_id=a';
|
||||||
|
final responses = [
|
||||||
|
http.StreamedResponse(
|
||||||
|
Stream.value(
|
||||||
|
utf8.encode(
|
||||||
|
json.encode(
|
||||||
|
const CreateReleaseArtifactResponse(
|
||||||
|
id: artifactId,
|
||||||
|
releaseId: releaseId,
|
||||||
|
arch: arch,
|
||||||
|
platform: platform,
|
||||||
|
hash: hash,
|
||||||
|
size: size,
|
||||||
|
url: sessionUrl,
|
||||||
|
uploadMethod: ArtifactUploadMethod.resumable,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
HttpStatus.ok,
|
||||||
|
),
|
||||||
|
http.StreamedResponse(const Stream.empty(), HttpStatus.ok),
|
||||||
|
];
|
||||||
|
when(
|
||||||
|
() => httpClient.send(any()),
|
||||||
|
).thenAnswer((_) async => responses.removeAt(0));
|
||||||
|
|
||||||
|
final tempDir = Directory.systemTemp.createTempSync();
|
||||||
|
final fixture = File(path.join(tempDir.path, 'release.txt'))
|
||||||
|
..writeAsBytesSync([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
codePushClient.createReleaseArtifact(
|
||||||
|
appId: appId,
|
||||||
|
artifactPath: fixture.path,
|
||||||
|
releaseId: releaseId,
|
||||||
|
arch: arch,
|
||||||
|
platform: platform,
|
||||||
|
hash: hash,
|
||||||
|
canSideload: canSideload,
|
||||||
|
podfileLockHash: podfileLockHash,
|
||||||
|
),
|
||||||
|
completes,
|
||||||
|
);
|
||||||
|
|
||||||
|
final requests = verify(
|
||||||
|
() => httpClient.send(captureAny()),
|
||||||
|
).captured.cast<http.BaseRequest>();
|
||||||
|
expect(requests, hasLength(2));
|
||||||
|
final uploadRequest = requests.last;
|
||||||
|
expect(uploadRequest.method, equals('PUT'));
|
||||||
|
expect(uploadRequest.url, equals(Uri.parse(sessionUrl)));
|
||||||
|
expect(uploadRequest.headers['content-range'], equals('bytes 0-4/5'));
|
||||||
|
});
|
||||||
|
|
||||||
|
group('resumable upload driver', () {
|
||||||
|
const sessionUrl = 'https://storage.googleapis.com/session?id=abc';
|
||||||
|
|
||||||
|
http.StreamedResponse resumableMeta() => http.StreamedResponse(
|
||||||
|
Stream.value(
|
||||||
|
utf8.encode(
|
||||||
|
json.encode(
|
||||||
|
const CreateReleaseArtifactResponse(
|
||||||
|
id: 42,
|
||||||
|
releaseId: releaseId,
|
||||||
|
arch: arch,
|
||||||
|
platform: platform,
|
||||||
|
hash: hash,
|
||||||
|
size: size,
|
||||||
|
url: sessionUrl,
|
||||||
|
uploadMethod: ArtifactUploadMethod.resumable,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
HttpStatus.ok,
|
||||||
|
);
|
||||||
|
|
||||||
|
String fiveByteFixture() {
|
||||||
|
final dir = Directory.systemTemp.createTempSync();
|
||||||
|
return (File(
|
||||||
|
path.join(dir.path, 'release.txt'),
|
||||||
|
)..writeAsBytesSync([1, 2, 3, 4, 5])).path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stubActions(
|
||||||
|
List<Future<http.StreamedResponse> Function()> actions,
|
||||||
|
) {
|
||||||
|
when(
|
||||||
|
() => httpClient.send(any()),
|
||||||
|
).thenAnswer((_) => actions.removeAt(0)());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> upload(String artifactPath) =>
|
||||||
|
codePushClient.createReleaseArtifact(
|
||||||
|
appId: appId,
|
||||||
|
artifactPath: artifactPath,
|
||||||
|
releaseId: releaseId,
|
||||||
|
arch: arch,
|
||||||
|
platform: platform,
|
||||||
|
hash: hash,
|
||||||
|
canSideload: canSideload,
|
||||||
|
podfileLockHash: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
List<http.BaseRequest> capturedPuts() =>
|
||||||
|
verify(
|
||||||
|
() => httpClient.send(captureAny()),
|
||||||
|
).captured
|
||||||
|
.cast<http.BaseRequest>()
|
||||||
|
.where((r) => r.method == 'PUT')
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
test('continues to the next chunk on a 308 response', () async {
|
||||||
|
stubActions([
|
||||||
|
() async => resumableMeta(),
|
||||||
|
() async => http.StreamedResponse(
|
||||||
|
const Stream.empty(),
|
||||||
|
308,
|
||||||
|
headers: const {'range': 'bytes=0-2'},
|
||||||
|
),
|
||||||
|
() async =>
|
||||||
|
http.StreamedResponse(const Stream.empty(), HttpStatus.ok),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectLater(upload(fiveByteFixture()), completes);
|
||||||
|
|
||||||
|
final puts = capturedPuts();
|
||||||
|
expect(puts, hasLength(2));
|
||||||
|
expect(puts[0].headers['content-range'], 'bytes 0-4/5');
|
||||||
|
expect(puts[1].headers['content-range'], 'bytes 3-4/5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when a chunk returns an error status', () async {
|
||||||
|
stubActions([
|
||||||
|
() async => resumableMeta(),
|
||||||
|
() async => http.StreamedResponse(
|
||||||
|
const Stream.empty(),
|
||||||
|
HttpStatus.badRequest,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
upload(fiveByteFixture()),
|
||||||
|
throwsA(
|
||||||
|
isA<CodePushException>().having(
|
||||||
|
(e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains('Failed to upload artifact'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'queries the offset and resumes after a network failure',
|
||||||
|
() async {
|
||||||
|
stubActions([
|
||||||
|
() async => resumableMeta(),
|
||||||
|
() async => throw http.ClientException('connection reset'),
|
||||||
|
() async => http.StreamedResponse(
|
||||||
|
const Stream.empty(),
|
||||||
|
308,
|
||||||
|
headers: const {'range': 'bytes=0-1'},
|
||||||
|
),
|
||||||
|
() async =>
|
||||||
|
http.StreamedResponse(const Stream.empty(), HttpStatus.ok),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectLater(upload(fiveByteFixture()), completes);
|
||||||
|
|
||||||
|
final puts = capturedPuts();
|
||||||
|
// failed chunk, status query, resumed chunk from offset 2.
|
||||||
|
expect(puts, hasLength(3));
|
||||||
|
expect(puts[1].headers['content-range'], 'bytes */5');
|
||||||
|
expect(puts[2].headers['content-range'], 'bytes 2-4/5');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('throws when the status query returns an error', () async {
|
||||||
|
stubActions([
|
||||||
|
() async => resumableMeta(),
|
||||||
|
() async => throw http.ClientException('connection reset'),
|
||||||
|
() async => http.StreamedResponse(
|
||||||
|
const Stream.empty(),
|
||||||
|
HttpStatus.badRequest,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
upload(fiveByteFixture()),
|
||||||
|
throwsA(isA<CodePushException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restarts from the beginning on a 308 with no range', () async {
|
||||||
|
stubActions([
|
||||||
|
() async => resumableMeta(),
|
||||||
|
// 308 without a `range` header: GCS stored nothing, restart at 0.
|
||||||
|
() async => http.StreamedResponse(const Stream.empty(), 308),
|
||||||
|
() async =>
|
||||||
|
http.StreamedResponse(const Stream.empty(), HttpStatus.ok),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectLater(upload(fiveByteFixture()), completes);
|
||||||
|
|
||||||
|
final puts = capturedPuts();
|
||||||
|
expect(puts, hasLength(2));
|
||||||
|
expect(puts[0].headers['content-range'], 'bytes 0-4/5');
|
||||||
|
// Restarts from offset 0 rather than advancing.
|
||||||
|
expect(puts[1].headers['content-range'], 'bytes 0-4/5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when a 308 never makes forward progress', () async {
|
||||||
|
// A session stuck at offset 0 keeps returning a 308 with no range;
|
||||||
|
// the upload must give up rather than spin forever.
|
||||||
|
stubActions([
|
||||||
|
() async => resumableMeta(),
|
||||||
|
for (var i = 0; i < 6; i++)
|
||||||
|
() async => http.StreamedResponse(const Stream.empty(), 308),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
upload(fiveByteFixture()),
|
||||||
|
throwsA(isA<CodePushException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('queries the offset and resumes after a 5xx', () async {
|
||||||
|
stubActions([
|
||||||
|
() async => resumableMeta(),
|
||||||
|
() async => http.StreamedResponse(
|
||||||
|
const Stream.empty(),
|
||||||
|
HttpStatus.serviceUnavailable,
|
||||||
|
),
|
||||||
|
() async => http.StreamedResponse(
|
||||||
|
const Stream.empty(),
|
||||||
|
308,
|
||||||
|
headers: const {'range': 'bytes=0-1'},
|
||||||
|
),
|
||||||
|
() async =>
|
||||||
|
http.StreamedResponse(const Stream.empty(), HttpStatus.ok),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectLater(upload(fiveByteFixture()), completes);
|
||||||
|
|
||||||
|
final puts = capturedPuts();
|
||||||
|
// failed chunk, status query, resumed chunk from offset 2.
|
||||||
|
expect(puts, hasLength(3));
|
||||||
|
expect(puts[0].headers['content-range'], 'bytes 0-4/5');
|
||||||
|
expect(puts[1].headers['content-range'], 'bytes */5');
|
||||||
|
expect(puts[2].headers['content-range'], 'bytes 2-4/5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when a 5xx persists through the status query', () async {
|
||||||
|
stubActions([
|
||||||
|
() async => resumableMeta(),
|
||||||
|
() async => http.StreamedResponse(
|
||||||
|
const Stream.empty(),
|
||||||
|
HttpStatus.serviceUnavailable,
|
||||||
|
),
|
||||||
|
// The status query also fails, so we surface the error.
|
||||||
|
() async => http.StreamedResponse(
|
||||||
|
const Stream.empty(),
|
||||||
|
HttpStatus.serviceUnavailable,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
upload(fiveByteFixture()),
|
||||||
|
throwsA(isA<CodePushException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws after exhausting retries on repeated 5xx', () async {
|
||||||
|
// Each chunk PUT fails with a 5xx while the status query reports no
|
||||||
|
// forward progress, so failures accumulate until we give up.
|
||||||
|
final actions = <Future<http.StreamedResponse> Function()>[
|
||||||
|
() async => resumableMeta(),
|
||||||
|
];
|
||||||
|
for (var i = 0; i < 6; i++) {
|
||||||
|
actions
|
||||||
|
..add(
|
||||||
|
() async => http.StreamedResponse(
|
||||||
|
const Stream.empty(),
|
||||||
|
HttpStatus.serviceUnavailable,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..add(
|
||||||
|
() async => http.StreamedResponse(
|
||||||
|
const Stream.empty(),
|
||||||
|
308,
|
||||||
|
headers: const {'range': 'bytes=0-1'},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
stubActions(actions);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
upload(fiveByteFixture()),
|
||||||
|
throwsA(isA<CodePushException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('createApp', () {
|
group('createApp', () {
|
||||||
|
|||||||
@@ -24,13 +24,16 @@ export 'package:shorebird_code_push_protocol/src/messages/create_release_artifac
|
|||||||
export 'package:shorebird_code_push_protocol/src/messages/create_user/create_user_request.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/create_user/create_user_request.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/error_response.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/error_response.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/get_active_hours/get_active_hours_response.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/get_active_hours/get_active_hours_response.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/messages/get_activity_heatmap/get_activity_heatmap_response.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/get_apps/get_apps_response.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/get_apps/get_apps_response.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/get_gcp_download_speed_test_url/get_gcp_download_speed_test_url200_response.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/get_gcp_download_speed_test_url/get_gcp_download_speed_test_url200_response.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/get_gcp_upload_speed_test_url/get_gcp_upload_speed_test_url200_response.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/get_gcp_upload_speed_test_url/get_gcp_upload_speed_test_url200_response.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/messages/get_new_devices/get_new_devices_response.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/get_organization_apps/get_organization_apps_response.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/get_organization_apps/get_organization_apps_response.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/get_organization_users/get_organization_users_response.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/get_organization_users/get_organization_users_response.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/get_organizations_response.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/get_organizations_response.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/get_patch_adoption/get_patch_adoption_response.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/get_patch_adoption/get_patch_adoption_response.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/messages/get_patch_metric_response.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/get_release/get_release_response.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/get_release/get_release_response.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/get_release_artifacts/get_release_artifacts_response.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/get_release_artifacts/get_release_artifacts_response.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/get_release_patches/get_release_patches_response.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/get_release_patches/get_release_patches_response.dart';
|
||||||
@@ -44,11 +47,21 @@ export 'package:shorebird_code_push_protocol/src/messages/update_app_collaborato
|
|||||||
export 'package:shorebird_code_push_protocol/src/messages/update_patch/update_patch_request.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/update_patch/update_patch_request.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/messages/update_release/update_release_request.dart';
|
export 'package:shorebird_code_push_protocol/src/messages/update_release/update_release_request.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/active_hour_entry.dart';
|
export 'package:shorebird_code_push_protocol/src/models/active_hour_entry.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/activity_heatmap_cell.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/app.dart';
|
export 'package:shorebird_code_push_protocol/src/models/app.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/app_collaborator_role.dart';
|
export 'package:shorebird_code_push_protocol/src/models/app_collaborator_role.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/app_metadata.dart';
|
export 'package:shorebird_code_push_protocol/src/models/app_metadata.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/artifact_upload_method.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/channel.dart';
|
export 'package:shorebird_code_push_protocol/src/models/channel.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/get_app_patch_downloads_parameter2.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/get_app_patch_downloads_parameter3.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/get_app_patch_installs_parameter2.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/get_app_patch_installs_parameter3.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/get_patch_adoption_parameter3.dart';
|
export 'package:shorebird_code_push_protocol/src/models/get_patch_adoption_parameter3.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/get_release_patch_downloads_parameter2.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/get_release_patch_downloads_parameter3.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/get_release_patch_installs_parameter2.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/get_release_patch_installs_parameter3.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/get_unique_users_parameter2.dart';
|
export 'package:shorebird_code_push_protocol/src/models/get_unique_users_parameter2.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/get_unique_users_parameter3.dart';
|
export 'package:shorebird_code_push_protocol/src/models/get_unique_users_parameter3.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/latest_release.dart';
|
export 'package:shorebird_code_push_protocol/src/models/latest_release.dart';
|
||||||
@@ -62,6 +75,10 @@ export 'package:shorebird_code_push_protocol/src/models/patch_adoption_entry.dar
|
|||||||
export 'package:shorebird_code_push_protocol/src/models/patch_adoption_point.dart';
|
export 'package:shorebird_code_push_protocol/src/models/patch_adoption_point.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/patch_artifact.dart';
|
export 'package:shorebird_code_push_protocol/src/models/patch_artifact.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/patch_check_metadata.dart';
|
export 'package:shorebird_code_push_protocol/src/models/patch_check_metadata.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/patch_metric_breakdown_entry.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/patch_metric_current_window.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/patch_metric_time_series_entry.dart';
|
||||||
|
export 'package:shorebird_code_push_protocol/src/models/patch_metric_window.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/pending_release.dart';
|
export 'package:shorebird_code_push_protocol/src/models/pending_release.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/private_user.dart';
|
export 'package:shorebird_code_push_protocol/src/models/private_user.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/public_user.dart';
|
export 'package:shorebird_code_push_protocol/src/models/public_user.dart';
|
||||||
|
|||||||
+17
-2
@@ -1,5 +1,6 @@
|
|||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/src/models/artifact_upload_method.dart';
|
||||||
import 'package:shorebird_code_push_protocol/src/models/release_platform.dart';
|
import 'package:shorebird_code_push_protocol/src/models/release_platform.dart';
|
||||||
|
|
||||||
/// {@template create_patch_artifact_response}
|
/// {@template create_patch_artifact_response}
|
||||||
@@ -16,6 +17,7 @@ class CreatePatchArtifactResponse {
|
|||||||
required this.hash,
|
required this.hash,
|
||||||
required this.size,
|
required this.size,
|
||||||
required this.url,
|
required this.url,
|
||||||
|
this.uploadMethod,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Converts a `Map<String, dynamic>` to a [CreatePatchArtifactResponse].
|
/// Converts a `Map<String, dynamic>` to a [CreatePatchArtifactResponse].
|
||||||
@@ -31,6 +33,9 @@ class CreatePatchArtifactResponse {
|
|||||||
hash: json['hash'] as String,
|
hash: json['hash'] as String,
|
||||||
size: json['size'] as int,
|
size: json['size'] as int,
|
||||||
url: json['url'] as String,
|
url: json['url'] as String,
|
||||||
|
uploadMethod: ArtifactUploadMethod.maybeFromJson(
|
||||||
|
json['upload_method'] as String?,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -64,9 +69,16 @@ class CreatePatchArtifactResponse {
|
|||||||
/// The size of the artifact in bytes.
|
/// The size of the artifact in bytes.
|
||||||
final int size;
|
final int size;
|
||||||
|
|
||||||
/// The upload URL for the artifact.
|
/// The upload URL for the artifact (a signed URL for [ArtifactUploadMethod
|
||||||
|
/// .multipart], or a resumable session URI for [ArtifactUploadMethod
|
||||||
|
/// .resumable]).
|
||||||
final String url;
|
final String url;
|
||||||
|
|
||||||
|
/// How the client should upload the artifact bytes to [url]. Null on
|
||||||
|
/// responses from older servers, which always implied
|
||||||
|
/// [ArtifactUploadMethod.multipart].
|
||||||
|
final ArtifactUploadMethod? uploadMethod;
|
||||||
|
|
||||||
/// Converts a [CreatePatchArtifactResponse] to a `Map<String, dynamic>`.
|
/// Converts a [CreatePatchArtifactResponse] to a `Map<String, dynamic>`.
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
@@ -77,6 +89,7 @@ class CreatePatchArtifactResponse {
|
|||||||
'hash': hash,
|
'hash': hash,
|
||||||
'size': size,
|
'size': size,
|
||||||
'url': url,
|
'url': url,
|
||||||
|
'upload_method': uploadMethod?.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +102,7 @@ class CreatePatchArtifactResponse {
|
|||||||
hash,
|
hash,
|
||||||
size,
|
size,
|
||||||
url,
|
url,
|
||||||
|
uploadMethod,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -101,6 +115,7 @@ class CreatePatchArtifactResponse {
|
|||||||
platform == other.platform &&
|
platform == other.platform &&
|
||||||
hash == other.hash &&
|
hash == other.hash &&
|
||||||
size == other.size &&
|
size == other.size &&
|
||||||
url == other.url;
|
url == other.url &&
|
||||||
|
uploadMethod == other.uploadMethod;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-2
@@ -1,5 +1,6 @@
|
|||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/src/models/artifact_upload_method.dart';
|
||||||
import 'package:shorebird_code_push_protocol/src/models/release_platform.dart';
|
import 'package:shorebird_code_push_protocol/src/models/release_platform.dart';
|
||||||
|
|
||||||
/// {@template create_release_artifact_response}
|
/// {@template create_release_artifact_response}
|
||||||
@@ -16,6 +17,7 @@ class CreateReleaseArtifactResponse {
|
|||||||
required this.hash,
|
required this.hash,
|
||||||
required this.size,
|
required this.size,
|
||||||
required this.url,
|
required this.url,
|
||||||
|
this.uploadMethod,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Converts a `Map<String, dynamic>` to a [CreateReleaseArtifactResponse].
|
/// Converts a `Map<String, dynamic>` to a [CreateReleaseArtifactResponse].
|
||||||
@@ -31,6 +33,9 @@ class CreateReleaseArtifactResponse {
|
|||||||
hash: json['hash'] as String,
|
hash: json['hash'] as String,
|
||||||
size: json['size'] as int,
|
size: json['size'] as int,
|
||||||
url: json['url'] as String,
|
url: json['url'] as String,
|
||||||
|
uploadMethod: ArtifactUploadMethod.maybeFromJson(
|
||||||
|
json['upload_method'] as String?,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -64,9 +69,16 @@ class CreateReleaseArtifactResponse {
|
|||||||
/// The size of the artifact in bytes.
|
/// The size of the artifact in bytes.
|
||||||
final int size;
|
final int size;
|
||||||
|
|
||||||
/// The upload URL for the artifact.
|
/// The upload URL for the artifact (a signed URL for [ArtifactUploadMethod
|
||||||
|
/// .multipart], or a resumable session URI for [ArtifactUploadMethod
|
||||||
|
/// .resumable]).
|
||||||
final String url;
|
final String url;
|
||||||
|
|
||||||
|
/// How the client should upload the artifact bytes to [url]. Null on
|
||||||
|
/// responses from older servers, which always implied
|
||||||
|
/// [ArtifactUploadMethod.multipart].
|
||||||
|
final ArtifactUploadMethod? uploadMethod;
|
||||||
|
|
||||||
/// Converts a [CreateReleaseArtifactResponse] to a `Map<String, dynamic>`.
|
/// Converts a [CreateReleaseArtifactResponse] to a `Map<String, dynamic>`.
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
@@ -77,6 +89,7 @@ class CreateReleaseArtifactResponse {
|
|||||||
'hash': hash,
|
'hash': hash,
|
||||||
'size': size,
|
'size': size,
|
||||||
'url': url,
|
'url': url,
|
||||||
|
'upload_method': uploadMethod?.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +102,7 @@ class CreateReleaseArtifactResponse {
|
|||||||
hash,
|
hash,
|
||||||
size,
|
size,
|
||||||
url,
|
url,
|
||||||
|
uploadMethod,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -101,6 +115,7 @@ class CreateReleaseArtifactResponse {
|
|||||||
platform == other.platform &&
|
platform == other.platform &&
|
||||||
hash == other.hash &&
|
hash == other.hash &&
|
||||||
size == other.size &&
|
size == other.size &&
|
||||||
url == other.url;
|
url == other.url &&
|
||||||
|
uploadMethod == other.uploadMethod;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/src/models/activity_heatmap_cell.dart';
|
||||||
|
|
||||||
|
/// {@template get_activity_heatmap_response}
|
||||||
|
/// The response body for GET /apps/{appId}/metrics/activity-heatmap. A 7×24
|
||||||
|
/// grid of average active devices per UTC weekday-hour, powering the
|
||||||
|
/// insights activity heatmap.
|
||||||
|
/// {@endtemplate}
|
||||||
|
@immutable
|
||||||
|
class GetActivityHeatmapResponse {
|
||||||
|
/// {@macro get_activity_heatmap_response}
|
||||||
|
const GetActivityHeatmapResponse({
|
||||||
|
required this.cells,
|
||||||
|
required this.busiestDayOfWeekUtc,
|
||||||
|
required this.busiestHourUtc,
|
||||||
|
required this.lookbackDays,
|
||||||
|
required this.asOf,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Converts a `Map<String, dynamic>` to a [GetActivityHeatmapResponse].
|
||||||
|
factory GetActivityHeatmapResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return parseFromJson(
|
||||||
|
'GetActivityHeatmapResponse',
|
||||||
|
json,
|
||||||
|
() => GetActivityHeatmapResponse(
|
||||||
|
cells: (json['cells'] as List)
|
||||||
|
.map<ActivityHeatmapCell>(
|
||||||
|
(e) => ActivityHeatmapCell.fromJson(e as Map<String, dynamic>),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
busiestDayOfWeekUtc:
|
||||||
|
checkedKey(json, 'busiest_day_of_week_utc') as int?,
|
||||||
|
busiestHourUtc: checkedKey(json, 'busiest_hour_utc') as int?,
|
||||||
|
lookbackDays: json['lookback_days'] as int,
|
||||||
|
asOf: DateTime.parse(json['as_of'] as String),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json object.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static GetActivityHeatmapResponse? maybeFromJson(Map<String, dynamic>? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GetActivityHeatmapResponse.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 168 entries (7 weekdays × 24 hours), zero-filled, ordered by
|
||||||
|
/// day_of_week_utc (1–7) then hour_utc (0–23) ascending.
|
||||||
|
final List<ActivityHeatmapCell> cells;
|
||||||
|
|
||||||
|
/// UTC day-of-week (1–7) of the cell with the highest average active
|
||||||
|
/// devices, for peak-relative coloring and labeling. Null when there is
|
||||||
|
/// no data.
|
||||||
|
final int? busiestDayOfWeekUtc;
|
||||||
|
|
||||||
|
/// UTC hour (0–23) of the busiest cell. Null when there is no data.
|
||||||
|
final int? busiestHourUtc;
|
||||||
|
|
||||||
|
/// Number of days of history the heatmap is computed over.
|
||||||
|
final int lookbackDays;
|
||||||
|
|
||||||
|
/// Server's UTC timestamp at the moment the response was constructed.
|
||||||
|
/// Not a freshness indicator for the underlying data, which is
|
||||||
|
/// refreshed by an hourly scheduled query and may lag by up to ~1 hour.
|
||||||
|
final DateTime asOf;
|
||||||
|
|
||||||
|
/// Converts a [GetActivityHeatmapResponse] to a `Map<String, dynamic>`.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'cells': cells.map((e) => e.toJson()).toList(),
|
||||||
|
'busiest_day_of_week_utc': busiestDayOfWeekUtc,
|
||||||
|
'busiest_hour_utc': busiestHourUtc,
|
||||||
|
'lookback_days': lookbackDays,
|
||||||
|
'as_of': asOf.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hashAll([
|
||||||
|
listHash(cells),
|
||||||
|
busiestDayOfWeekUtc,
|
||||||
|
busiestHourUtc,
|
||||||
|
lookbackDays,
|
||||||
|
asOf,
|
||||||
|
]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is GetActivityHeatmapResponse &&
|
||||||
|
listsEqual(cells, other.cells) &&
|
||||||
|
busiestDayOfWeekUtc == other.busiestDayOfWeekUtc &&
|
||||||
|
busiestHourUtc == other.busiestHourUtc &&
|
||||||
|
lookbackDays == other.lookbackDays &&
|
||||||
|
asOf == other.asOf;
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||||
|
|
||||||
|
/// {@template get_new_devices_response}
|
||||||
|
/// The response body for GET /apps/{appId}/metrics/new-devices.
|
||||||
|
/// {@endtemplate}
|
||||||
|
@immutable
|
||||||
|
class GetNewDevicesResponse {
|
||||||
|
/// {@macro get_new_devices_response}
|
||||||
|
const GetNewDevicesResponse({
|
||||||
|
required this.current,
|
||||||
|
required this.previous,
|
||||||
|
required this.windowDays,
|
||||||
|
required this.asOf,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Converts a `Map<String, dynamic>` to a [GetNewDevicesResponse].
|
||||||
|
factory GetNewDevicesResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return parseFromJson(
|
||||||
|
'GetNewDevicesResponse',
|
||||||
|
json,
|
||||||
|
() => GetNewDevicesResponse(
|
||||||
|
current: json['current'] as int,
|
||||||
|
previous: checkedKey(json, 'previous') as int?,
|
||||||
|
windowDays: json['window_days'] as int,
|
||||||
|
asOf: DateTime.parse(json['as_of'] as String),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json object.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static GetNewDevicesResponse? maybeFromJson(Map<String, dynamic>? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GetNewDevicesResponse.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devices first seen in the window `[as_of − window_days, as_of)`.
|
||||||
|
/// An exact (non-HLL) count.
|
||||||
|
final int current;
|
||||||
|
|
||||||
|
/// Devices first seen in the equal-length window immediately
|
||||||
|
/// preceding the current one, or null when that window would begin
|
||||||
|
/// before the metrics data floor (comparing against
|
||||||
|
/// partially-recorded history would show a misleading delta).
|
||||||
|
final int? previous;
|
||||||
|
|
||||||
|
/// The window length in days. Hardcoded server-side in v1; echoed
|
||||||
|
/// so clients label the metric from the response rather than
|
||||||
|
/// assuming a length.
|
||||||
|
final int windowDays;
|
||||||
|
|
||||||
|
/// Server's UTC timestamp at the moment the response was
|
||||||
|
/// constructed. Not a freshness indicator for the underlying
|
||||||
|
/// data, which is refreshed by an hourly scheduled query and
|
||||||
|
/// may lag by up to ~1 hour.
|
||||||
|
final DateTime asOf;
|
||||||
|
|
||||||
|
/// Converts a [GetNewDevicesResponse] to a `Map<String, dynamic>`.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'current': current,
|
||||||
|
'previous': previous,
|
||||||
|
'window_days': windowDays,
|
||||||
|
'as_of': asOf.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hashAll([
|
||||||
|
current,
|
||||||
|
previous,
|
||||||
|
windowDays,
|
||||||
|
asOf,
|
||||||
|
]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is GetNewDevicesResponse &&
|
||||||
|
current == other.current &&
|
||||||
|
previous == other.previous &&
|
||||||
|
windowDays == other.windowDays &&
|
||||||
|
asOf == other.asOf;
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/src/models/patch_metric_current_window.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/src/models/patch_metric_window.dart';
|
||||||
|
|
||||||
|
/// {@template get_patch_metric_response}
|
||||||
|
/// The response body for the patch-installs and patch-downloads metric
|
||||||
|
/// endpoints (app- and release-scoped): a current/previous envelope.
|
||||||
|
/// `previous` covers the equal-length window immediately preceding
|
||||||
|
/// `current`; period-over-period deltas are client display logic over the
|
||||||
|
/// two totals. `previous` is omitted only when the prior window predates
|
||||||
|
/// the data floor (no comparison data exists). When it reaches past the
|
||||||
|
/// plan's metrics-history horizon, `previous` is still present with its
|
||||||
|
/// total — the delta renders — but without a `time_series` (no
|
||||||
|
/// prior-window overlay): granular history is the resolution the horizon
|
||||||
|
/// gates, the scalar comparison is not.
|
||||||
|
/// {@endtemplate}
|
||||||
|
@immutable
|
||||||
|
class GetPatchMetricResponse {
|
||||||
|
/// {@macro get_patch_metric_response}
|
||||||
|
const GetPatchMetricResponse({
|
||||||
|
required this.asOf,
|
||||||
|
required this.granularity,
|
||||||
|
required this.current,
|
||||||
|
this.previous,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Converts a `Map<String, dynamic>` to a [GetPatchMetricResponse].
|
||||||
|
factory GetPatchMetricResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return parseFromJson(
|
||||||
|
'GetPatchMetricResponse',
|
||||||
|
json,
|
||||||
|
() => GetPatchMetricResponse(
|
||||||
|
asOf: DateTime.parse(json['as_of'] as String),
|
||||||
|
granularity: checkedKey(json, 'granularity') as String?,
|
||||||
|
current: PatchMetricCurrentWindow.fromJson(
|
||||||
|
json['current'] as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
previous: PatchMetricWindow.maybeFromJson(
|
||||||
|
json['previous'] as Map<String, dynamic>?,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json object.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static GetPatchMetricResponse? maybeFromJson(Map<String, dynamic>? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GetPatchMetricResponse.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Server's UTC timestamp at the moment the response was constructed.
|
||||||
|
/// Not a freshness indicator for the underlying data, which is
|
||||||
|
/// refreshed by an hourly scheduled job and may lag by up to ~1 hour.
|
||||||
|
final DateTime asOf;
|
||||||
|
|
||||||
|
/// The time-series bucket resolution (`hour`, `day`, or `week`), or
|
||||||
|
/// null when no time series was requested. Applies to both windows.
|
||||||
|
final String? granularity;
|
||||||
|
|
||||||
|
/// The `current` window of the patch-metric envelope: the base window atom
|
||||||
|
/// plus the optional `breakdown`. Only `current` carries a breakdown — no
|
||||||
|
/// chart renders a previous-window breakdown, so the asymmetry is declared
|
||||||
|
/// in the contract rather than left as an optional-but-never-populated
|
||||||
|
/// field.
|
||||||
|
final PatchMetricCurrentWindow current;
|
||||||
|
|
||||||
|
/// One window of the patch-metric envelope: the summed count over the
|
||||||
|
/// window's effective range, with a per-bucket series when a `granularity`
|
||||||
|
/// was requested. This base atom is the full shape of `previous`;
|
||||||
|
/// `current` extends it (see PatchMetricCurrentWindow).
|
||||||
|
final PatchMetricWindow? previous;
|
||||||
|
|
||||||
|
/// Converts a [GetPatchMetricResponse] to a `Map<String, dynamic>`.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'as_of': asOf.toIso8601String(),
|
||||||
|
'granularity': granularity,
|
||||||
|
'current': current.toJson(),
|
||||||
|
'previous': previous?.toJson(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hashAll([
|
||||||
|
asOf,
|
||||||
|
granularity,
|
||||||
|
current,
|
||||||
|
previous,
|
||||||
|
]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is GetPatchMetricResponse &&
|
||||||
|
asOf == other.asOf &&
|
||||||
|
granularity == other.granularity &&
|
||||||
|
current == other.current &&
|
||||||
|
previous == other.previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
-8
@@ -15,7 +15,6 @@ class PatchCheckRequest {
|
|||||||
required this.appId,
|
required this.appId,
|
||||||
required this.channel,
|
required this.channel,
|
||||||
this.patchNumber,
|
this.patchNumber,
|
||||||
this.patchHash,
|
|
||||||
this.clientId,
|
this.clientId,
|
||||||
this.currentPatchNumber,
|
this.currentPatchNumber,
|
||||||
});
|
});
|
||||||
@@ -28,7 +27,6 @@ class PatchCheckRequest {
|
|||||||
() => PatchCheckRequest(
|
() => PatchCheckRequest(
|
||||||
releaseVersion: json['release_version'] as String,
|
releaseVersion: json['release_version'] as String,
|
||||||
patchNumber: json['patch_number'] as int?,
|
patchNumber: json['patch_number'] as int?,
|
||||||
patchHash: json['patch_hash'] as String?,
|
|
||||||
platform: ReleasePlatform.fromJson(json['platform'] as String),
|
platform: ReleasePlatform.fromJson(json['platform'] as String),
|
||||||
arch: json['arch'] as String,
|
arch: json['arch'] as String,
|
||||||
appId: json['app_id'] as String,
|
appId: json['app_id'] as String,
|
||||||
@@ -56,9 +54,6 @@ class PatchCheckRequest {
|
|||||||
/// number. If omitted, the server returns the latest available.
|
/// number. If omitted, the server returns the latest available.
|
||||||
final int? patchNumber;
|
final int? patchNumber;
|
||||||
|
|
||||||
/// The current patch hash of the app.
|
|
||||||
final String? patchHash;
|
|
||||||
|
|
||||||
/// A platform to which a Shorebird release can be deployed.
|
/// A platform to which a Shorebird release can be deployed.
|
||||||
final ReleasePlatform platform;
|
final ReleasePlatform platform;
|
||||||
|
|
||||||
@@ -85,7 +80,6 @@ class PatchCheckRequest {
|
|||||||
return {
|
return {
|
||||||
'release_version': releaseVersion,
|
'release_version': releaseVersion,
|
||||||
'patch_number': patchNumber,
|
'patch_number': patchNumber,
|
||||||
'patch_hash': patchHash,
|
|
||||||
'platform': platform.toJson(),
|
'platform': platform.toJson(),
|
||||||
'arch': arch,
|
'arch': arch,
|
||||||
'app_id': appId,
|
'app_id': appId,
|
||||||
@@ -99,7 +93,6 @@ class PatchCheckRequest {
|
|||||||
int get hashCode => Object.hashAll([
|
int get hashCode => Object.hashAll([
|
||||||
releaseVersion,
|
releaseVersion,
|
||||||
patchNumber,
|
patchNumber,
|
||||||
patchHash,
|
|
||||||
platform,
|
platform,
|
||||||
arch,
|
arch,
|
||||||
appId,
|
appId,
|
||||||
@@ -114,7 +107,6 @@ class PatchCheckRequest {
|
|||||||
return other is PatchCheckRequest &&
|
return other is PatchCheckRequest &&
|
||||||
releaseVersion == other.releaseVersion &&
|
releaseVersion == other.releaseVersion &&
|
||||||
patchNumber == other.patchNumber &&
|
patchNumber == other.patchNumber &&
|
||||||
patchHash == other.patchHash &&
|
|
||||||
platform == other.platform &&
|
platform == other.platform &&
|
||||||
arch == other.arch &&
|
arch == other.arch &&
|
||||||
appId == other.appId &&
|
appId == other.appId &&
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||||
|
|
||||||
|
/// {@template activity_heatmap_cell}
|
||||||
|
/// Average number of distinct active devices during one (UTC day-of-week,
|
||||||
|
/// UTC hour-of-day) cell, averaged across every occurrence of that weekday
|
||||||
|
/// in the lookback window (occurrences with no activity count as zero).
|
||||||
|
/// {@endtemplate}
|
||||||
|
@immutable
|
||||||
|
class ActivityHeatmapCell {
|
||||||
|
/// {@macro activity_heatmap_cell}
|
||||||
|
const ActivityHeatmapCell({
|
||||||
|
required this.dayOfWeekUtc,
|
||||||
|
required this.hourUtc,
|
||||||
|
required this.averageActiveDevices,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Converts a `Map<String, dynamic>` to an [ActivityHeatmapCell].
|
||||||
|
factory ActivityHeatmapCell.fromJson(Map<String, dynamic> json) {
|
||||||
|
return parseFromJson(
|
||||||
|
'ActivityHeatmapCell',
|
||||||
|
json,
|
||||||
|
() => ActivityHeatmapCell(
|
||||||
|
dayOfWeekUtc: json['day_of_week_utc'] as int,
|
||||||
|
hourUtc: json['hour_utc'] as int,
|
||||||
|
averageActiveDevices: (json['average_active_devices'] as num)
|
||||||
|
.toDouble(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json object.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static ActivityHeatmapCell? maybeFromJson(Map<String, dynamic>? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ActivityHeatmapCell.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Day of week in UTC, 1–7 where 1 = Sunday and 7 = Saturday.
|
||||||
|
final int dayOfWeekUtc;
|
||||||
|
|
||||||
|
/// Hour of day in UTC, 0–23.
|
||||||
|
final int hourUtc;
|
||||||
|
|
||||||
|
/// Mean distinct active devices seen during this UTC weekday-hour,
|
||||||
|
/// averaged over the weekday's occurrences in the window with implicit
|
||||||
|
/// zeros included.
|
||||||
|
final double averageActiveDevices;
|
||||||
|
|
||||||
|
/// Converts an [ActivityHeatmapCell] to a `Map<String, dynamic>`.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'day_of_week_utc': dayOfWeekUtc,
|
||||||
|
'hour_utc': hourUtc,
|
||||||
|
'average_active_devices': averageActiveDevices,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hashAll([
|
||||||
|
dayOfWeekUtc,
|
||||||
|
hourUtc,
|
||||||
|
averageActiveDevices,
|
||||||
|
]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is ActivityHeatmapCell &&
|
||||||
|
dayOfWeekUtc == other.dayOfWeekUtc &&
|
||||||
|
hourUtc == other.hourUtc &&
|
||||||
|
averageActiveDevices == other.averageActiveDevices;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/// How a client should upload an artifact's bytes to storage.
|
||||||
|
///
|
||||||
|
/// Returned by the create-artifact endpoints so the client knows how to use the
|
||||||
|
/// `url` it was handed. Absent (null) on responses from older servers, which
|
||||||
|
/// always implied [multipart].
|
||||||
|
enum ArtifactUploadMethod {
|
||||||
|
/// Legacy single `multipart/form-data` POST of the file to a signed URL.
|
||||||
|
multipart._('multipart'),
|
||||||
|
|
||||||
|
/// Resumable upload: PUT the bytes (chunked, with `Content-Range`) to a
|
||||||
|
/// server-initiated GCS resumable session URI given in `url`. The session is
|
||||||
|
/// size-bound at initiation, so GCS rejects an oversized upload.
|
||||||
|
resumable._('resumable');
|
||||||
|
|
||||||
|
const ArtifactUploadMethod._(this.value);
|
||||||
|
|
||||||
|
/// Creates an [ArtifactUploadMethod] from a json value.
|
||||||
|
factory ArtifactUploadMethod.fromJson(String json) {
|
||||||
|
return ArtifactUploadMethod.values.firstWhere(
|
||||||
|
(value) => value.value == json,
|
||||||
|
orElse: () =>
|
||||||
|
throw FormatException('Unknown ArtifactUploadMethod value: $json'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json value.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static ArtifactUploadMethod? maybeFromJson(String? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ArtifactUploadMethod.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The wire value of the enum, used for network transport.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Converts the enum to its json value.
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
/// Returns the string form of the enum.
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
enum GetAppPatchDownloadsParameter2 {
|
||||||
|
hour._('hour'),
|
||||||
|
day._('day'),
|
||||||
|
week._('week');
|
||||||
|
|
||||||
|
const GetAppPatchDownloadsParameter2._(this.value);
|
||||||
|
|
||||||
|
/// Creates a GetAppPatchDownloadsParameter2 from a json value.
|
||||||
|
factory GetAppPatchDownloadsParameter2.fromJson(String json) {
|
||||||
|
return GetAppPatchDownloadsParameter2.values.firstWhere(
|
||||||
|
(value) => value.value == json,
|
||||||
|
orElse: () => throw FormatException(
|
||||||
|
'Unknown GetAppPatchDownloadsParameter2 value: $json',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json value.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static GetAppPatchDownloadsParameter2? maybeFromJson(String? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GetAppPatchDownloadsParameter2.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of the enum. This is the exact value
|
||||||
|
/// from the OpenAPI spec and will be used for network transport.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Converts the enum to its json value.
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
/// Returns the string form of the enum.
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
enum GetAppPatchDownloadsParameter3 {
|
||||||
|
release._('release');
|
||||||
|
|
||||||
|
const GetAppPatchDownloadsParameter3._(this.value);
|
||||||
|
|
||||||
|
/// Creates a GetAppPatchDownloadsParameter3 from a json value.
|
||||||
|
factory GetAppPatchDownloadsParameter3.fromJson(String json) {
|
||||||
|
return GetAppPatchDownloadsParameter3.values.firstWhere(
|
||||||
|
(value) => value.value == json,
|
||||||
|
orElse: () => throw FormatException(
|
||||||
|
'Unknown GetAppPatchDownloadsParameter3 value: $json',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json value.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static GetAppPatchDownloadsParameter3? maybeFromJson(String? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GetAppPatchDownloadsParameter3.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of the enum. This is the exact value
|
||||||
|
/// from the OpenAPI spec and will be used for network transport.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Converts the enum to its json value.
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
/// Returns the string form of the enum.
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
enum GetAppPatchInstallsParameter2 {
|
||||||
|
hour._('hour'),
|
||||||
|
day._('day'),
|
||||||
|
week._('week');
|
||||||
|
|
||||||
|
const GetAppPatchInstallsParameter2._(this.value);
|
||||||
|
|
||||||
|
/// Creates a GetAppPatchInstallsParameter2 from a json value.
|
||||||
|
factory GetAppPatchInstallsParameter2.fromJson(String json) {
|
||||||
|
return GetAppPatchInstallsParameter2.values.firstWhere(
|
||||||
|
(value) => value.value == json,
|
||||||
|
orElse: () => throw FormatException(
|
||||||
|
'Unknown GetAppPatchInstallsParameter2 value: $json',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json value.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static GetAppPatchInstallsParameter2? maybeFromJson(String? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GetAppPatchInstallsParameter2.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of the enum. This is the exact value
|
||||||
|
/// from the OpenAPI spec and will be used for network transport.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Converts the enum to its json value.
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
/// Returns the string form of the enum.
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
enum GetAppPatchInstallsParameter3 {
|
||||||
|
release._('release');
|
||||||
|
|
||||||
|
const GetAppPatchInstallsParameter3._(this.value);
|
||||||
|
|
||||||
|
/// Creates a GetAppPatchInstallsParameter3 from a json value.
|
||||||
|
factory GetAppPatchInstallsParameter3.fromJson(String json) {
|
||||||
|
return GetAppPatchInstallsParameter3.values.firstWhere(
|
||||||
|
(value) => value.value == json,
|
||||||
|
orElse: () => throw FormatException(
|
||||||
|
'Unknown GetAppPatchInstallsParameter3 value: $json',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json value.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static GetAppPatchInstallsParameter3? maybeFromJson(String? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GetAppPatchInstallsParameter3.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of the enum. This is the exact value
|
||||||
|
/// from the OpenAPI spec and will be used for network transport.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Converts the enum to its json value.
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
/// Returns the string form of the enum.
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
enum GetReleasePatchDownloadsParameter2 {
|
||||||
|
hour._('hour'),
|
||||||
|
day._('day'),
|
||||||
|
week._('week');
|
||||||
|
|
||||||
|
const GetReleasePatchDownloadsParameter2._(this.value);
|
||||||
|
|
||||||
|
/// Creates a GetReleasePatchDownloadsParameter2 from a json value.
|
||||||
|
factory GetReleasePatchDownloadsParameter2.fromJson(String json) {
|
||||||
|
return GetReleasePatchDownloadsParameter2.values.firstWhere(
|
||||||
|
(value) => value.value == json,
|
||||||
|
orElse: () => throw FormatException(
|
||||||
|
'Unknown GetReleasePatchDownloadsParameter2 value: $json',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json value.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static GetReleasePatchDownloadsParameter2? maybeFromJson(String? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GetReleasePatchDownloadsParameter2.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of the enum. This is the exact value
|
||||||
|
/// from the OpenAPI spec and will be used for network transport.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Converts the enum to its json value.
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
/// Returns the string form of the enum.
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
enum GetReleasePatchDownloadsParameter3 {
|
||||||
|
patch._('patch');
|
||||||
|
|
||||||
|
const GetReleasePatchDownloadsParameter3._(this.value);
|
||||||
|
|
||||||
|
/// Creates a GetReleasePatchDownloadsParameter3 from a json value.
|
||||||
|
factory GetReleasePatchDownloadsParameter3.fromJson(String json) {
|
||||||
|
return GetReleasePatchDownloadsParameter3.values.firstWhere(
|
||||||
|
(value) => value.value == json,
|
||||||
|
orElse: () => throw FormatException(
|
||||||
|
'Unknown GetReleasePatchDownloadsParameter3 value: $json',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json value.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static GetReleasePatchDownloadsParameter3? maybeFromJson(String? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GetReleasePatchDownloadsParameter3.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of the enum. This is the exact value
|
||||||
|
/// from the OpenAPI spec and will be used for network transport.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Converts the enum to its json value.
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
/// Returns the string form of the enum.
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
enum GetReleasePatchInstallsParameter2 {
|
||||||
|
hour._('hour'),
|
||||||
|
day._('day'),
|
||||||
|
week._('week');
|
||||||
|
|
||||||
|
const GetReleasePatchInstallsParameter2._(this.value);
|
||||||
|
|
||||||
|
/// Creates a GetReleasePatchInstallsParameter2 from a json value.
|
||||||
|
factory GetReleasePatchInstallsParameter2.fromJson(String json) {
|
||||||
|
return GetReleasePatchInstallsParameter2.values.firstWhere(
|
||||||
|
(value) => value.value == json,
|
||||||
|
orElse: () => throw FormatException(
|
||||||
|
'Unknown GetReleasePatchInstallsParameter2 value: $json',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json value.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static GetReleasePatchInstallsParameter2? maybeFromJson(String? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GetReleasePatchInstallsParameter2.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of the enum. This is the exact value
|
||||||
|
/// from the OpenAPI spec and will be used for network transport.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Converts the enum to its json value.
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
/// Returns the string form of the enum.
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
enum GetReleasePatchInstallsParameter3 {
|
||||||
|
patch._('patch');
|
||||||
|
|
||||||
|
const GetReleasePatchInstallsParameter3._(this.value);
|
||||||
|
|
||||||
|
/// Creates a GetReleasePatchInstallsParameter3 from a json value.
|
||||||
|
factory GetReleasePatchInstallsParameter3.fromJson(String json) {
|
||||||
|
return GetReleasePatchInstallsParameter3.values.firstWhere(
|
||||||
|
(value) => value.value == json,
|
||||||
|
orElse: () => throw FormatException(
|
||||||
|
'Unknown GetReleasePatchInstallsParameter3 value: $json',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json value.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static GetReleasePatchInstallsParameter3? maybeFromJson(String? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GetReleasePatchInstallsParameter3.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of the enum. This is the exact value
|
||||||
|
/// from the OpenAPI spec and will be used for network transport.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Converts the enum to its json value.
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
/// Returns the string form of the enum.
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
}
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
enum GetUniqueUsersParameter3 {
|
enum GetUniqueUsersParameter3 {
|
||||||
platform._('platform');
|
platform._('platform'),
|
||||||
|
releaseVersion._('release_version');
|
||||||
|
|
||||||
const GetUniqueUsersParameter3._(this.value);
|
const GetUniqueUsersParameter3._(this.value);
|
||||||
|
|
||||||
|
|||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/src/models/patch_metric_time_series_entry.dart';
|
||||||
|
|
||||||
|
/// {@template patch_metric_breakdown_entry}
|
||||||
|
/// A patch metric for one value of the `group_by` dimension (one release
|
||||||
|
/// at app scope, or one patch at release scope), optionally with its own
|
||||||
|
/// time series.
|
||||||
|
/// {@endtemplate}
|
||||||
|
@immutable
|
||||||
|
class PatchMetricBreakdownEntry {
|
||||||
|
/// {@macro patch_metric_breakdown_entry}
|
||||||
|
const PatchMetricBreakdownEntry({
|
||||||
|
required this.groupBy,
|
||||||
|
required this.groupValue,
|
||||||
|
required this.count,
|
||||||
|
this.timeSeries,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Converts a `Map<String, dynamic>` to a [PatchMetricBreakdownEntry].
|
||||||
|
factory PatchMetricBreakdownEntry.fromJson(Map<String, dynamic> json) {
|
||||||
|
return parseFromJson(
|
||||||
|
'PatchMetricBreakdownEntry',
|
||||||
|
json,
|
||||||
|
() => PatchMetricBreakdownEntry(
|
||||||
|
groupBy: json['group_by'] as String,
|
||||||
|
groupValue: json['group_value'] as String,
|
||||||
|
count: json['count'] as int,
|
||||||
|
timeSeries: (json['time_series'] as List?)
|
||||||
|
?.map<PatchMetricTimeSeriesEntry>(
|
||||||
|
(e) => PatchMetricTimeSeriesEntry.fromJson(
|
||||||
|
e as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json object.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static PatchMetricBreakdownEntry? maybeFromJson(Map<String, dynamic>? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return PatchMetricBreakdownEntry.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The dimension this entry breaks down by ("release" or "patch").
|
||||||
|
final String groupBy;
|
||||||
|
|
||||||
|
/// The value within `group_by`: the release version, or the patch
|
||||||
|
/// number as a string.
|
||||||
|
final String groupValue;
|
||||||
|
|
||||||
|
/// The summed count for this group over the window.
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
/// Per-bucket series for this group, present only when a `granularity`
|
||||||
|
/// was requested; otherwise null.
|
||||||
|
final List<PatchMetricTimeSeriesEntry>? timeSeries;
|
||||||
|
|
||||||
|
/// Converts a [PatchMetricBreakdownEntry] to a `Map<String, dynamic>`.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'group_by': groupBy,
|
||||||
|
'group_value': groupValue,
|
||||||
|
'count': count,
|
||||||
|
'time_series': timeSeries?.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hashAll([
|
||||||
|
groupBy,
|
||||||
|
groupValue,
|
||||||
|
count,
|
||||||
|
listHash(timeSeries),
|
||||||
|
]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is PatchMetricBreakdownEntry &&
|
||||||
|
groupBy == other.groupBy &&
|
||||||
|
groupValue == other.groupValue &&
|
||||||
|
count == other.count &&
|
||||||
|
listsEqual(timeSeries, other.timeSeries);
|
||||||
|
}
|
||||||
|
}
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/src/models/metrics_range.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/src/models/patch_metric_breakdown_entry.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/src/models/patch_metric_time_series_entry.dart';
|
||||||
|
|
||||||
|
/// {@template patch_metric_current_window}
|
||||||
|
/// The `current` window of the patch-metric envelope: the base window atom
|
||||||
|
/// plus the optional `breakdown`. Only `current` carries a breakdown — no
|
||||||
|
/// chart renders a previous-window breakdown, so the asymmetry is declared
|
||||||
|
/// in the contract rather than left as an optional-but-never-populated
|
||||||
|
/// field.
|
||||||
|
/// {@endtemplate}
|
||||||
|
@immutable
|
||||||
|
class PatchMetricCurrentWindow {
|
||||||
|
/// {@macro patch_metric_current_window}
|
||||||
|
const PatchMetricCurrentWindow({
|
||||||
|
required this.count,
|
||||||
|
required this.range,
|
||||||
|
this.timeSeries,
|
||||||
|
this.breakdown,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Converts a `Map<String, dynamic>` to a [PatchMetricCurrentWindow].
|
||||||
|
factory PatchMetricCurrentWindow.fromJson(Map<String, dynamic> json) {
|
||||||
|
return parseFromJson(
|
||||||
|
'PatchMetricCurrentWindow',
|
||||||
|
json,
|
||||||
|
() => PatchMetricCurrentWindow(
|
||||||
|
count: json['count'] as int,
|
||||||
|
range: MetricsRange.fromJson(json['range'] as Map<String, dynamic>),
|
||||||
|
timeSeries: (json['time_series'] as List?)
|
||||||
|
?.map<PatchMetricTimeSeriesEntry>(
|
||||||
|
(e) => PatchMetricTimeSeriesEntry.fromJson(
|
||||||
|
e as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
breakdown: (json['breakdown'] as List?)
|
||||||
|
?.map<PatchMetricBreakdownEntry>(
|
||||||
|
(e) =>
|
||||||
|
PatchMetricBreakdownEntry.fromJson(e as Map<String, dynamic>),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json object.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static PatchMetricCurrentWindow? maybeFromJson(Map<String, dynamic>? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return PatchMetricCurrentWindow.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The summed count (installs or downloads) over this window. Unlike
|
||||||
|
/// the HLL-based metrics, per-bucket `time_series` values sum to this
|
||||||
|
/// total.
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
/// The effective (post-default, post-clamp) window a metrics response —
|
||||||
|
/// or one window of a metrics envelope — covers. Always echoed by the
|
||||||
|
/// server; clients must treat it as authoritative rather than reusing
|
||||||
|
/// the requested range.
|
||||||
|
final MetricsRange range;
|
||||||
|
|
||||||
|
/// Per-bucket series for this window, present only when a
|
||||||
|
/// `granularity` was requested; otherwise null. On `previous`, also
|
||||||
|
/// null when the prior window reaches past the plan's metrics-history
|
||||||
|
/// horizon (the total is still present — only the granular overlay is
|
||||||
|
/// withheld). Empty buckets are omitted — gap-fill against this
|
||||||
|
/// window's `range`.
|
||||||
|
final List<PatchMetricTimeSeriesEntry>? timeSeries;
|
||||||
|
|
||||||
|
/// Per-group counts for this window, present only when a
|
||||||
|
/// `group_by` was requested; otherwise null.
|
||||||
|
final List<PatchMetricBreakdownEntry>? breakdown;
|
||||||
|
|
||||||
|
/// Converts a [PatchMetricCurrentWindow] to a `Map<String, dynamic>`.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'count': count,
|
||||||
|
'range': range.toJson(),
|
||||||
|
'time_series': timeSeries?.map((e) => e.toJson()).toList(),
|
||||||
|
'breakdown': breakdown?.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hashAll([
|
||||||
|
count,
|
||||||
|
range,
|
||||||
|
listHash(timeSeries),
|
||||||
|
listHash(breakdown),
|
||||||
|
]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is PatchMetricCurrentWindow &&
|
||||||
|
count == other.count &&
|
||||||
|
range == other.range &&
|
||||||
|
listsEqual(timeSeries, other.timeSeries) &&
|
||||||
|
listsEqual(breakdown, other.breakdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||||
|
|
||||||
|
/// {@template patch_metric_time_series_entry}
|
||||||
|
/// One bucket of a patch-metric time series: the summed count (installs or
|
||||||
|
/// downloads, per the endpoint) in the bucket starting at `period`.
|
||||||
|
/// {@endtemplate}
|
||||||
|
@immutable
|
||||||
|
class PatchMetricTimeSeriesEntry {
|
||||||
|
/// {@macro patch_metric_time_series_entry}
|
||||||
|
const PatchMetricTimeSeriesEntry({
|
||||||
|
required this.period,
|
||||||
|
required this.count,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Converts a `Map<String, dynamic>` to a [PatchMetricTimeSeriesEntry].
|
||||||
|
factory PatchMetricTimeSeriesEntry.fromJson(Map<String, dynamic> json) {
|
||||||
|
return parseFromJson(
|
||||||
|
'PatchMetricTimeSeriesEntry',
|
||||||
|
json,
|
||||||
|
() => PatchMetricTimeSeriesEntry(
|
||||||
|
period: DateTime.parse(json['period'] as String),
|
||||||
|
count: json['count'] as int,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json object.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static PatchMetricTimeSeriesEntry? maybeFromJson(Map<String, dynamic>? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return PatchMetricTimeSeriesEntry.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bucket start (UTC).
|
||||||
|
final DateTime period;
|
||||||
|
|
||||||
|
/// The summed count in this bucket.
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
/// Converts a [PatchMetricTimeSeriesEntry] to a `Map<String, dynamic>`.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'period': period.toIso8601String(),
|
||||||
|
'count': count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hashAll([
|
||||||
|
period,
|
||||||
|
count,
|
||||||
|
]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is PatchMetricTimeSeriesEntry &&
|
||||||
|
period == other.period &&
|
||||||
|
count == other.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/src/models/metrics_range.dart';
|
||||||
|
import 'package:shorebird_code_push_protocol/src/models/patch_metric_time_series_entry.dart';
|
||||||
|
|
||||||
|
/// {@template patch_metric_window}
|
||||||
|
/// One window of the patch-metric envelope: the summed count over the
|
||||||
|
/// window's effective range, with a per-bucket series when a `granularity`
|
||||||
|
/// was requested. This base atom is the full shape of `previous`;
|
||||||
|
/// `current` extends it (see PatchMetricCurrentWindow).
|
||||||
|
/// {@endtemplate}
|
||||||
|
@immutable
|
||||||
|
class PatchMetricWindow {
|
||||||
|
/// {@macro patch_metric_window}
|
||||||
|
const PatchMetricWindow({
|
||||||
|
required this.count,
|
||||||
|
required this.range,
|
||||||
|
this.timeSeries,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Converts a `Map<String, dynamic>` to a [PatchMetricWindow].
|
||||||
|
factory PatchMetricWindow.fromJson(Map<String, dynamic> json) {
|
||||||
|
return parseFromJson(
|
||||||
|
'PatchMetricWindow',
|
||||||
|
json,
|
||||||
|
() => PatchMetricWindow(
|
||||||
|
count: json['count'] as int,
|
||||||
|
range: MetricsRange.fromJson(json['range'] as Map<String, dynamic>),
|
||||||
|
timeSeries: (json['time_series'] as List?)
|
||||||
|
?.map<PatchMetricTimeSeriesEntry>(
|
||||||
|
(e) => PatchMetricTimeSeriesEntry.fromJson(
|
||||||
|
e as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience to create a nullable type from a nullable json object.
|
||||||
|
/// Useful when parsing optional fields.
|
||||||
|
static PatchMetricWindow? maybeFromJson(Map<String, dynamic>? json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return PatchMetricWindow.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The summed count (installs or downloads) over this window. Unlike
|
||||||
|
/// the HLL-based metrics, per-bucket `time_series` values sum to this
|
||||||
|
/// total.
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
/// The effective (post-default, post-clamp) window a metrics response —
|
||||||
|
/// or one window of a metrics envelope — covers. Always echoed by the
|
||||||
|
/// server; clients must treat it as authoritative rather than reusing
|
||||||
|
/// the requested range.
|
||||||
|
final MetricsRange range;
|
||||||
|
|
||||||
|
/// Per-bucket series for this window, present only when a
|
||||||
|
/// `granularity` was requested; otherwise null. On `previous`, also
|
||||||
|
/// null when the prior window reaches past the plan's metrics-history
|
||||||
|
/// horizon (the total is still present — only the granular overlay is
|
||||||
|
/// withheld). Empty buckets are omitted — gap-fill against this
|
||||||
|
/// window's `range`.
|
||||||
|
final List<PatchMetricTimeSeriesEntry>? timeSeries;
|
||||||
|
|
||||||
|
/// Converts a [PatchMetricWindow] to a `Map<String, dynamic>`.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'count': count,
|
||||||
|
'range': range.toJson(),
|
||||||
|
'time_series': timeSeries?.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hashAll([
|
||||||
|
count,
|
||||||
|
range,
|
||||||
|
listHash(timeSeries),
|
||||||
|
]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is PatchMetricWindow &&
|
||||||
|
count == other.count &&
|
||||||
|
range == other.range &&
|
||||||
|
listsEqual(timeSeries, other.timeSeries);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -48,7 +48,10 @@ class UniqueUsersBreakdownEntry {
|
|||||||
/// The dimension this entry breaks down by (e.g. "platform").
|
/// The dimension this entry breaks down by (e.g. "platform").
|
||||||
final String groupBy;
|
final String groupBy;
|
||||||
|
|
||||||
/// The value within `group_by` (e.g. "android").
|
/// The value within `group_by` (e.g. "android"). For
|
||||||
|
/// `group_by=release_version`, the empty string is the
|
||||||
|
/// unknown-version group — devices whose client is too old to
|
||||||
|
/// report a release version.
|
||||||
final String groupValue;
|
final String groupValue;
|
||||||
|
|
||||||
/// Distinct active devices for this group over the window (an HLL
|
/// Distinct active devices for this group over the window (an HLL
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ environment:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
collection: ^1.19.1
|
collection: ^1.19.1
|
||||||
meta: ^1.16.0
|
meta: ^1.18.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
mocktail: ^1.0.5
|
mocktail: ^1.0.5
|
||||||
# Only used by `tool/gen.dart` to regenerate lib/src/ from the OpenAPI
|
# Only used by `tool/gen.dart` to regenerate lib/src/ from the OpenAPI
|
||||||
# spec.
|
# spec.
|
||||||
space_gen: ^1.2.2
|
space_gen: ^1.3.0
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GetActivityHeatmapResponse', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = GetActivityHeatmapResponse(
|
||||||
|
cells: <ActivityHeatmapCell>[
|
||||||
|
ActivityHeatmapCell(
|
||||||
|
dayOfWeekUtc: 0,
|
||||||
|
hourUtc: 0,
|
||||||
|
averageActiveDevices: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
busiestDayOfWeekUtc: 0,
|
||||||
|
busiestHourUtc: 0,
|
||||||
|
lookbackDays: 0,
|
||||||
|
asOf: DateTime.utc(2024),
|
||||||
|
);
|
||||||
|
final parsed = GetActivityHeatmapResponse.maybeFromJson(
|
||||||
|
instance.toJson(),
|
||||||
|
);
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(GetActivityHeatmapResponse.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => GetActivityHeatmapResponse.maybeFromJson(<String, dynamic>{}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GetNewDevicesResponse', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = GetNewDevicesResponse(
|
||||||
|
current: 0,
|
||||||
|
previous: 0,
|
||||||
|
windowDays: 0,
|
||||||
|
asOf: DateTime.utc(2024),
|
||||||
|
);
|
||||||
|
final parsed = GetNewDevicesResponse.maybeFromJson(instance.toJson());
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(GetNewDevicesResponse.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => GetNewDevicesResponse.maybeFromJson(<String, dynamic>{}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GetPatchMetricResponse', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = GetPatchMetricResponse(
|
||||||
|
asOf: DateTime.utc(2024),
|
||||||
|
granularity: 'example',
|
||||||
|
current: PatchMetricCurrentWindow(
|
||||||
|
count: 0,
|
||||||
|
range: MetricsRange(
|
||||||
|
start: DateTime.utc(2024),
|
||||||
|
end: DateTime.utc(2024),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final parsed = GetPatchMetricResponse.maybeFromJson(instance.toJson());
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(GetPatchMetricResponse.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => GetPatchMetricResponse.maybeFromJson(<String, dynamic>{}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('ActivityHeatmapCell', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = ActivityHeatmapCell(
|
||||||
|
dayOfWeekUtc: 0,
|
||||||
|
hourUtc: 0,
|
||||||
|
averageActiveDevices: 0,
|
||||||
|
);
|
||||||
|
final parsed = ActivityHeatmapCell.maybeFromJson(instance.toJson());
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(ActivityHeatmapCell.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => ActivityHeatmapCell.maybeFromJson(<String, dynamic>{}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GetAppPatchDownloadsParameter2', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = GetAppPatchDownloadsParameter2.values.first;
|
||||||
|
final parsed = GetAppPatchDownloadsParameter2.maybeFromJson(
|
||||||
|
instance.toJson(),
|
||||||
|
);
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(GetAppPatchDownloadsParameter2.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => GetAppPatchDownloadsParameter2.maybeFromJson(
|
||||||
|
'__invalid_enum_value__',
|
||||||
|
),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toString matches toJson for every value', () {
|
||||||
|
for (final value in GetAppPatchDownloadsParameter2.values) {
|
||||||
|
expect(value.toString(), equals(value.toJson()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson round-trips every value', () {
|
||||||
|
for (final value in GetAppPatchDownloadsParameter2.values) {
|
||||||
|
expect(
|
||||||
|
GetAppPatchDownloadsParameter2.fromJson(value.toJson()),
|
||||||
|
equals(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GetAppPatchDownloadsParameter3', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = GetAppPatchDownloadsParameter3.values.first;
|
||||||
|
final parsed = GetAppPatchDownloadsParameter3.maybeFromJson(
|
||||||
|
instance.toJson(),
|
||||||
|
);
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(GetAppPatchDownloadsParameter3.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => GetAppPatchDownloadsParameter3.maybeFromJson(
|
||||||
|
'__invalid_enum_value__',
|
||||||
|
),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toString matches toJson for every value', () {
|
||||||
|
for (final value in GetAppPatchDownloadsParameter3.values) {
|
||||||
|
expect(value.toString(), equals(value.toJson()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson round-trips every value', () {
|
||||||
|
for (final value in GetAppPatchDownloadsParameter3.values) {
|
||||||
|
expect(
|
||||||
|
GetAppPatchDownloadsParameter3.fromJson(value.toJson()),
|
||||||
|
equals(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GetAppPatchInstallsParameter2', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = GetAppPatchInstallsParameter2.values.first;
|
||||||
|
final parsed = GetAppPatchInstallsParameter2.maybeFromJson(
|
||||||
|
instance.toJson(),
|
||||||
|
);
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(GetAppPatchInstallsParameter2.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => GetAppPatchInstallsParameter2.maybeFromJson(
|
||||||
|
'__invalid_enum_value__',
|
||||||
|
),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toString matches toJson for every value', () {
|
||||||
|
for (final value in GetAppPatchInstallsParameter2.values) {
|
||||||
|
expect(value.toString(), equals(value.toJson()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson round-trips every value', () {
|
||||||
|
for (final value in GetAppPatchInstallsParameter2.values) {
|
||||||
|
expect(
|
||||||
|
GetAppPatchInstallsParameter2.fromJson(value.toJson()),
|
||||||
|
equals(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GetAppPatchInstallsParameter3', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = GetAppPatchInstallsParameter3.values.first;
|
||||||
|
final parsed = GetAppPatchInstallsParameter3.maybeFromJson(
|
||||||
|
instance.toJson(),
|
||||||
|
);
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(GetAppPatchInstallsParameter3.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => GetAppPatchInstallsParameter3.maybeFromJson(
|
||||||
|
'__invalid_enum_value__',
|
||||||
|
),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toString matches toJson for every value', () {
|
||||||
|
for (final value in GetAppPatchInstallsParameter3.values) {
|
||||||
|
expect(value.toString(), equals(value.toJson()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson round-trips every value', () {
|
||||||
|
for (final value in GetAppPatchInstallsParameter3.values) {
|
||||||
|
expect(
|
||||||
|
GetAppPatchInstallsParameter3.fromJson(value.toJson()),
|
||||||
|
equals(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GetReleasePatchDownloadsParameter2', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = GetReleasePatchDownloadsParameter2.values.first;
|
||||||
|
final parsed = GetReleasePatchDownloadsParameter2.maybeFromJson(
|
||||||
|
instance.toJson(),
|
||||||
|
);
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(GetReleasePatchDownloadsParameter2.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => GetReleasePatchDownloadsParameter2.maybeFromJson(
|
||||||
|
'__invalid_enum_value__',
|
||||||
|
),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toString matches toJson for every value', () {
|
||||||
|
for (final value in GetReleasePatchDownloadsParameter2.values) {
|
||||||
|
expect(value.toString(), equals(value.toJson()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson round-trips every value', () {
|
||||||
|
for (final value in GetReleasePatchDownloadsParameter2.values) {
|
||||||
|
expect(
|
||||||
|
GetReleasePatchDownloadsParameter2.fromJson(value.toJson()),
|
||||||
|
equals(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GetReleasePatchDownloadsParameter3', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = GetReleasePatchDownloadsParameter3.values.first;
|
||||||
|
final parsed = GetReleasePatchDownloadsParameter3.maybeFromJson(
|
||||||
|
instance.toJson(),
|
||||||
|
);
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(GetReleasePatchDownloadsParameter3.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => GetReleasePatchDownloadsParameter3.maybeFromJson(
|
||||||
|
'__invalid_enum_value__',
|
||||||
|
),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toString matches toJson for every value', () {
|
||||||
|
for (final value in GetReleasePatchDownloadsParameter3.values) {
|
||||||
|
expect(value.toString(), equals(value.toJson()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson round-trips every value', () {
|
||||||
|
for (final value in GetReleasePatchDownloadsParameter3.values) {
|
||||||
|
expect(
|
||||||
|
GetReleasePatchDownloadsParameter3.fromJson(value.toJson()),
|
||||||
|
equals(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GetReleasePatchInstallsParameter2', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = GetReleasePatchInstallsParameter2.values.first;
|
||||||
|
final parsed = GetReleasePatchInstallsParameter2.maybeFromJson(
|
||||||
|
instance.toJson(),
|
||||||
|
);
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(GetReleasePatchInstallsParameter2.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => GetReleasePatchInstallsParameter2.maybeFromJson(
|
||||||
|
'__invalid_enum_value__',
|
||||||
|
),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toString matches toJson for every value', () {
|
||||||
|
for (final value in GetReleasePatchInstallsParameter2.values) {
|
||||||
|
expect(value.toString(), equals(value.toJson()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson round-trips every value', () {
|
||||||
|
for (final value in GetReleasePatchInstallsParameter2.values) {
|
||||||
|
expect(
|
||||||
|
GetReleasePatchInstallsParameter2.fromJson(value.toJson()),
|
||||||
|
equals(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GetReleasePatchInstallsParameter3', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = GetReleasePatchInstallsParameter3.values.first;
|
||||||
|
final parsed = GetReleasePatchInstallsParameter3.maybeFromJson(
|
||||||
|
instance.toJson(),
|
||||||
|
);
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(GetReleasePatchInstallsParameter3.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => GetReleasePatchInstallsParameter3.maybeFromJson(
|
||||||
|
'__invalid_enum_value__',
|
||||||
|
),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toString matches toJson for every value', () {
|
||||||
|
for (final value in GetReleasePatchInstallsParameter3.values) {
|
||||||
|
expect(value.toString(), equals(value.toJson()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson round-trips every value', () {
|
||||||
|
for (final value in GetReleasePatchInstallsParameter3.values) {
|
||||||
|
expect(
|
||||||
|
GetReleasePatchInstallsParameter3.fromJson(value.toJson()),
|
||||||
|
equals(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('PatchMetricBreakdownEntry', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = PatchMetricBreakdownEntry(
|
||||||
|
groupBy: 'example',
|
||||||
|
groupValue: 'example',
|
||||||
|
count: 0,
|
||||||
|
);
|
||||||
|
final parsed = PatchMetricBreakdownEntry.maybeFromJson(
|
||||||
|
instance.toJson(),
|
||||||
|
);
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(PatchMetricBreakdownEntry.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => PatchMetricBreakdownEntry.maybeFromJson(<String, dynamic>{}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('PatchMetricCurrentWindow', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = PatchMetricCurrentWindow(
|
||||||
|
count: 0,
|
||||||
|
range: MetricsRange(
|
||||||
|
start: DateTime.utc(2024),
|
||||||
|
end: DateTime.utc(2024),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final parsed = PatchMetricCurrentWindow.maybeFromJson(instance.toJson());
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(PatchMetricCurrentWindow.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => PatchMetricCurrentWindow.maybeFromJson(<String, dynamic>{}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('PatchMetricTimeSeriesEntry', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = PatchMetricTimeSeriesEntry(
|
||||||
|
period: DateTime.utc(2024),
|
||||||
|
count: 0,
|
||||||
|
);
|
||||||
|
final parsed = PatchMetricTimeSeriesEntry.maybeFromJson(
|
||||||
|
instance.toJson(),
|
||||||
|
);
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(PatchMetricTimeSeriesEntry.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => PatchMetricTimeSeriesEntry.maybeFromJson(<String, dynamic>{}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
// GENERATED — do not hand-edit.
|
||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('PatchMetricWindow', () {
|
||||||
|
test('round-trips via maybeFromJson/toJson', () {
|
||||||
|
final instance = PatchMetricWindow(
|
||||||
|
count: 0,
|
||||||
|
range: MetricsRange(
|
||||||
|
start: DateTime.utc(2024),
|
||||||
|
end: DateTime.utc(2024),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final parsed = PatchMetricWindow.maybeFromJson(instance.toJson());
|
||||||
|
expect(parsed, equals(instance));
|
||||||
|
expect(parsed.hashCode, equals(instance.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null on null input', () {
|
||||||
|
expect(PatchMetricWindow.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson throws FormatException on invalid input', () {
|
||||||
|
expect(
|
||||||
|
() => PatchMetricWindow.maybeFromJson(<String, dynamic>{}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+16
@@ -18,5 +18,21 @@ void main() {
|
|||||||
equals(request.toJson()),
|
equals(request.toJson()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can be (de)serialized with a resumable upload method', () {
|
||||||
|
const response = CreatePatchArtifactResponse(
|
||||||
|
id: 42,
|
||||||
|
patchId: 1,
|
||||||
|
arch: 'arm64',
|
||||||
|
platform: ReleasePlatform.android,
|
||||||
|
hash: '1234',
|
||||||
|
size: 9876,
|
||||||
|
url: 'https://example.com',
|
||||||
|
uploadMethod: ArtifactUploadMethod.resumable,
|
||||||
|
);
|
||||||
|
final decoded = CreatePatchArtifactResponse.fromJson(response.toJson());
|
||||||
|
expect(decoded.toJson(), equals(response.toJson()));
|
||||||
|
expect(decoded.uploadMethod, equals(ArtifactUploadMethod.resumable));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -18,5 +18,21 @@ void main() {
|
|||||||
equals(response.toJson()),
|
equals(response.toJson()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can be (de)serialized with a resumable upload method', () {
|
||||||
|
const response = CreateReleaseArtifactResponse(
|
||||||
|
id: 42,
|
||||||
|
releaseId: 1,
|
||||||
|
arch: 'arm64',
|
||||||
|
platform: ReleasePlatform.android,
|
||||||
|
hash: '1234',
|
||||||
|
size: 9876,
|
||||||
|
url: 'https://example.com',
|
||||||
|
uploadMethod: ArtifactUploadMethod.resumable,
|
||||||
|
);
|
||||||
|
final decoded = CreateReleaseArtifactResponse.fromJson(response.toJson());
|
||||||
|
expect(decoded.toJson(), equals(response.toJson()));
|
||||||
|
expect(decoded.uploadMethod, equals(ArtifactUploadMethod.resumable));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -6,7 +6,6 @@ void main() {
|
|||||||
const request = PatchCheckRequest(
|
const request = PatchCheckRequest(
|
||||||
releaseVersion: '1',
|
releaseVersion: '1',
|
||||||
patchNumber: 2,
|
patchNumber: 2,
|
||||||
patchHash: '3',
|
|
||||||
platform: ReleasePlatform.android,
|
platform: ReleasePlatform.android,
|
||||||
arch: 'arm64',
|
arch: 'arm64',
|
||||||
appId: 'app_123',
|
appId: 'app_123',
|
||||||
@@ -26,7 +25,6 @@ void main() {
|
|||||||
const copy = PatchCheckRequest(
|
const copy = PatchCheckRequest(
|
||||||
releaseVersion: '1',
|
releaseVersion: '1',
|
||||||
patchNumber: 2,
|
patchNumber: 2,
|
||||||
patchHash: '3',
|
|
||||||
platform: ReleasePlatform.android,
|
platform: ReleasePlatform.android,
|
||||||
arch: 'arm64',
|
arch: 'arm64',
|
||||||
appId: 'app_123',
|
appId: 'app_123',
|
||||||
@@ -37,7 +35,6 @@ void main() {
|
|||||||
const different = PatchCheckRequest(
|
const different = PatchCheckRequest(
|
||||||
releaseVersion: '1',
|
releaseVersion: '1',
|
||||||
patchNumber: 2,
|
patchNumber: 2,
|
||||||
patchHash: '3',
|
|
||||||
platform: ReleasePlatform.android,
|
platform: ReleasePlatform.android,
|
||||||
arch: 'arm64',
|
arch: 'arm64',
|
||||||
appId: 'app_123',
|
appId: 'app_123',
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group(ArtifactUploadMethod, () {
|
||||||
|
test('round-trips through json', () {
|
||||||
|
for (final method in ArtifactUploadMethod.values) {
|
||||||
|
expect(ArtifactUploadMethod.fromJson(method.toJson()), equals(method));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maybeFromJson returns null for null', () {
|
||||||
|
expect(ArtifactUploadMethod.maybeFromJson(null), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson throws on an unknown value', () {
|
||||||
|
expect(
|
||||||
|
() => ArtifactUploadMethod.fromJson('nope'),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toString returns the wire value', () {
|
||||||
|
expect(ArtifactUploadMethod.multipart.toString(), 'multipart');
|
||||||
|
expect(ArtifactUploadMethod.resumable.toString(), 'resumable');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -36,7 +36,10 @@ class StripeApi {
|
|||||||
|
|
||||||
final response = await _client.get(uri, headers: _authHeaders);
|
final response = await _client.get(uri, headers: _authHeaders);
|
||||||
if (response.statusCode != HttpStatus.ok) {
|
if (response.statusCode != HttpStatus.ok) {
|
||||||
throw Exception('Failed to retrieve customer with id $customerId');
|
throw StripeApiException.fromResponse(
|
||||||
|
response,
|
||||||
|
message: 'Failed to retrieve customer with id $customerId',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return StripeCustomer.fromJson(
|
return StripeCustomer.fromJson(
|
||||||
@@ -55,8 +58,9 @@ class StripeApi {
|
|||||||
|
|
||||||
final response = await _client.get(uri, headers: _authHeaders);
|
final response = await _client.get(uri, headers: _authHeaders);
|
||||||
if (response.statusCode != HttpStatus.ok) {
|
if (response.statusCode != HttpStatus.ok) {
|
||||||
throw Exception(
|
throw StripeApiException.fromResponse(
|
||||||
'Failed to retrieve subscription with id $subscriptionId',
|
response,
|
||||||
|
message: 'Failed to retrieve subscription with id $subscriptionId',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,10 +100,14 @@ class StripeApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != HttpStatus.ok) {
|
if (response.statusCode != HttpStatus.ok) {
|
||||||
throw Exception('''
|
throw StripeApiException.fromResponse(
|
||||||
|
response,
|
||||||
|
message:
|
||||||
|
'''
|
||||||
Failed to report $value for customer $customerId. Error:
|
Failed to report $value for customer $customerId. Error:
|
||||||
${response.body}
|
${response.body}
|
||||||
''');
|
''',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,10 +154,14 @@ ${response.body}
|
|||||||
|
|
||||||
final response = await _client.get(uri, headers: _authHeaders);
|
final response = await _client.get(uri, headers: _authHeaders);
|
||||||
if (response.statusCode != HttpStatus.ok) {
|
if (response.statusCode != HttpStatus.ok) {
|
||||||
throw Exception('''
|
throw StripeApiException.fromResponse(
|
||||||
|
response,
|
||||||
|
message:
|
||||||
|
'''
|
||||||
Failed to get paged response from $path with params $queryParameters. Error:
|
Failed to get paged response from $path with params $queryParameters. Error:
|
||||||
${response.body}
|
${response.body}
|
||||||
''');
|
''',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final pagedResponse = PagedResponse.fromJson(
|
final pagedResponse = PagedResponse.fromJson(
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
/// {@template stripe_api_exception}
|
||||||
|
/// An exception thrown when a request to the Stripe API returns a non-success
|
||||||
|
/// response.
|
||||||
|
///
|
||||||
|
/// Carries the HTTP [statusCode] of the failed response so callers can
|
||||||
|
/// distinguish, for example, a missing resource (`404`) from rate limiting
|
||||||
|
/// (`429`). When the response body is a Stripe error object, the
|
||||||
|
/// machine-readable [code] (e.g. `resource_missing`, `rate_limit`) is also
|
||||||
|
/// surfaced.
|
||||||
|
///
|
||||||
|
/// See https://docs.stripe.com/api/errors.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class StripeApiException implements Exception {
|
||||||
|
/// {@macro stripe_api_exception}
|
||||||
|
StripeApiException({
|
||||||
|
required this.statusCode,
|
||||||
|
required this.message,
|
||||||
|
this.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Builds a [StripeApiException] from a failed [response].
|
||||||
|
///
|
||||||
|
/// [message] is a human-readable description supplied by the caller and is
|
||||||
|
/// preserved verbatim. The Stripe error [code] is parsed from the response
|
||||||
|
/// body when it is a JSON error object.
|
||||||
|
///
|
||||||
|
/// Never throws: a non-JSON or unexpected body (e.g. an HTML gateway error)
|
||||||
|
/// simply leaves [code] null.
|
||||||
|
factory StripeApiException.fromResponse(
|
||||||
|
http.Response response, {
|
||||||
|
required String message,
|
||||||
|
}) {
|
||||||
|
String? code;
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(response.body);
|
||||||
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
final error = decoded['error'];
|
||||||
|
if (error is Map<String, dynamic> && error['code'] is String) {
|
||||||
|
code = error['code'] as String;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} on Object catch (_) {
|
||||||
|
// Non-JSON or unexpected body; the status code is still meaningful.
|
||||||
|
}
|
||||||
|
return StripeApiException(
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
message: message,
|
||||||
|
code: code,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The HTTP status code of the failed response.
|
||||||
|
final int statusCode;
|
||||||
|
|
||||||
|
/// The Stripe machine-readable error code (e.g. `resource_missing`), when the
|
||||||
|
/// response body was a Stripe error object; otherwise null.
|
||||||
|
final String? code;
|
||||||
|
|
||||||
|
/// A human-readable description of the failure.
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'StripeApiException($statusCode${code == null ? '' : ', $code'}): '
|
||||||
|
'$message';
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export 'src/models/models.dart';
|
export 'src/models/models.dart';
|
||||||
export 'src/stripe_api.dart';
|
export 'src/stripe_api.dart';
|
||||||
|
export 'src/stripe_api_exception.dart';
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ dev_dependencies:
|
|||||||
build_runner: ^2.15.0
|
build_runner: ^2.15.0
|
||||||
json_serializable: ^6.13.2
|
json_serializable: ^6.13.2
|
||||||
mocktail: ^1.0.5
|
mocktail: ^1.0.5
|
||||||
test: ^1.31.1
|
test: ^1.31.2
|
||||||
|
|||||||
@@ -130,6 +130,32 @@ void main() {
|
|||||||
).called(1);
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('throws StripeApiException carrying status and code on 404', () {
|
||||||
|
when(
|
||||||
|
() => httpClient.get(uri, headers: any(named: 'headers')),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'error': {
|
||||||
|
'type': 'invalid_request_error',
|
||||||
|
'code': 'resource_missing',
|
||||||
|
'message': "No such customer: 'cus_123'",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
HttpStatus.notFound,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => stripeApi.fetchCustomer(customerId: 'cus_123'),
|
||||||
|
throwsA(
|
||||||
|
isA<StripeApiException>()
|
||||||
|
.having((e) => e.statusCode, 'statusCode', HttpStatus.notFound)
|
||||||
|
.having((e) => e.code, 'code', 'resource_missing'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('returns a customer on successful request', () async {
|
test('returns a customer on successful request', () async {
|
||||||
when(
|
when(
|
||||||
() => httpClient.get(uri, headers: any(named: 'headers')),
|
() => httpClient.get(uri, headers: any(named: 'headers')),
|
||||||
@@ -171,6 +197,36 @@ void main() {
|
|||||||
).called(1);
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('throws StripeApiException carrying status 429 on rate limit', () {
|
||||||
|
when(
|
||||||
|
() => httpClient.get(uri, headers: any(named: 'headers')),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'error': {
|
||||||
|
'type': 'api_error',
|
||||||
|
'code': 'rate_limit',
|
||||||
|
'message': 'Too many requests',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
HttpStatus.tooManyRequests,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => stripeApi.fetchSubscription(subscriptionId: subscriptionId),
|
||||||
|
throwsA(
|
||||||
|
isA<StripeApiException>()
|
||||||
|
.having(
|
||||||
|
(e) => e.statusCode,
|
||||||
|
'statusCode',
|
||||||
|
HttpStatus.tooManyRequests,
|
||||||
|
)
|
||||||
|
.having((e) => e.code, 'code', 'rate_limit'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('returns a subscription on successful request', () async {
|
test('returns a subscription on successful request', () async {
|
||||||
when(
|
when(
|
||||||
() => httpClient.get(uri, headers: any(named: 'headers')),
|
() => httpClient.get(uri, headers: any(named: 'headers')),
|
||||||
@@ -409,4 +465,58 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group(StripeApiException, () {
|
||||||
|
test('fromResponse parses the Stripe error code from a JSON body', () {
|
||||||
|
final exception = StripeApiException.fromResponse(
|
||||||
|
http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'error': {
|
||||||
|
'code': 'resource_missing',
|
||||||
|
'message': 'No such customer',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
HttpStatus.notFound,
|
||||||
|
),
|
||||||
|
message: 'fallback',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(exception.statusCode, HttpStatus.notFound);
|
||||||
|
expect(exception.code, 'resource_missing');
|
||||||
|
expect(exception.message, 'fallback');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromResponse leaves code null on a non-JSON body', () {
|
||||||
|
final exception = StripeApiException.fromResponse(
|
||||||
|
http.Response('<html>502 Bad Gateway</html>', HttpStatus.badGateway),
|
||||||
|
message: 'fallback',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(exception.statusCode, HttpStatus.badGateway);
|
||||||
|
expect(exception.code, isNull);
|
||||||
|
expect(exception.message, 'fallback');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromResponse leaves code null when the body has no error object', () {
|
||||||
|
final exception = StripeApiException.fromResponse(
|
||||||
|
http.Response(jsonEncode({'ok': true}), HttpStatus.badRequest),
|
||||||
|
message: 'fallback',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(exception.code, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toString includes the status code and error code', () {
|
||||||
|
final exception = StripeApiException(
|
||||||
|
statusCode: HttpStatus.notFound,
|
||||||
|
message: 'No such customer',
|
||||||
|
code: 'resource_missing',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
exception.toString(),
|
||||||
|
'StripeApiException(404, resource_missing): No such customer',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-35
@@ -5,18 +5,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: "8d718c5c58904f9937290fd5dbf2d6a0e02456867706bfb6cd7b81d394e738d5"
|
sha256: "1b0e6a07425a3e460666e88bf1c949ccc7bb0116ad562ce94a1eca60fe820725"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "98.0.0"
|
version: "103.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "6141ad5d092d1e1d13929c0504658bbeccc1703505830d7c26e859908f5efc88"
|
sha256: "61c04d0c1bfed555c681ea079519933f071a5a026578ff73c4ff0df2d3462e5e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.0.0"
|
version: "13.3.0"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -61,10 +61,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build
|
name: build
|
||||||
sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c
|
sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.5"
|
version: "4.0.6"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -197,10 +197,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_style
|
name: dart_style
|
||||||
sha256: a4c1ccfee44c7e75ed80484071a5c142a385345e658fd8bd7c4b5c97e7198f98
|
sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.8"
|
version: "3.1.9"
|
||||||
decimal:
|
decimal:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -213,10 +213,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: equatable
|
name: equatable
|
||||||
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.1.0"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -277,10 +277,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: googleapis_auth
|
name: googleapis_auth
|
||||||
sha256: "2a8895c3885197f96bb2fd91ee0ae77b53ff3874c7b1f1eadb6566248e880958"
|
sha256: "1417d8846663df5e7b77ca56591c5edd442c66ffc9c01ab036e138a21a148e86"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.1"
|
version: "2.3.2"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -333,10 +333,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
version: "0.20.3"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -413,10 +413,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739
|
sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.2"
|
version: "1.18.3"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -517,10 +517,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: propertylistserialization
|
name: propertylistserialization
|
||||||
sha256: "645aaac37e73735286087fc81b27c7eabaa81b6146881bdf84090ba56f4e15ad"
|
sha256: "8ebe5e73b912c080916895bff96af9dd026cf87c1867447894f84e4c9e51b4b8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.5.0"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -637,18 +637,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_gen
|
name: source_gen
|
||||||
sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd"
|
sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.2"
|
version: "4.2.3"
|
||||||
source_helper:
|
source_helper:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_helper
|
name: source_helper
|
||||||
sha256: "1d3b229b2934034fb2e691fbb3d53e0f75a4af7b1407f88425ed8f209bcb1b8f"
|
sha256: "4227d54ceefd0bb8ca4c8fcb96e1719dc53f1ee1b6e2ca9d7a6069da160e4eae"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.11"
|
version: "1.3.12"
|
||||||
source_map_stack_trace:
|
source_map_stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -677,10 +677,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: space_gen
|
name: space_gen
|
||||||
sha256: "3aea8da8ffba5802110322093ef4e921e297d9b55ffa073a74b6cb38fd225ee1"
|
sha256: fb609d3e1d992e8da943990d0bae08bb0b5a05a20c95b66feee1f2a30c0ad60b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.3.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -725,26 +725,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f
|
sha256: "0d5ba5602ec3baa28c8ce365e1efc5575969c765f45c554a3e167dc7945b9c30"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.31.1"
|
version: "1.31.2"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
|
sha256: "475610b2aa23c19687cce2961e44b0cc57cafe220f67c2b80201231b2a07fbe7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.12"
|
version: "0.7.13"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5
|
sha256: a39c204a4fc7a7ccb04a2b985e359fda3cc37e45e0b8ac61c3fb1a05aa832132
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.18"
|
version: "0.6.19"
|
||||||
ttl_cache:
|
ttl_cache:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -781,10 +781,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: very_good_analysis
|
name: very_good_analysis
|
||||||
sha256: d1cb1d66a5aae2c702d68caca6c8347306d35e728fd94555fa21fa0448a972e0
|
sha256: "481af67ab5877af20325251dc215a4ebac7666a1c8cf09198ffd457bc612b33d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.2.0"
|
version: "10.3.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -845,10 +845,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.6.1"
|
version: "7.0.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -866,4 +866,4 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.4"
|
version: "2.2.4"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.11.0 <4.0.0"
|
dart: ">=3.12.0 <4.0.0"
|
||||||
|
|||||||
+1
-1
@@ -19,4 +19,4 @@ workspace:
|
|||||||
- packages/stripe_api
|
- packages/stripe_api
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
very_good_analysis: ^10.2.0
|
very_good_analysis: ^10.3.0
|
||||||
|
|||||||
Reference in New Issue
Block a user