diff --git a/.github/workflows/action_deploy_docs.yml b/.github/workflows/action_deploy_docs.yml index c47776be1..cd11f21e9 100644 --- a/.github/workflows/action_deploy_docs.yml +++ b/.github/workflows/action_deploy_docs.yml @@ -39,7 +39,7 @@ jobs: uses: actions/upload-pages-artifact@v4 with: name: generated_docs - path: './generated_docs' + path: './docs/site' # Deployment job deploy: diff --git a/.github/workflows/action_pull_request.yml b/.github/workflows/action_pull_request.yml index eb7c79eac..7ded9608d 100644 --- a/.github/workflows/action_pull_request.yml +++ b/.github/workflows/action_pull_request.yml @@ -46,7 +46,7 @@ jobs: - fastlane/Appfile - Gemfile - Gemfile.lock - - .github/workflows/** + - .github/workflows/action_pull_request.yml common-kotlin: &common-kotlin - *common-fastlane - build-logic/** diff --git a/.gitignore b/.gitignore index c9bf9ebc3..b8703f38f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ generated_docs/* .kotlin docs/__pycache__/ docs/site +docs/docs/overrides/homepage-content.html **/.gradle/** **/build/** **/.idea/** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fac764f5c..482717c60 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,6 +34,29 @@ It's a good idea to sanity check your work by using the library through the demo Open the root of this repo in Android Studio and run the `samples.demo-kmm.AndroidApp` or `samples.demo-android` targets. The KMM iOS app can also be run through XCode as normal. Note: Currently there's no way to test the Swift package locally without it first being deployed. +## Internal API (`@InternalMockzillaApi`) + +Some types must be `public` because they are shared across library modules (e.g. DTOs used by both `mockzilla` and `mockzilla-management`), but they are **not intended for use by external consumers**. + +These are annotated with `@InternalMockzillaApi` (defined in `mockzilla-common`). The annotation is a `@RequiresOptIn` at the `ERROR` level, so consumers who accidentally reference an internal type will get a compile-time error. + +**When to apply it:** Any public declaration (class, interface, function, property) that lives in a `*.internal.*` package. + +**When NOT to apply it:** +- Swift/Objective-C interop entry points (e.g. `AsyncUtils.kt`, `NestedClassBridgeGeneration.kt` on iOS) — Swift has no way to satisfy `@OptIn` requirements, so annotating these would break the Swift bridge. +- `@JsExport` declarations in `jsinterface/JsInterface.kt` — these are intentionally public JS API even though they happen to live in an `internal` package. +- Declarations that already have the Kotlin `internal` visibility modifier — the compiler already prevents access, so no annotation is needed (`private` and `internal` Kotlin modifiers should be preferred if they're possible). + +**How library modules opt in:** All modules in this repo are inside the internal-API boundary. Rather than adding `@file:OptIn` to every file, each module's `build.gradle.kts` opts in at the module level: + +```kotlin +compilerOptions { + freeCompilerArgs.add("-opt-in=com.apadmi.mockzilla.lib.InternalMockzillaApi") +} +``` + +**For `expect`/`actual` declarations:** The annotation must be present on both the `expect` declaration and **every** `actual` declaration across all platforms. Missing one platform will cause a compile error. + ## Spotless We use Spotless to reformat and organise all of our library code. It runs automatically on compilation so please ensure you've compiled your code before submitting a pull request. diff --git a/build-logic/src/main/kotlin/com/apadmi/mockzilla/MobileUiConfig.kt b/build-logic/src/main/kotlin/com/apadmi/mockzilla/MobileUiConfig.kt index 602b225aa..f2df33c24 100644 --- a/build-logic/src/main/kotlin/com/apadmi/mockzilla/MobileUiConfig.kt +++ b/build-logic/src/main/kotlin/com/apadmi/mockzilla/MobileUiConfig.kt @@ -3,7 +3,7 @@ package com.apadmi.mockzilla import org.gradle.api.Project object MobileUiConfig { - const val coreVersionForManagementUi = "3.0.0-alpha2" + const val coreVersionForManagementUi = "to be updated during release" } fun Project.isMobileUiDeployBuild() = properties["is_building_for_deployment"].toString().toBoolean() diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..19852566d --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +.venv +homepage/dist-ssr \ No newline at end of file diff --git a/docs/docs/browser_stack.md b/docs/docs/browser_stack.md index f48632ca9..710c78782 100644 --- a/docs/docs/browser_stack.md +++ b/docs/docs/browser_stack.md @@ -12,7 +12,6 @@ Browserstack seems to proxy local traffic by default. In your client app you'll ```kotlin OkHttpClient.Builder() .proxy(Proxy.NO_PROXY) - .protocols(listOf(Protocol.HTTP_1_1)).build() ``` ### Ktor Example: diff --git a/docs/docs/documentation.md b/docs/docs/documentation.md index 5bee95154..c59149082 100644 --- a/docs/docs/documentation.md +++ b/docs/docs/documentation.md @@ -1,17 +1,17 @@ -This documentation is primarily built using [MkDocs](https://www.mkdocs.org/) -with the [Material theme](https://squidfunk.github.io/mkdocs-material/). +This documentation is built using [Zensical](https://zensical.org/), a modern static site +generator by the team behind Material for MkDocs. -**Their documentation is brilliant so please check their docs if this is not sufficient.** +**Their documentation is great so check it if this is not sufficient.** ## Working on the HomePage -The homepage is a separate React site which is included in the MkDocs site. +The homepage is a separate React site which is included in the Zensical site. -In your IDE of choice open `docs/homepage` and treat it as regular standalone react site. +In your IDE of choice open `docs/homepage` and treat it as a regular standalone React site. Install dependencies with `npm install` and run it with `npm run dev`. -Note: Run `npm run build` to get your updates to the homepage reflected in the mkdocs site locally. +Note: Run `npm run build:fragment` (or `./serve.sh`) to get your updates to the homepage reflected in the docs site locally. ## Working on the rest of the documentation @@ -31,7 +31,11 @@ Tested on python `v{{get_python_version()}}` ```bash # Install all dependencies +python3 -m venv .venv +source .venv/bin/activate + pip install -r requirements.txt +cd homepage && npm install ``` Run the following to start the server. @@ -40,13 +44,13 @@ This supports hot reloading so updating the docs should automatically reload the docs in your browser. ```bash -mkdocs serve +./serve.sh ``` ## Macros -The docs also uses the [mkdocs-macros](https://mkdocs-macros-plugin.readthedocs.io/en/latest/) plugin. -This lets us call out to python code (and a load of other features) from within markdown. +The docs use Zensical's built-in macro support, which is compatible with the `mkdocs-macros-plugin` +API. This lets us call out to Python code from within Markdown. See the `main.py` file which includes some useful macros. diff --git a/docs/docs/endpoints.md b/docs/docs/endpoints.md index 95e1a3d0c..e548f5b30 100644 --- a/docs/docs/endpoints.md +++ b/docs/docs/endpoints.md @@ -1,3 +1,7 @@ +--- +description: Learn how to configure Mockzilla endpoints — define custom handlers for your mock HTTP server. +--- + # Configuring Endpoints ## Simple Example diff --git a/docs/docs/index.md b/docs/docs/index.md index 487ccfec7..0849dd3bc 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -1,4 +1,5 @@ --- -title: Home +title: Mockzilla template: home.html +description: A compile safe solution for running and configuring a local HTTP server for your mobile apps. Supports Android, iOS, Kotlin Multiplatform and Flutter. --- diff --git a/docs/docs/overrides/home.html b/docs/docs/overrides/home.html index d7b0564c4..e656c186e 100644 --- a/docs/docs/overrides/home.html +++ b/docs/docs/overrides/home.html @@ -1,18 +1 @@ -{% extends "main.html" %} {% block header %} {{ super() }} {% endblock %} {% block styles %} {{ super() }} - -{% endblock %} {% block footer %} {% endblock %} {% block tabs %} {% block content %} - -{% endblock %} {% endblock %} +{% include "homepage-content.html" %} diff --git a/docs/docs/overrides/main.html b/docs/docs/overrides/main.html index e6744171a..5cb6467e6 100644 --- a/docs/docs/overrides/main.html +++ b/docs/docs/overrides/main.html @@ -1,44 +1,2 @@ -{% extends "base.html" %} {% block scripts %} {{ super() }} - -{% endblock %} diff --git a/docs/docs/quick-start.md b/docs/docs/quick-start.md index 2d6a3bf87..5d26cef19 100644 --- a/docs/docs/quick-start.md +++ b/docs/docs/quick-start.md @@ -1,3 +1,7 @@ +--- +description: Get started with Mockzilla in minutes. Add the dependency, configure your endpoints, and start a local mock HTTP server for your mobile app. +--- + # Quick Start !!! important Mockzilla does not support HTTPS, all traffic is cleartext HTTP. diff --git a/docs/homepage/generate-fragment.mjs b/docs/homepage/generate-fragment.mjs new file mode 100644 index 000000000..849fd2b60 --- /dev/null +++ b/docs/homepage/generate-fragment.mjs @@ -0,0 +1,112 @@ +import { build } from 'vite' +import { readFileSync, writeFileSync, readdirSync } from 'fs' +import { resolve, dirname } from 'path' +import { fileURLToPath, pathToFileURL } from 'url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const outputPath = resolve(__dirname, '../docs/overrides/homepage-content.html') + +// Read version from gradle; VITE_VERSION_NAME env var takes precedence (set by CI via Fastlane). +const gradleText = readFileSync(resolve(__dirname, '../../mockzilla/build.gradle.kts'), 'utf-8') +const versionMatch = gradleText.match(/"(.*?)" \/\/ x-release-please-version/) +process.env.VITE_VERSION_NAME ||= versionMatch ? versionMatch[1] : 'Dev' + +// Collapses whitespace and strips single-line comments — sufficient for small inline scripts. +const minify = src => src.replace(/\/\/[^\n]*/g, '').replace(/\s+/g, ' ').trim() + +// Reads Material's /.__palette key before first paint to prevent FOUC. +// Homepage and docs share this single key so their dark-mode states are always in sync. +const themeInitScript = minify(` + try { + var p = localStorage.getItem('/.__palette'); + var dark = p + ? JSON.parse(p).index === 1 + : window.matchMedia('(prefers-color-scheme:dark)').matches; + if (dark) document.documentElement.classList.add('dark'); + } catch (e) {} +`) + +// Wires up the toggle button and writes Material's /.__palette key on click. +const toggleScript = minify(` + (function () { + var b = document.getElementById('theme-toggle'); + if (!b) return; + b.addEventListener('click', function () { + var dark = document.documentElement.classList.toggle('dark'); + try { + localStorage.setItem('/.__palette', JSON.stringify({ index: dark ? 1 : 0 })); + } catch (e) {} + }); + })() +`) + +// Step 1: Browser build — uses vite.config.mjs (SWC + Tailwind) to compile CSS. +console.log('Building assets...') +await build({ logLevel: 'warn' }) + +// Step 2: SSR build — uses @vitejs/plugin-react (standard Babel, not SWC) because the SWC +// plugin doesn't reliably produce Node.js-runnable output. react-syntax-highlighter is bundled +// inline (noExternal) to avoid CJS/ESM interop errors when it's externalized. +console.log('Building SSR bundle...') +const { default: reactPlugin } = await import('@vitejs/plugin-react') +await build({ + configFile: false, + plugins: [reactPlugin()], + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], + alias: { '@': resolve(__dirname, './src') }, + }, + logLevel: 'warn', + ssr: { noExternal: ['react-syntax-highlighter'] }, + build: { + ssr: 'src/ssr-entry.tsx', + outDir: 'dist-ssr', + rollupOptions: { output: { format: 'esm' } }, + }, +}) + +// Step 3: Run the SSR bundle to get pre-rendered HTML. +// The cache-bust query param (?t=...) forces Node to re-import on repeated runs. +console.log('Rendering HTML...') +const ssrEntryPath = resolve(__dirname, 'dist-ssr/ssr-entry.js') +const { render } = await import(`${pathToFileURL(ssrEntryPath).href}?t=${Date.now()}`) +const bodyHtml = render() + +// Step 4: Inline the compiled CSS. +const assetsDir = resolve(__dirname, 'build/homepage-assets') +const css = readdirSync(assetsDir) + .filter(f => f.endsWith('.css')) + .map(f => readFileSync(resolve(assetsDir, f), 'utf-8')) + .join('\n') + +// Step 5: Write the complete standalone HTML document. +const title = 'Mockzilla — Build API mocks with ease' +const desc = 'A compile-safe solution for running and configuring a local HTTP server for your mobile apps. Supports Android, iOS, Kotlin Multiplatform and Flutter.' +const ogUrl = 'https://mockzilla.apadmi.dev/' + +const fullDocument = ` + + + + +${title} + + + + + + + + + + + +${bodyHtml} + + +` + +writeFileSync(outputPath, fullDocument) +console.log(`✓ Standalone HTML document written to ${outputPath}`) diff --git a/docs/homepage/package-lock.json b/docs/homepage/package-lock.json index c2deb3321..ab3efef41 100644 --- a/docs/homepage/package-lock.json +++ b/docs/homepage/package-lock.json @@ -1187,9 +1187,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1204,9 +1201,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1221,9 +1215,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1238,9 +1229,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1255,9 +1243,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1272,9 +1257,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1289,9 +1271,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1306,9 +1285,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1323,9 +1299,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1340,9 +1313,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1357,9 +1327,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1534,9 +1501,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1554,9 +1518,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1574,9 +1535,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1594,9 +1552,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1812,9 +1767,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1832,9 +1784,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1852,9 +1801,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1872,9 +1818,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3644,9 +3587,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3668,9 +3608,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3692,9 +3629,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3716,9 +3650,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/docs/homepage/package.json b/docs/homepage/package.json index 16854a8eb..82377eb93 100644 --- a/docs/homepage/package.json +++ b/docs/homepage/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "export VITE_VERSION_NAME=Debug; vite", "build": "tsc -b && vite build", + "build:fragment": "node generate-fragment.mjs", "lint": "eslint .", "preview": "vite preview" }, diff --git a/docs/homepage/src/App.tsx b/docs/homepage/src/App.tsx index 0e5e39fc9..8dc3f3508 100644 --- a/docs/homepage/src/App.tsx +++ b/docs/homepage/src/App.tsx @@ -1,15 +1,14 @@ import { Features } from "./components/Features"; import { Footer } from "./components/Footer"; +import { Header } from "./components/Header"; import { Hero } from "./components/Hero"; import { PlatformBanner } from "./components/PlatformBanner"; import { PlatformSupport } from "./components/PlatformSupport"; -import { useTheme } from "./hooks/useTheme"; export default function App() { - useTheme(); - return (
+
diff --git a/docs/homepage/src/components/Footer.tsx b/docs/homepage/src/components/Footer.tsx index 31362072a..504a039ab 100644 --- a/docs/homepage/src/components/Footer.tsx +++ b/docs/homepage/src/components/Footer.tsx @@ -1,6 +1,8 @@ -import { Button } from "./ui/button"; +import { buttonVariants } from "./ui/button"; +import { cn } from "./ui/utils"; import { GithubIcon } from "./ui/icons"; -import logo from "../assets/logo.svg"; + +const logo = "/img/icon.svg"; export function Footer() { return ( @@ -22,7 +24,6 @@ export function Footer() {
diff --git a/docs/homepage/src/components/Header.tsx b/docs/homepage/src/components/Header.tsx new file mode 100644 index 000000000..1579d5e4d --- /dev/null +++ b/docs/homepage/src/components/Header.tsx @@ -0,0 +1,69 @@ +import { GithubIcon } from "./ui/icons"; + +const logo = "/img/icon.svg"; + +export function Header() { + return ( +
+
+
+ + Mockzilla + Mockzilla + + +
+
+
+ ); +} diff --git a/docs/homepage/src/components/Hero.tsx b/docs/homepage/src/components/Hero.tsx index 6ba8470d4..d4db7ff05 100644 --- a/docs/homepage/src/components/Hero.tsx +++ b/docs/homepage/src/components/Hero.tsx @@ -1,43 +1,18 @@ -import { MockzillaLogoDark, MockzillaLogoLight } from "./ui/icons"; -import { - atomOneDark, - colorBrewer, -} from "react-syntax-highlighter/dist/cjs/styles/hljs"; -import { useEffect, useState } from "react"; +import { MockzillaLogoLight } from "./ui/icons"; +import { atomOneDark } from "react-syntax-highlighter/dist/cjs/styles/hljs"; import { ArrowRight } from "lucide-react"; import { Badge } from "./ui/badge"; -import { Button } from "./ui/button"; +import { buttonVariants } from "./ui/button"; +import { cn } from "./ui/utils"; import SyntaxHighlighter from "react-syntax-highlighter"; export function Hero(props: any) { - const [isDark, setIsDark] = useState(false); - - useEffect(() => { - // Check initial theme - const checkTheme = () => { - setIsDark(document.documentElement.classList.contains("dark")); - }; - - checkTheme(); - - // Watch for theme changes - const observer = new MutationObserver(checkTheme); - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ["class"], - }); - - return () => observer.disconnect(); - }, []); - return (
@@ -60,9 +35,7 @@ export function Hero(props: any) { paddingLeft: "0", paddingRight: "0", }} - style={ - isDark ? atomOneDark : colorBrewer - }>{`// Configure Server + style={atomOneDark}>{`// Configure Server val endpoint = EndpointConfiguration .Builder("GET - Customer") .setDefaultHandler { @@ -75,7 +48,7 @@ val endpoint = EndpointConfiguration val config = MockzillaConfig.Builder() .addEndpoint(endpoint) - + // Start Server startMockzilla(config)`} @@ -101,25 +74,23 @@ startMockzilla(config)`}
- - +
diff --git a/docs/homepage/src/components/ui/button.tsx b/docs/homepage/src/components/ui/button.tsx index 78c536dfb..2e0668639 100644 --- a/docs/homepage/src/components/ui/button.tsx +++ b/docs/homepage/src/components/ui/button.tsx @@ -54,4 +54,4 @@ function Button({ ); } -export { Button }; +export { Button, buttonVariants }; diff --git a/docs/homepage/src/components/ui/icons.tsx b/docs/homepage/src/components/ui/icons.tsx index 4a1e66cd7..3588abb3a 100644 --- a/docs/homepage/src/components/ui/icons.tsx +++ b/docs/homepage/src/components/ui/icons.tsx @@ -134,5 +134,4 @@ export const FlutterIcon = (props: SVGProps) => ( ); -export const MockzillaLogoDark = `url('data:image/svg+xml,<%3Fxml version="1.0" encoding="UTF-8" standalone="no"%3F>')`; export const MockzillaLogoLight = `url('data:image/svg+xml,<%3Fxml version="1.0" encoding="UTF-8" standalone="no"%3F>')`; diff --git a/docs/homepage/src/ssr-entry.tsx b/docs/homepage/src/ssr-entry.tsx new file mode 100644 index 000000000..31595d1a0 --- /dev/null +++ b/docs/homepage/src/ssr-entry.tsx @@ -0,0 +1,6 @@ +import { renderToStaticMarkup } from 'react-dom/server' +import App from './App' + +export function render(): string { + return renderToStaticMarkup() +} diff --git a/docs/homepage/src/styles/globals.css b/docs/homepage/src/styles/globals.css index 62d0f41c0..186a69d08 100644 --- a/docs/homepage/src/styles/globals.css +++ b/docs/homepage/src/styles/globals.css @@ -19,7 +19,7 @@ --secondary-foreground: #262626; --muted: #f5f5f5; --muted-foreground: #737373; - --accent: #4eced8; + --accent: #3ea6af; --accent-foreground: #262626; --success: #4ed887; --success-foreground: #0d0d0d; @@ -258,3 +258,9 @@ html { outline-offset: 2px; } } + +/* Theme toggle icon visibility — controlled by .dark class on */ +#icon-sun { display: none; } +#icon-moon { display: block; } +.dark #icon-sun { display: block; } +.dark #icon-moon { display: none; } diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml deleted file mode 100644 index f64853152..000000000 --- a/docs/mkdocs.yml +++ /dev/null @@ -1,73 +0,0 @@ -site_name: Mockzilla -site_url: https://mockzilla.apadmi.dev/ -repo_url: https://github.com/Apadmi-Engineering/Mockzilla -copyright: Copyright © 2025 Apadmi Ltd -theme: - logo: "img/icon.svg" - favicon: "img/favicon.ico" - custom_dir: docs/overrides - palette: - # Palette toggle for light mode - - media: "(prefers-color-scheme: light)" - scheme: default - toggle: - icon: material/brightness-7 - name: Switch to dark mode - primary: custom - accent: custom - - # Palette toggle for dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate - toggle: - icon: material/brightness-4 - name: Switch to light mode - primary: custom - accent: custom - name: material - features: - - navigation.tabs - - navigation.sections - - navigation.top -plugins: - - search - - macros -extra_css: - - stylesheets/extra.css -nav: - - index.md - - Docs: - - quick-start.md - - endpoints.md - - Advanced: - - additional_config.md - - browser_stack.md - - snapshots.md - - Configuration at Runtime: - - desktop/overview.md - - mobile_ui.md - - presets.md - - Contributing: - - contributing.md - - Desktop App: desktop_contributing.md - - Documentation: documentation.md - - Flutter: flutter_contributing.md - - Api Reference: - ./dokka/index.html -markdown_extensions: - - pymdownx.arithmatex: - generic: true - - admonition - - pymdownx.details - - pymdownx.superfences - - pymdownx.tabbed: - alternate_style: true - - pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format -extra_javascript: - - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js -hooks: - - custom_hooks.py \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 96d706e0e..fe4ebb73d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1 @@ -mkdocs-material==9.6.20 -mkdocs-macros-plugin==1.0.5 \ No newline at end of file +zensical==0.0.43 \ No newline at end of file diff --git a/docs/serve.sh b/docs/serve.sh new file mode 100755 index 000000000..60b406976 --- /dev/null +++ b/docs/serve.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +cd "$SCRIPT_DIR/homepage" && npm run build:fragment + +cd "$SCRIPT_DIR" +if [[ -x "$SCRIPT_DIR/.venv/bin/zensical" ]]; then + "$SCRIPT_DIR/.venv/bin/zensical" serve + else + zensical serve + fi \ No newline at end of file diff --git a/docs/zensical.toml b/docs/zensical.toml new file mode 100644 index 000000000..bf72cbdaf --- /dev/null +++ b/docs/zensical.toml @@ -0,0 +1,77 @@ +[project] +site_name = "Mockzilla" +site_url = "https://mockzilla.apadmi.dev/" +repo_url = "https://github.com/Apadmi-Engineering/Mockzilla" +copyright = "Copyright © 2026 Apadmi Ltd" +site_description = "A compile-safe solution for running and configuring a local HTTP server for your mobile apps. Supports Android, iOS, Kotlin Multiplatform and Flutter." +extra_css = ["stylesheets/extra.css"] +extra_javascript = ["https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"] + +nav = [ + "index.md", + { Docs = [ + "quick-start.md", + "endpoints.md", + { Advanced = [ + "additional_config.md", + "browser_stack.md", + "snapshots.md", + ]}, + { "Configuration at Runtime" = [ + "desktop/overview.md", + "mobile_ui.md", + "presets.md", + ]}, + ]}, + { Contributing = [ + "contributing.md", + { "Desktop App" = "desktop_contributing.md" }, + { Documentation = "documentation.md" }, + { Flutter = "flutter_contributing.md" }, + ]}, + { "Api Reference" = "./dokka/index.html" }, +] + +[project.theme] +logo = "img/icon.svg" +favicon = "img/favicon.ico" +custom_dir = "docs/overrides" +features = [ + "navigation.tabs", + "navigation.sections", + "navigation.top", +] + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +primary = "custom" +accent = "custom" +toggle.icon = "material/brightness-7" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +primary = "custom" +accent = "custom" +toggle.icon = "material/brightness-4" +toggle.name = "Switch to light mode" + +[project.markdown_extensions.pymdownx.arithmatex] +generic = true + +[project.markdown_extensions.admonition] + +[project.markdown_extensions.pymdownx.details] + +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true + +[project.markdown_extensions.pymdownx.superfences] +custom_fences = [ + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" } +] + +[project.markdown_extensions.zensical.extensions.macros] +module_name = "main" diff --git a/fastlane/fastfiles/docs.rb b/fastlane/fastfiles/docs.rb index be3eb74b6..4c7497a10 100644 --- a/fastlane/fastfiles/docs.rb +++ b/fastlane/fastfiles/docs.rb @@ -1,6 +1,4 @@ lane :generate_docs do - output_dir = "#{lane_context[:repo_root]}/generated_docs" - # Build the page to redirect to the desktop app download site sh("cd #{lane_context[:repo_root]}/docs; python -c 'import main; main.update_download_file()'") @@ -9,7 +7,7 @@ cd #{lane_context[:repo_root]}/docs/homepage; npm i; export VITE_VERSION_NAME=#{get_core_mockzilla_version_name}; - npm run build; + npm run build:fragment; "); # Generate Kotlin documentation @@ -20,6 +18,6 @@ } ) - # Build mkdocs - sh("cd #{lane_context[:repo_root]}/docs; mkdocs build -d #{output_dir}") + # Build docs + sh("cd #{lane_context[:repo_root]}/docs; zensical build") end diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4e671f16..a4b603bb3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ buildKonfig = "0.20.0" # https://github.com/yshrsmz/BuildKonfig dokka = "2.2.0" # https://github.com/Kotlin/dokka # Localization -lyricist = "1.8.0" # https://github.com/adrielcafe/lyricist +lyricist = "1.7.0" # https://github.com/adrielcafe/lyricist # Networking ktor = "3.4.3" # https://github.com/ktorio/ktor @@ -93,6 +93,7 @@ semver = { module = "io.github.z4kn4fein:semver", version.ref = "semver" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" } diff --git a/mockzilla-common/build.gradle.kts b/mockzilla-common/build.gradle.kts index 732afe3ba..c310208b3 100644 --- a/mockzilla-common/build.gradle.kts +++ b/mockzilla-common/build.gradle.kts @@ -79,6 +79,7 @@ kotlin { } compilerOptions { freeCompilerArgs.addAll(CompilerConfig.freeCompilerArgs) + freeCompilerArgs.add("-opt-in=com.apadmi.mockzilla.lib.InternalMockzillaApi") } } diff --git a/mockzilla-common/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt b/mockzilla-common/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.android.kt similarity index 91% rename from mockzilla-common/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt rename to mockzilla-common/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.android.kt index e415c8da8..3dc6db273 100644 --- a/mockzilla-common/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt +++ b/mockzilla-common/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.android.kt @@ -1,11 +1,15 @@ +@file:JvmName("FileIoKt") + package com.apadmi.mockzilla.lib.internal.utils import android.annotation.TargetApi import android.os.Build +import com.apadmi.mockzilla.lib.InternalMockzillaApi import java.io.File import java.io.IOException import java.nio.file.Files +@InternalMockzillaApi actual class FileIo(private val cacheDir: File) { private val cacheDirectory get() = File( @@ -44,5 +48,6 @@ actual class FileIo(private val cacheDir: File) { } } +@InternalMockzillaApi @TargetApi(Build.VERSION_CODES.O) actual fun createFileIoforTesting() = FileIo(Files.createTempDirectory("").toFile()) diff --git a/mockzilla-common/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.kt b/mockzilla-common/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.android.kt similarity index 66% rename from mockzilla-common/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.kt rename to mockzilla-common/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.android.kt index 745f795c5..14a30ea71 100644 --- a/mockzilla-common/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.kt +++ b/mockzilla-common/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.android.kt @@ -1,6 +1,10 @@ +@file:JvmName("ServerUtilsKt") + package com.apadmi.mockzilla.lib.internal.utils +import com.apadmi.mockzilla.lib.InternalMockzillaApi import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +@InternalMockzillaApi actual val Dispatchers.multiPlatformIo: CoroutineDispatcher get() = Dispatchers.IO diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/InternalMockzillaApi.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/InternalMockzillaApi.kt similarity index 52% rename from mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/InternalMockzillaApi.kt rename to mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/InternalMockzillaApi.kt index 04f417bc0..f0ce02080 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/InternalMockzillaApi.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/InternalMockzillaApi.kt @@ -1,8 +1,11 @@ -package com.apadmi.mockzilla.lib.internal.utils +package com.apadmi.mockzilla.lib /** - * API marked with this annotation is internal, and it is not intended to be used outside Mockzilla. - * It could be modified or removed without any notice. Please do not use it. + * API marked with this annotation is internal to Mockzilla and is not intended to be used outside + * the library. It could be modified or removed without any notice. Please do not use it. + * + * Library modules opt in at the module level via `freeCompilerArgs` in their `build.gradle.kts`. + * See `CONTRIBUTING.md` for guidance on when and how to apply this annotation. */ @RequiresOptIn( level = RequiresOptIn.Level.ERROR, @@ -16,7 +19,8 @@ package com.apadmi.mockzilla.lib.internal.utils AnnotationTarget.FIELD, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.PROPERTY_SETTER, - AnnotationTarget.PROPERTY_SETTER + AnnotationTarget.PROPERTY_GETTER ) @Retention(AnnotationRetention.BINARY) +@MustBeDocumented annotation class InternalMockzillaApi diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/config/ZeroConfConfig.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/config/ZeroConfConfig.kt index 52e425563..7708a73b2 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/config/ZeroConfConfig.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/config/ZeroConfConfig.kt @@ -1,8 +1,17 @@ package com.apadmi.mockzilla.lib.config +/** + * Constants for Mockzilla's ZeroConf (Bonjour/DNS-SD) service discovery integration. Used by + * the server to advertise itself and by the management UI to locate devices on the network. + */ object ZeroConfConfig { + /** + * The ZeroConf service type Mockzilla registers under. + */ const val serviceType = "_mockzilla._tcp" - // Limit defined here: https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.1 + /** + * Maximum byte length for a ZeroConf service name, as defined by RFC 1035 section 2.3.1. + */ const val serviceNameByteLimit = 63 } diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/models/ClearCachesRequestDto.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/models/ClearCachesRequestDto.kt index c435279b9..b464a9d09 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/models/ClearCachesRequestDto.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/models/ClearCachesRequestDto.kt @@ -1,11 +1,13 @@ package com.apadmi.mockzilla.lib.internal.models +import com.apadmi.mockzilla.lib.InternalMockzillaApi import com.apadmi.mockzilla.lib.models.EndpointConfiguration import kotlinx.serialization.Serializable /** * @property keys */ +@InternalMockzillaApi @Serializable data class ClearCachesRequestDto( val keys: List diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/models/SerializableEndpointConfig.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/models/SerializableEndpointConfig.kt index f1c3e0c86..71c32574a 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/models/SerializableEndpointConfig.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/models/SerializableEndpointConfig.kt @@ -2,6 +2,7 @@ package com.apadmi.mockzilla.lib.internal.models +import com.apadmi.mockzilla.lib.InternalMockzillaApi import com.apadmi.mockzilla.lib.internal.utils.HttpStatusCodeSerializer import com.apadmi.mockzilla.lib.models.DashboardOverridePreset import com.apadmi.mockzilla.lib.models.EndpointConfiguration @@ -91,6 +92,7 @@ data class SerializableEndpointConfig( * @property errorHeaders * @property appliedPresetOverride */ +@InternalMockzillaApi @Suppress("TYPE_ALIAS") @Serializable data class SerializableEndpointPatchItemDto( @@ -137,6 +139,7 @@ data class SerializableEndpointPatchItemDto( /** * @property entries */ +@InternalMockzillaApi @Serializable data class MockDataResponseDto( val entries: List @@ -145,6 +148,7 @@ data class MockDataResponseDto( /** * @property entries */ +@InternalMockzillaApi @Serializable data class SerializableEndpointConfigPatchRequestDto( val entries: List @@ -152,6 +156,7 @@ data class SerializableEndpointConfigPatchRequestDto( constructor(entry: SerializableEndpointPatchItemDto) : this(listOf(entry)) } +@InternalMockzillaApi @Serializable(with = ServiceResultSerializer::class) sealed class SetOrDont { @Serializable @@ -166,6 +171,7 @@ sealed class SetOrDont { data class Set(val value: T) : SetOrDont() } +@InternalMockzillaApi class ServiceResultSerializer( serializer: KSerializer ) : KSerializer> { diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt index 052669541..1725438be 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt @@ -1,5 +1,8 @@ package com.apadmi.mockzilla.lib.internal.utils +import com.apadmi.mockzilla.lib.InternalMockzillaApi + +@InternalMockzillaApi expect class FileIo { suspend fun readFromCache(filename: String): String? suspend fun saveToCache(filename: String, contents: String) @@ -7,4 +10,5 @@ expect class FileIo { suspend fun deleteAllCaches() } +@InternalMockzillaApi expect fun createFileIoforTesting(): FileIo diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/JsonProvider.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/JsonProvider.kt index 45243211e..7364b3d40 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/JsonProvider.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/JsonProvider.kt @@ -1,7 +1,9 @@ package com.apadmi.mockzilla.lib.internal.utils +import com.apadmi.mockzilla.lib.InternalMockzillaApi import kotlinx.serialization.json.Json +@InternalMockzillaApi object JsonProvider { val json = Json { ignoreUnknownKeys = true diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.kt index 9579e15cc..7247a0f67 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.kt @@ -1,6 +1,8 @@ package com.apadmi.mockzilla.lib.internal.utils +import com.apadmi.mockzilla.lib.InternalMockzillaApi import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +@InternalMockzillaApi expect val Dispatchers.multiPlatformIo: CoroutineDispatcher diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/models/EndpointConfiguration.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/models/EndpointConfiguration.kt index 133217c90..48fc53dfa 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/models/EndpointConfiguration.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/models/EndpointConfiguration.kt @@ -10,15 +10,27 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames /** - * @property name - * @property key - * @property shouldFail - * @property delay - * @property endpointMatcher - * @property versionCode - * @property defaultHandler - * @property errorHandler - * @property dashboardOptionsConfig + * Configures a single mock endpoint within a Mockzilla server. Defines how incoming requests are + * matched to this endpoint and what response to return, along with optional dashboard presets and + * latency simulation. + * + * Construct via [Builder]. + * + * @property name Human-readable display name shown in the management dashboard. + * @property key Unique identifier for this endpoint, used in management API operations. + * @property shouldFail Whether this endpoint returns an error response by default. When `true`, + * [errorHandler] is called instead of [defaultHandler]. + * @property delay Artificial response delay in milliseconds. `null` falls back to the global delay + * set on [MockzillaConfig.Builder]. + * @property dashboardOptionsConfig Preset responses available to users in the management dashboard. + * @property versionCode Version number for this endpoint's configuration. Incrementing this value + * automatically invalidates any cached responses on connected devices. + * @property endpointMatcher Predicate that determines whether an incoming request should be routed + * to this endpoint. The first matching endpoint wins. + * @property defaultHandler Called when a request matches this endpoint and [shouldFail] is `false`. + * Returns the mock response to send back to the caller. + * @property errorHandler Called when a request matches this endpoint and [shouldFail] is `true`. + * Returns the simulated error response to send back to the caller. */ data class EndpointConfiguration( val name: String, @@ -32,7 +44,10 @@ data class EndpointConfiguration( val errorHandler: suspend MockzillaHttpRequest.() -> MockzillaHttpResponse, ) { /** - * @property raw + * Unique serializable identifier for an [EndpointConfiguration]. Used to reference endpoints + * in management API operations such as cache clearing or runtime overrides. + * + * @property raw The raw string value of the key. */ @Serializable @JvmInline @@ -59,7 +74,7 @@ data class EndpointConfiguration( /** * Sets the human readable name of the endpoint (defaults to the value of the `key`) * - * @param name + * @param name The human-readable display name for this endpoint. */ fun setName(name: String) = apply { config = config.copy(name = name) @@ -98,17 +113,17 @@ data class EndpointConfiguration( * [setShouldFail] causes Mockzilla to generate a failure response, then this block * will *not* be called, instead the block specified by [setErrorHandler] is called. * - * @param handler + * @param handler Lambda invoked with the incoming request that returns the mock response. */ fun setDefaultHandler(handler: suspend MockzillaHttpRequest.() -> MockzillaHttpResponse) = apply { config = config.copy(defaultHandler = handler) } /** - * The block called when a network request is made to this endpoint but Mockzilladecides to + * The block called when a network request is made to this endpoint but Mockzilla decides to * simulate a server failure. * - * @param handler + * @param handler Lambda invoked with the incoming request that returns the simulated error response. */ fun setErrorHandler(handler: suspend MockzillaHttpRequest.() -> MockzillaHttpResponse) = apply { config = config.copy(errorHandler = handler) @@ -117,8 +132,8 @@ data class EndpointConfiguration( /** * Configure the presets that are available to users of the dashboard. * - * @param action - * @return + * @param action Builder block for configuring dashboard presets. + * @return This builder, for chaining. */ fun configureDashboardOverrides( action: DashboardOptionsConfig.Builder.() -> DashboardOptionsConfig.Builder @@ -142,7 +157,7 @@ data class EndpointConfiguration( * * This is just a utility wrapper around the more flexible [setPatternMatcher] endpoint. * - * @param regex + * @param regex The regular expression to match against the full request URI. */ @Suppress("unused") fun setPattern(regex: String) = apply { @@ -170,9 +185,13 @@ data class EndpointConfiguration( } /** - * @property statusCode - * @property headers - * @property body + * An HTTP response returned by a mock endpoint handler. Returned from + * [EndpointConfiguration.Builder.setDefaultHandler] and [EndpointConfiguration.Builder.setErrorHandler] + * lambdas. + * + * @property statusCode The HTTP status code of the response. Defaults to `200 OK`. + * @property headers HTTP response headers. + * @property body The response body as a string. */ @Serializable data class MockzillaHttpResponse( @@ -185,9 +204,12 @@ data class MockzillaHttpResponse( } /** - * @property statusCode - * @property headers - * @property body + * A partial HTTP response used by dashboard presets, allowing a subset of response fields to + * be overridden. `null` fields are left unchanged from the endpoint's default response. + * + * @property statusCode The HTTP status code override, or `null` to leave unchanged. + * @property headers HTTP response headers override, or `null` to leave unchanged. + * @property body The response body override, or `null` to leave unchanged. */ @Serializable data class PartialMockzillaHttpResponse( @@ -227,6 +249,12 @@ interface MockzillaHttpRequest { } /** + * Configures the preset responses available to users in the Mockzilla management dashboard for a + * specific endpoint. Presets let dashboard users quickly switch between common response scenarios + * without modifying code. + * + * Construct via [Builder] and attach to an endpoint using + * [EndpointConfiguration.Builder.configureDashboardOverrides]. * @property errorPresets * @property successPresets */ @@ -242,10 +270,24 @@ data class DashboardOptionsConfig( @JsonNames("presets") val successPresets: List ) { + /** + * The list of preset responses available in the dashboard for this endpoint. + */ val presets get() = successPresets class Builder { private val presets = mutableListOf() + + /** + * Adds a preset response option to the dashboard for this endpoint. + * + * @param response The full response this preset applies when selected. + * @param name Display name for this preset in the dashboard. Defaults to "Preset N". + * @param description Optional description shown alongside the preset. + * @param type Visual classification for this preset. Defaults to a type inferred from the + * response status code when `null`. + * @return This builder, for chaining. + */ fun addPreset( response: MockzillaHttpResponse, name: String? = null, @@ -253,6 +295,17 @@ data class DashboardOptionsConfig( type: DashboardOverridePreset.Type? = null ) = addPreset(response.toPartial(), name, description, type) + /** + * Adds a partial preset response option to the dashboard for this endpoint. Only fields + * set on [response] are overridden; `null` fields retain the endpoint's default values. + * + * @param response The partial response this preset applies when selected. + * @param name Display name for this preset in the dashboard. Defaults to "Preset N". + * @param description Optional description shown alongside the preset. + * @param type Visual classification for this preset. Defaults to a type inferred from the + * response status code when `null`. + * @return This builder, for chaining. + */ fun addPreset( response: PartialMockzillaHttpResponse, name: String? = null, @@ -290,11 +343,17 @@ data class DashboardOptionsConfig( } /** - * @property name - * @property description - * @property type Overrides the type of the preset shown in UI, defaults to correspond with status code - * @property response - * @property isManagementUiDefinedCustomPreset + * A named response configuration that can be applied to an endpoint from the Mockzilla management + * dashboard, overriding the endpoint's default or error response for a session. + * + * @property name Display name shown in the dashboard preset list. + * @property description Optional description shown alongside the preset in the dashboard. + * @property type Visual classification for the preset in the dashboard. Defaults to a type + * inferred from the response status code when `null`. + * @property response The partial response this preset applies when selected. + * @property isManagementUiDefinedCustomPreset `true` when this preset was created interactively + * by a user in the management dashboard, as opposed to being defined in code via + * [EndpointConfiguration.Builder.configureDashboardOverrides]. */ @Serializable data class DashboardOverridePreset( @@ -304,6 +363,10 @@ data class DashboardOverridePreset( val response: PartialMockzillaHttpResponse, val isManagementUiDefinedCustomPreset: Boolean = false ) { + /** + * Visual classification for a [DashboardOverridePreset] in the management dashboard. Used to + * display presets with appropriate styling. + */ @Serializable enum class Type { ClientError, diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/models/MetaData.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/models/MetaData.kt index 65834d718..4cbdef87d 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/models/MetaData.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/models/MetaData.kt @@ -8,19 +8,20 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNames /** - * @property appName - * @property appPackage - * @property operatingSystemVersion - * @property deviceModel - * @property appVersion - * @property runTarget - * @property mockzillaVersion + * Device and application metadata collected when Mockzilla starts. Displayed in the management + * dashboard to identify the connected device, and used in ZeroConf service records. * - * Don't add non optional fields to this type since that will break backward compatibility - * - * Short alternative JsonNames used for encoding/decoding when ZeroConf is used to reduce payload size + * Don't add non-optional fields to this type since that will break backward compatibility * + * @property appName The name of the application. + * @property appPackage The application package name or bundle identifier. + * @property operatingSystemVersion The OS version string of the device. + * @property deviceModel The device model identifier. + * @property appVersion The application version string. + * @property runTarget The platform the server is running on, or `null` if unknown. + * @property mockzillaVersion The version of the Mockzilla library. */ +@OptIn(ExperimentalSerializationApi::class) @Serializable data class MetaData @OptIn(ExperimentalSerializationApi::class) constructor( @JsonNames("appName") @@ -51,14 +52,27 @@ data class MetaData @OptIn(ExperimentalSerializationApi::class) constructor( @SerialName("mzVer") val mockzillaVersion: String ) { + /** + * `true` if the server is running on an Android device or emulator. + */ val isAndroid = runTarget in listOf(RunTarget.AndroidEmulator, RunTarget.AndroidDevice) + /** + * Serialises this metadata to a [Map] for embedding in ZeroConf TXT records. + * + * @return A map of field names to string values. + */ fun toMap(): Map { val encoded = json.encodeToString(this) return json.decodeFromString>(encoded) } companion object { + /** + * Maximum length in characters for each metadata field. Fields collected from the platform + * (device model, OS version, etc.) are truncated to this limit to comply with ZeroConf + * DNS-SD payload constraints (RFC 1035). + */ const val maxFieldLength = 254 private val json = Json { isLenient = true @@ -66,6 +80,13 @@ data class MetaData @OptIn(ExperimentalSerializationApi::class) constructor( explicitNulls = false } + /** + * Deserialises a [MetaData] instance from a [Map] of field names to string values, as + * produced by [MetaData.toMap]. Intended for reconstructing metadata received via ZeroConf + * TXT records. + * + * @return The deserialised [MetaData]. + */ fun Map.parseMetaData(): MetaData { val encoded = json.encodeToString(this) return json.decodeFromString(encoded) @@ -73,6 +94,10 @@ data class MetaData @OptIn(ExperimentalSerializationApi::class) constructor( } } +/** + * Identifies the platform on which the Mockzilla server is running. Reported in [MetaData] and + * visible in the management dashboard. + */ enum class RunTarget { AndroidDevice, AndroidEmulator, diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/models/MockzillaConfig.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/models/MockzillaConfig.kt index 9c95bc691..a1b95a27d 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/models/MockzillaConfig.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/models/MockzillaConfig.kt @@ -7,14 +7,20 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds /** - * @property port - * @property endpoints - * @property logLevel - * @property isRelease - * @property releaseModeConfig - * @property localhostOnly - * @property additionalLogWriters - * @property isNetworkDiscoveryEnabled + * Top-level configuration for a Mockzilla server instance. All properties are set via + * [MockzillaConfig.Builder]. + * + * @property port The port the server binds to. `0` causes the OS to assign an available port. + * @property endpoints The mock endpoints registered on this server. + * @property isRelease When `true`, activates release mode: rate limiting, token authentication, + * and localhost-only restrictions are applied. See [ReleaseModeConfig] for details. + * @property localhostOnly When `true`, the server only accepts connections from `127.0.0.1`, + * blocking the management desktop interface and other external tools. + * @property logLevel Verbosity of Mockzilla's internal logging. + * @property releaseModeConfig Rate limiting and authentication config applied in release mode. + * @property isNetworkDiscoveryEnabled When `true`, Mockzilla broadcasts itself via ZeroConf + * (Bonjour) so the management desktop can discover it. Always disabled in release mode. + * @property additionalLogWriters Extra log sinks in addition to standard output. */ data class MockzillaConfig( val port: Int, @@ -26,6 +32,9 @@ data class MockzillaConfig( val isNetworkDiscoveryEnabled: Boolean, val additionalLogWriters: List ) { + /** + * Defines the verbosity of Mockzilla's internal logging. + */ enum class LogLevel { Assert, Debug, @@ -72,9 +81,9 @@ data class MockzillaConfig( /** * Sets the port which the server will bind to. Setting port to `0` will cause the server to - * choose it's port auto-magically. + * choose its port automatically. * - * @param port + * @param port Port number to bind to. Use `0` for automatic port assignment. */ fun setPort(port: Int): Builder = apply { this.port = port @@ -129,7 +138,7 @@ data class MockzillaConfig( /** * Enable or disable release mode. See [setReleaseModeConfig] for more details * - * @param isRelease + * @param isRelease `true` to enable release mode, `false` to disable. */ fun setIsReleaseModeEnabled(isRelease: Boolean) = apply { this.isRelease = isRelease @@ -139,7 +148,7 @@ data class MockzillaConfig( * Setting this value to `true` means the mockzilla server will only accept calls from localhost. * Calls from other IPs will be blocked (including blocking the Mockzilla desktop interface) * - * @param localhostOnly + * @param localhostOnly `true` to restrict connections to localhost only. */ fun setLocalhostOnly(localhostOnly: Boolean) = apply { this.localhostOnly = localhostOnly @@ -160,15 +169,15 @@ data class MockzillaConfig( /** * Register an new endpoint configuration * - * @param endpoint + * @param endpoint The endpoint builder to register. */ fun addEndpoint(endpoint: EndpointConfiguration.Builder) = addEndpoint(endpoint.build()) /** * Register an new endpoint configuration * - * @param endpoint - * @return + * @param endpoint The endpoint configuration to register. + * @return This builder, for chaining. */ fun addEndpoint(endpoint: EndpointConfiguration) = apply { endpoints.add(endpoint) @@ -179,8 +188,8 @@ data class MockzillaConfig( * * Mockzilla logs will then log to standard output and to any additional log writers * - * @param logWriter - * @return + * @param logWriter The log writer to register. + * @return This builder, for chaining. */ fun addLogWriter(logWriter: MockzillaLogWriter) = apply { additionalLogWriters += logWriter @@ -197,7 +206,7 @@ data class MockzillaConfig( /** * Completes the builder pattern, returning an immutable config. * - * @return + * @return The fully constructed [MockzillaConfig]. */ fun build() = MockzillaConfig(port, endpoints.map { it.copy( @@ -212,13 +221,18 @@ data class MockzillaConfig( } /** - * @property config - * @property mockBaseUrl - * @property apiBaseUrl - * @property port - * @property authHeaderProvider - * @property mockzillaVersion - * @property ip + * Runtime details of a started Mockzilla server, returned by `startMockzilla`. Use [mockBaseUrl] + * as the base URL in the app under test's HTTP client to route requests through the mock server. + * + * @property config The configuration the server was started with. + * @property ip The IP address the server is listening on. + * @property mockBaseUrl Base URL for mock endpoint requests. Configure the app under test's HTTP + * client to use this URL. + * @property apiBaseUrl Base URL for the Mockzilla control API. + * @property port The port the server is bound to. + * @property authHeaderProvider Provides authentication headers for making requests to this server + * instance. + * @property mockzillaVersion The version of the Mockzilla library. */ data class MockzillaRuntimeParams( val config: MockzillaConfig, diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/service/AuthHeaderProvider.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/service/AuthHeaderProvider.kt index e4fae77f4..e61a0af24 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/service/AuthHeaderProvider.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/service/AuthHeaderProvider.kt @@ -1,10 +1,23 @@ package com.apadmi.mockzilla.lib.service +/** + * Generates the authentication header required to make requests to a running Mockzilla server. + * An instance pre-configured for the running server is available via + * [com.apadmi.mockzilla.lib.models.MockzillaRuntimeParams.authHeaderProvider]. + */ interface AuthHeaderProvider { + /** + * Generates a fresh authentication header. Each invocation may produce a new token value. + * + * @return The header key and value to include in requests to the Mockzilla server. + */ suspend fun generateHeader(): Header + /** - * @property key - * @property value + * An HTTP header represented as a key-value pair. + * + * @property key The header field name. + * @property value The header field value. */ data class Header(val key: String, val value: String) } diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/service/MockzillaLogWriter.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/service/MockzillaLogWriter.kt index 1008138fa..a47a356ac 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/service/MockzillaLogWriter.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/service/MockzillaLogWriter.kt @@ -2,7 +2,19 @@ package com.apadmi.mockzilla.lib.service import com.apadmi.mockzilla.lib.models.MockzillaConfig +/** + * Extension point for routing Mockzilla's internal log output to a custom sink, such as a crash + * reporting service or custom logger. Register via [MockzillaConfig.Builder.addLogWriter]. + */ interface MockzillaLogWriter { + /** + * Called by Mockzilla for each log entry. + * + * @param logLevel The severity of this log entry. + * @param message The log message. + * @param tag The source tag identifying the component that produced this log entry. + * @param throwable An associated exception, if any. + */ fun log( logLevel: MockzillaConfig.LogLevel, message: String, diff --git a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/sharedstate/MockzillaSharedProcessState.kt b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/sharedstate/MockzillaSharedProcessState.kt index 12c039131..afc02be16 100644 --- a/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/sharedstate/MockzillaSharedProcessState.kt +++ b/mockzilla-common/src/commonMain/kotlin/com/apadmi/mockzilla/lib/sharedstate/MockzillaSharedProcessState.kt @@ -5,23 +5,42 @@ import com.apadmi.mockzilla.lib.internal.utils.JsonProvider import kotlinx.serialization.Serializable /** - * @property ip - * @property port + * The IP address and port of a running Mockzilla server, persisted so it can be shared between + * processes on the same device. Written by the mockzilla server and read by the management UI module. + * + * @property ip The IP address the server is listening on. + * @property port The port the server is bound to. */ @Serializable data class MockzillaSharedProcessState(val ip: String, val port: Int) -// Used to share state between `mockzilla` and `mockzilla-mobile-ui` when -// running on the same device +/** + * Reads and writes [MockzillaSharedProcessState] to a file cache, allowing the Mockzilla server + * and the management UI to exchange connection details when running on the same device. + */ class MockzillaSharedProcessStateHandler(private val fileIo: FileIo) { private val fileName = "mockzilla-shared-state.json" private var sharedState: MockzillaSharedProcessState? = null + + /** + * Returns the most recently written [MockzillaSharedProcessState], reading from the file cache + * if no value has been set in the current process. Returns `null` if no state has been + * persisted yet. + * + * @return The shared process state, or `null` if unavailable. + */ suspend fun getSharedProcessState() = sharedState ?: fileIo.readFromCache(fileName)?.let { runCatching { JsonProvider.json.decodeFromString(it) }.getOrNull() } + /** + * Writes [state] to both the in-memory cache and the file cache so it is available to other + * processes reading via [getSharedProcessState]. + * + * @param state The server connection details to persist. + */ suspend fun setSharedProcessState(state: MockzillaSharedProcessState) { sharedState = state fileIo.saveToCache( diff --git a/mockzilla-common/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt b/mockzilla-common/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.ios.kt similarity index 93% rename from mockzilla-common/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt rename to mockzilla-common/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.ios.kt index 92137e304..4f480eae5 100644 --- a/mockzilla-common/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt +++ b/mockzilla-common/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.ios.kt @@ -1,9 +1,11 @@ package com.apadmi.mockzilla.lib.internal.utils +import com.apadmi.mockzilla.lib.InternalMockzillaApi import platform.Foundation.* import kotlinx.cinterop.ExperimentalForeignApi +@InternalMockzillaApi @OptIn(ExperimentalForeignApi::class) actual class FileIo { private val directoryPath by lazy { @@ -37,4 +39,5 @@ actual class FileIo { private fun filePath(filename: String) = "$directoryPath/$filename" } +@InternalMockzillaApi actual fun createFileIoforTesting() = FileIo() diff --git a/mockzilla-common/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.ios.kt b/mockzilla-common/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.ios.kt index 9c4926da4..5ac322493 100644 --- a/mockzilla-common/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.ios.kt +++ b/mockzilla-common/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.ios.kt @@ -1,7 +1,9 @@ package com.apadmi.mockzilla.lib.internal.utils +import com.apadmi.mockzilla.lib.InternalMockzillaApi import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO +@InternalMockzillaApi actual val Dispatchers.multiPlatformIo: CoroutineDispatcher get() = Dispatchers.IO diff --git a/mockzilla-common/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt b/mockzilla-common/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.js.kt similarity index 91% rename from mockzilla-common/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt rename to mockzilla-common/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.js.kt index cbfcd83e3..12b7c9b5f 100644 --- a/mockzilla-common/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt +++ b/mockzilla-common/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.js.kt @@ -1,10 +1,12 @@ package com.apadmi.mockzilla.lib.internal.utils +import com.apadmi.mockzilla.lib.InternalMockzillaApi import kotlin.random.Random import kotlinx.browser.localStorage var incrementForUniqueness = 0 +@InternalMockzillaApi actual class FileIo(private val filePrefix: String = "mockzilla_cache_") { actual suspend fun readFromCache(filename: String): String? = localStorage.getItem(filePrefix + filename) @@ -24,6 +26,7 @@ actual class FileIo(private val filePrefix: String = "mockzilla_cache_") { .forEach { localStorage.removeItem(it) } } } +@InternalMockzillaApi actual fun createFileIoforTesting() = FileIo( // Ensure each test has a de-facto isolated storage bucket to prevent overlap // in parallel tests diff --git a/mockzilla-common/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.js.kt b/mockzilla-common/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.js.kt index 082b3637d..60ea569a8 100644 --- a/mockzilla-common/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.js.kt +++ b/mockzilla-common/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.js.kt @@ -1,6 +1,8 @@ package com.apadmi.mockzilla.lib.internal.utils +import com.apadmi.mockzilla.lib.InternalMockzillaApi import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +@InternalMockzillaApi actual val Dispatchers.multiPlatformIo: CoroutineDispatcher get() = Dispatchers.Main diff --git a/mockzilla-common/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt b/mockzilla-common/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.jvm.kt similarity index 91% rename from mockzilla-common/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt rename to mockzilla-common/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.jvm.kt index 904d33b4e..fe6c9bae3 100644 --- a/mockzilla-common/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.kt +++ b/mockzilla-common/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/FileIo.jvm.kt @@ -1,9 +1,13 @@ +@file:JvmName("FileIoKt") + package com.apadmi.mockzilla.lib.internal.utils +import com.apadmi.mockzilla.lib.InternalMockzillaApi import java.io.File import java.io.IOException import java.nio.file.Files +@InternalMockzillaApi actual class FileIo(private val cacheDir: File) { private val cacheDirectory get() = File( @@ -43,4 +47,5 @@ actual class FileIo(private val cacheDir: File) { } } +@InternalMockzillaApi actual fun createFileIoforTesting(): FileIo = FileIo(Files.createTempDirectory("").toFile()) diff --git a/mockzilla-common/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.kt b/mockzilla-common/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.jvm.kt similarity index 66% rename from mockzilla-common/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.kt rename to mockzilla-common/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.jvm.kt index 745f795c5..14a30ea71 100644 --- a/mockzilla-common/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.kt +++ b/mockzilla-common/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ServerUtils.jvm.kt @@ -1,6 +1,10 @@ +@file:JvmName("ServerUtilsKt") + package com.apadmi.mockzilla.lib.internal.utils +import com.apadmi.mockzilla.lib.InternalMockzillaApi import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +@InternalMockzillaApi actual val Dispatchers.multiPlatformIo: CoroutineDispatcher get() = Dispatchers.IO diff --git a/mockzilla-management-ui/mockzilla-desktop/build.gradle.kts b/mockzilla-management-ui/mockzilla-desktop/build.gradle.kts index 055f47360..a20ac79a0 100644 --- a/mockzilla-management-ui/mockzilla-desktop/build.gradle.kts +++ b/mockzilla-management-ui/mockzilla-desktop/build.gradle.kts @@ -119,6 +119,7 @@ kotlin { } compilerOptions { freeCompilerArgs.addAll(CompilerConfig.freeCompilerArgs) + freeCompilerArgs.add("-opt-in=com.apadmi.mockzilla.lib.InternalMockzillaApi") } } diff --git a/mockzilla-management-ui/mockzilla-desktop/src/androidMain/kotlin/com/apadmi/mockzilla/desktop/engine/connection/ZeroConfSdkWrapper.kt b/mockzilla-management-ui/mockzilla-desktop/src/androidMain/kotlin/com/apadmi/mockzilla/desktop/engine/connection/ZeroConfSdkWrapper.android.kt similarity index 100% rename from mockzilla-management-ui/mockzilla-desktop/src/androidMain/kotlin/com/apadmi/mockzilla/desktop/engine/connection/ZeroConfSdkWrapper.kt rename to mockzilla-management-ui/mockzilla-desktop/src/androidMain/kotlin/com/apadmi/mockzilla/desktop/engine/connection/ZeroConfSdkWrapper.android.kt diff --git a/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/engine/connection/AdbConnectorService.kt b/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/engine/connection/AdbConnectorService.kt index 9bef3616d..62eac7f85 100644 --- a/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/engine/connection/AdbConnectorService.kt +++ b/mockzilla-management-ui/mockzilla-desktop/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/engine/connection/AdbConnectorService.kt @@ -14,7 +14,6 @@ import com.malinskiy.adam.request.forwarding.LocalTcpPortSpec import com.malinskiy.adam.request.forwarding.PortForwardRequest import com.malinskiy.adam.request.forwarding.RemoteTcpPortSpec import com.malinskiy.adam.request.shell.v2.ShellCommandRequest - import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds diff --git a/mockzilla-management-ui/mockzilla-desktop/src/desktopMain/kotlin/com/apadmi/mockzilla/desktop/engine/connection/ZeroConfSdkWrapper.kt b/mockzilla-management-ui/mockzilla-desktop/src/desktopMain/kotlin/com/apadmi/mockzilla/desktop/engine/connection/ZeroConfSdkWrapper.desktop.kt similarity index 100% rename from mockzilla-management-ui/mockzilla-desktop/src/desktopMain/kotlin/com/apadmi/mockzilla/desktop/engine/connection/ZeroConfSdkWrapper.kt rename to mockzilla-management-ui/mockzilla-desktop/src/desktopMain/kotlin/com/apadmi/mockzilla/desktop/engine/connection/ZeroConfSdkWrapper.desktop.kt diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/build.gradle.kts b/mockzilla-management-ui/mockzilla-management-ui-common/build.gradle.kts index 55d46151c..8e33d3554 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/build.gradle.kts +++ b/mockzilla-management-ui/mockzilla-management-ui-common/build.gradle.kts @@ -105,15 +105,13 @@ kotlin { implementation(libs.koin.compose) implementation(libs.androidx.compose.activity) - implementation(compose.preview) - implementation(compose.components.uiToolingPreview) + implementation(libs.ui.tooling.preview) } - val androidUnitTest by getting { - dependencies { - implementation(libs.androidx.test.junit) - implementation(libs.testParamInjector) - } + androidUnitTest.dependencies { + implementation(libs.androidx.test.junit) + implementation(libs.testParamInjector) } + val desktopMain by getting { dependencies { /* Compose */ @@ -141,14 +139,10 @@ kotlin { } compilerOptions { freeCompilerArgs.addAll(CompilerConfig.freeCompilerArgs) + freeCompilerArgs.add("-opt-in=com.apadmi.mockzilla.lib.InternalMockzillaApi") } } -dependencies { - /* Compose Previews */ - debugImplementation(compose.uiTooling) -} - android { namespace = "$group.mockzilla.mobile.ui.common" compileSdk = AndroidConfig.targetSdk diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinHandler.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinHandler.kt index 74b7f7776..afe218d59 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinHandler.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/di/utils/KoinHandler.kt @@ -8,6 +8,7 @@ import com.apadmi.mockzilla.ui.engine.device.ActiveDeviceMonitor import com.apadmi.mockzilla.ui.engine.device.ActiveDeviceSelector import com.apadmi.mockzilla.ui.engine.events.EventBus import com.apadmi.mockzilla.ui.engine.events.EventBusImpl +import com.apadmi.mockzilla.ui.utils.Platform import org.koin.core.module.Module import org.koin.dsl.binds @@ -22,16 +23,22 @@ object MockzillaUiKoinContext { @OptIn(DelicateCoroutinesApi::class) private val koinApp = koinApplication { + val mockzillaManagement = MockzillaManagement.constructInstance(config = MockzillaManagement.Config( + // Bypasses proxy when running on mobile devices since the server is on device + // going via a proxy can redirect calls to the proxy machine instead of the local device + // (Notably this is needed for Mockzilla to run on Browserstack) + disableProxy = Platform.current != Platform.Desktop + )) modules( viewModelModule(), useCaseModule(), module { - single { MockzillaManagement.instance.appIconService } - single { MockzillaManagement.instance.metaDataService } - single { MockzillaManagement.instance.logsService } - single { MockzillaManagement.instance.endpointsService } - single { MockzillaManagement.instance.updateService } - single { MockzillaManagement.instance.cacheClearingService } + single { mockzillaManagement.appIconService } + single { mockzillaManagement.metaDataService } + single { mockzillaManagement.logsService } + single { mockzillaManagement.endpointsService } + single { mockzillaManagement.updateService } + single { mockzillaManagement.cacheClearingService } single { EventBusImpl(GlobalScope) } single { ActiveDeviceManagerImpl(get(), GlobalScope) } binds arrayOf( ActiveDeviceMonitor::class, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/Config.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/Config.kt index b5fded64c..5642fd25d 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/Config.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/Config.kt @@ -3,7 +3,7 @@ package com.apadmi.mockzilla.ui.engine import com.apadmi.mockzilla.ui.utils.Platform import io.github.z4kn4fein.semver.Version -object Config { +internal object Config { val minSupportedMockzillaVersion get() = when (Platform.current) { Platform.Desktop -> Version.parse("1.99.99") Platform.Android, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/ActiveDeviceManager.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/ActiveDeviceManager.kt index a6b341685..533d0492a 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/ActiveDeviceManager.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/ActiveDeviceManager.kt @@ -33,7 +33,7 @@ interface ActiveDeviceSelector { fun removeDevice(device: Device) } -class ActiveDeviceManagerImpl( +internal class ActiveDeviceManagerImpl( private val metaDataUseCase: MetaDataUseCase, private val scope: CoroutineScope ) : ActiveDeviceMonitor, ActiveDeviceSelector { diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/MetaDataUseCase.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/MetaDataUseCase.kt index 8e37fb5d1..cd98569e6 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/MetaDataUseCase.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/MetaDataUseCase.kt @@ -14,7 +14,7 @@ interface MetaDataUseCase { suspend fun getMetaData(device: Device, isPolling: Boolean = false): Result } -class MetaDataUseCaseImpl( +internal class MetaDataUseCaseImpl( private val managementMetaDataService: MockzillaManagement.MetaDataService, private val currentTimeStamp: TimeStampAccessor = { Clock.System.now().toEpochMilliseconds() } ) : MetaDataUseCase { diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/MonitorLogsUseCase.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/MonitorLogsUseCase.kt index f0b36f7ea..23cb478db 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/MonitorLogsUseCase.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/device/MonitorLogsUseCase.kt @@ -5,12 +5,12 @@ import com.apadmi.mockzilla.management.MockzillaManagement import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -interface MonitorLogsUseCase { +internal interface MonitorLogsUseCase { suspend fun getMonitorLogs(device: Device): Result> suspend fun clearMonitorLogs(device: Device): Result } -class MonitorLogsUseCaseImpl( +internal class MonitorLogsUseCaseImpl( private val managementLogsService: MockzillaManagement.LogsService, private val managementMetaDataService: MockzillaManagement.MetaDataService, ) : MonitorLogsUseCase { diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/events/EventBus.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/events/EventBus.kt index 8244daf6d..ba8f0bf74 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/events/EventBus.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/events/EventBus.kt @@ -23,7 +23,7 @@ interface EventBus { } } -class EventBusImpl( +internal class EventBusImpl( private val coroutineScope: CoroutineScope ) : EventBus { override val events = MutableSharedFlow() diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/filter/FuzzyFilter.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/filter/FuzzyFilter.kt index ec8700636..2c7e3d2a7 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/filter/FuzzyFilter.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/filter/FuzzyFilter.kt @@ -3,7 +3,7 @@ package com.apadmi.mockzilla.ui.engine.filter import kotlin.math.max import kotlin.math.min -data object FuzzyFilter { +internal data object FuzzyFilter { /** * Filters a list of items to only return items matching * the filter exactly or with minor edits, sorted by the diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/jsoneditor/JsonEditor.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/jsoneditor/JsonEditor.kt index f1a6135c6..0487a7795 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/jsoneditor/JsonEditor.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/engine/jsoneditor/JsonEditor.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.json.Json // or comments are allowed for JSON responses so we can match the application's validation private val jsonConfiguration = Json -class JsonEditor( +internal class JsonEditor( private val body: String ) { fun isValidJson(): Boolean = try { diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/ForceFailureBanner.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/ForceFailureBanner.kt index 9daca8cc1..c83c1c969 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/ForceFailureBanner.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/components/ForceFailureBanner.kt @@ -42,7 +42,7 @@ import com.apadmi.mockzilla.ui.ui.common.theme.warning private const val bannerCornerRadius = 8 -enum class ForceFailureBannerState { +internal enum class ForceFailureBannerState { FullFailure, Normal, PartialFailure, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetViewModel.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetViewModel.kt index 88b6fbe03..ed7b7ef6f 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetViewModel.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetViewModel.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.serialization.json.Json -class CreateEditPresetViewModel( +internal class CreateEditPresetViewModel( private val key: EndpointConfiguration.Key, private val device: Device, private val variant: State.Editing.Variant, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetWidget.kt index 6207bdf3d..27d5917f5 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/createeditpreset/CreateEditPresetWidget.kt @@ -479,7 +479,7 @@ fun CreateEditPresetWidget( } @Composable -fun CreateEditPresetWidgetContent( +internal fun CreateEditPresetWidgetContent( state: State, endpointName: String? = null, onCancel: () -> Unit = {}, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/details/EndpointDetailsViewModel.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/details/EndpointDetailsViewModel.kt index 0c8fb652b..16074aea7 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/details/EndpointDetailsViewModel.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/details/EndpointDetailsViewModel.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.launch private typealias UpdateServerBlock = (config: SerializableEndpointConfig, device: Device) -> Unit private typealias UpdateStateBlock = EndpointDetailsViewModel.State.Endpoint.() -> EndpointDetailsViewModel.State.Endpoint -class EndpointDetailsViewModel( +internal class EndpointDetailsViewModel( private val key: EndpointConfiguration.Key?, private val device: Device, private val endpointsService: MockzillaManagement.EndpointsService, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/details/EndpointDetailsWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/details/EndpointDetailsWidget.kt index 67a8b547b..577c0be21 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/details/EndpointDetailsWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/details/EndpointDetailsWidget.kt @@ -271,7 +271,7 @@ fun EndpointDetailsWidget( } @Composable -fun EndpointDetailsWidgetContent( +internal fun EndpointDetailsWidgetContent( state: State, onDelayChange: (Int?) -> Unit, onFailChange: (Boolean?) -> Unit, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/endpoints/EndpointsViewModel.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/endpoints/EndpointsViewModel.kt index 2ec74430b..749c39ec4 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/endpoints/EndpointsViewModel.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/endpoints/endpoints/EndpointsViewModel.kt @@ -17,7 +17,12 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -class EndpointsViewModel( +/** Controls how much information each row in the endpoint list shows. */ +internal enum class RowDensity { + Comfy, Compact +} + +internal class EndpointsViewModel( private val device: Device, private val endpointsService: MockzillaManagement.EndpointsService, private val eventBus: EventBus, @@ -104,15 +109,10 @@ class EndpointsViewModel( } } -/** Controls how much information each row in the endpoint list shows. */ -enum class RowDensity { - Comfy, Compact -} - /** * @property displayName */ -enum class EndpointProperties(val displayName: String) { +internal enum class EndpointProperties(val displayName: String) { Body("Body"), Delay("Latency"), Headers("Headers"), diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/metadata/MetaDataWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/metadata/MetaDataWidget.kt index 126b92ff2..132ab1971 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/metadata/MetaDataWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/metadata/MetaDataWidget.kt @@ -70,8 +70,31 @@ fun MetaDataWidget(device: Device) { MetaDataWidgetContent(state, device) } +// ── Preview ─────────────────────────────────────────────────────────────────── + +@Suppress("COMPLEX_EXPRESSION") +@Preview @Composable -fun MetaDataWidgetContent( +fun MetaDataListViewPreview() = PreviewSurface() { + MetaDataListView( + state = MetaDataWidgetViewModel.State.DisplayMetaData( + metaData = MetaData( + appName = "Runner", + appPackage = "uk.co.homeserve.pega.sus.internal", + operatingSystemVersion = "Version 18.5 (Build 22F77)", + deviceModel = "iPhone 16 Plus", + appVersion = "999.999.1", + mockzillaVersion = "3.0.0-alpha2", + runTarget = RunTarget.IosSimulator + ), + requestCount = 17, + ), + device = Device(ip = "127.0.0.1", port = "49812") + ) +} + +@Composable +internal fun MetaDataWidgetContent( state: MetaDataWidgetViewModel.State, device: Device? = null, strings: Strings = LocalStrings.current @@ -81,7 +104,12 @@ fun MetaDataWidgetContent( contentAlignment = Alignment.Center ) { when (state) { - is MetaDataWidgetViewModel.State.DisplayMetaData -> MetaDataListView(state, device, strings) + is MetaDataWidgetViewModel.State.DisplayMetaData -> MetaDataListView( + state, + device, + strings + ) + MetaDataWidgetViewModel.State.Error -> Text(strings.widgets.metaData.error) MetaDataWidgetViewModel.State.Loading -> CircularProgressIndicator() } @@ -89,11 +117,15 @@ fun MetaDataWidgetContent( } @Composable -fun MetaDataListView( +internal fun MetaDataListView( state: MetaDataWidgetViewModel.State.DisplayMetaData, device: Device? = null, strings: Strings = LocalStrings.current -) = Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) { +) = Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) +) { AppHeader( appName = state.metaData.appName, appPackage = state.metaData.appPackage, @@ -113,7 +145,7 @@ fun MetaDataListView( } @Composable -fun MetaDataRow( +internal fun MetaDataRow( label: String, value: String, showDivider: Boolean = true @@ -143,29 +175,6 @@ fun MetaDataRow( } } -// ── Preview ─────────────────────────────────────────────────────────────────── - -@Suppress("COMPLEX_EXPRESSION") -@Preview -@Composable -fun MetaDataListViewPreview() = PreviewSurface() { - MetaDataListView( - state = MetaDataWidgetViewModel.State.DisplayMetaData( - metaData = MetaData( - appName = "Runner", - appPackage = "uk.co.homeserve.pega.sus.internal", - operatingSystemVersion = "Version 18.5 (Build 22F77)", - deviceModel = "iPhone 16 Plus", - appVersion = "999.999.1", - mockzillaVersion = "3.0.0-alpha2", - runTarget = RunTarget.IosSimulator - ), - requestCount = 17, - ), - device = Device(ip = "127.0.0.1", port = "49812") - ) -} - // ── Sections ───────────────────────────────────────────────────────────────── @Composable diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/metadata/MetaDataWidgetViewModel.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/metadata/MetaDataWidgetViewModel.kt index 6ebe15cf1..40aa440e8 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/metadata/MetaDataWidgetViewModel.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/metadata/MetaDataWidgetViewModel.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class MetaDataWidgetViewModel( +internal class MetaDataWidgetViewModel( private val device: Device, private val metaDataUseCase: MetaDataUseCase, private val monitorLogsUseCase: MonitorLogsUseCase, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/misccontrols/MiscControlsViewModel.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/misccontrols/MiscControlsViewModel.kt index 3695309b1..b8f9377f8 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/misccontrols/MiscControlsViewModel.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/misccontrols/MiscControlsViewModel.kt @@ -8,7 +8,7 @@ import com.apadmi.mockzilla.ui.viewmodel.ViewModel import kotlinx.coroutines.CoroutineScope -class MiscControlsViewModel( +internal class MiscControlsViewModel( private val device: Device?, private val eventBus: EventBus, private val clearingService: MockzillaManagement.CacheClearingService, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/misccontrols/MiscControlsWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/misccontrols/MiscControlsWidget.kt index 7008b8fb1..1b47ce7b2 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/misccontrols/MiscControlsWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/misccontrols/MiscControlsWidget.kt @@ -80,8 +80,17 @@ fun MiscControlsWidget( ) } +@Preview +@Composable +fun MiscControlsWidgetPreview() = PreviewSurface(darkTheme = true) { + MiscControlsWidgetContent( + onRefreshAll = {}, + onClearAllOverrides = {} + ) +} + @Composable -fun MiscControlsWidgetContent( +internal fun MiscControlsWidgetContent( onRefreshAll: () -> Unit, onClearAllOverrides: () -> Unit, strings: Strings = LocalStrings.current @@ -149,15 +158,6 @@ fun MiscControlsWidgetContent( } } -@Preview -@Composable -fun MiscControlsWidgetPreview() = PreviewSurface(darkTheme = true) { - MiscControlsWidgetContent( - onRefreshAll = {}, - onClearAllOverrides = {} - ) -} - @Composable private fun DarkModeSettings( strings: Strings = LocalStrings.current diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/MonitorLogsViewModel.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/MonitorLogsViewModel.kt index 2dc76ddc2..2e5fcca20 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/MonitorLogsViewModel.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/MonitorLogsViewModel.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -class MonitorLogsViewModel( +internal class MonitorLogsViewModel( private val device: Device, private val monitorLogsUseCase: MonitorLogsUseCase, scope: CoroutineScope? = null diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/MonitorLogsWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/MonitorLogsWidget.kt index a1f9f5ea1..4072864bf 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/MonitorLogsWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/MonitorLogsWidget.kt @@ -97,95 +97,6 @@ fun MonitorLogsWidget( ) } -@Suppress("MAGIC_NUMBER") -@Composable -fun MonitorLogsWidgetContent( - state: MonitorLogsViewModel.State.DisplayLogs, - onClearAll: () -> Unit, - onViewDetail: (LogEvent) -> Unit, - onOpenPanel: () -> Unit = {}, - strings: Strings = LocalStrings.current, -) { - val monoFont = LocalMonoFontFamily.current - val cs = MaterialTheme.colorScheme - val streamingColor = cs.success.primary - val dimColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) - val faintColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) - val entryList = state.entries.toList() - val titleStyle = MaterialTheme.typography.labelSmall.copy( - fontFamily = monoFont, - fontWeight = FontWeight.SemiBold, - ) - val logsTitle = strings.widgets.logs.title.uppercase() - - var isExpanded by remember { mutableStateOf(false) } - val chevronRotation by animateFloatAsState( - targetValue = if (isExpanded) 90f else 0f, - animationSpec = tween(durationMillis = 150), - label = "chevronRotation", - ) - - Column(modifier = Modifier.background(cs.background)) { - HorizontalDivider(color = MaterialTheme.colorScheme.outline) - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { isExpanded = !isExpanded } - .padding(horizontal = 12.dp, vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(5.dp), - ) { - Icon( - imageVector = Icons.Default.KeyboardArrowRight, - contentDescription = null, - tint = faintColor, - modifier = Modifier.size(14.dp).rotate(chevronRotation), - ) - Text( - text = logsTitle, - style = titleStyle, - color = dimColor, - ) - Canvas(modifier = Modifier.size(6.dp)) { drawCircle(color = streamingColor) } - Text( - text = strings.widgets.logs.streaming, - style = MaterialTheme.typography.labelSmall.copy(fontFamily = monoFont), - color = streamingColor, - ) - Text( - text = strings.widgets.logs.clickToInspect, - style = MaterialTheme.typography.labelSmall.copy(fontFamily = monoFont), - color = faintColor, - ) - Spacer(modifier = Modifier.weight(1f)) - Text( - modifier = Modifier.clickable(onClick = onOpenPanel), - text = strings.widgets.logs.openInPanel, - style = MaterialTheme.typography.labelSmall.copy(fontFamily = monoFont), - color = dimColor, - ) - } - - AnimatedVisibility( - visible = isExpanded, - enter = expandVertically( - expandFrom = Alignment.Top, - animationSpec = tween(durationMillis = 160), - ) + fadeIn(animationSpec = tween(durationMillis = 120)), - exit = shrinkVertically( - shrinkTowards = Alignment.Top, - animationSpec = tween(durationMillis = 130), - ) + fadeOut(animationSpec = tween(durationMillis = 100)), - ) { - MonitorLogsList( - entryList = entryList, - onViewDetail = onViewDetail, - modifier = Modifier.height(280.dp), - ) - } - } -} - @Suppress("MAGIC_NUMBER") @Composable fun LogRow( @@ -329,6 +240,95 @@ fun MonitorLogsWidgetPreview() = PreviewSurface { ) } +@Suppress("MAGIC_NUMBER") +@Composable +internal fun MonitorLogsWidgetContent( + state: MonitorLogsViewModel.State.DisplayLogs, + onClearAll: () -> Unit, + onViewDetail: (LogEvent) -> Unit, + onOpenPanel: () -> Unit = {}, + strings: Strings = LocalStrings.current, +) { + val monoFont = LocalMonoFontFamily.current + val cs = MaterialTheme.colorScheme + val streamingColor = cs.success.primary + val dimColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) + val faintColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) + val entryList = state.entries.toList() + val titleStyle = MaterialTheme.typography.labelSmall.copy( + fontFamily = monoFont, + fontWeight = FontWeight.SemiBold, + ) + val logsTitle = strings.widgets.logs.title.uppercase() + + var isExpanded by remember { mutableStateOf(false) } + val chevronRotation by animateFloatAsState( + targetValue = if (isExpanded) 90f else 0f, + animationSpec = tween(durationMillis = 150), + label = "chevronRotation", + ) + + Column(modifier = Modifier.background(cs.background)) { + HorizontalDivider(color = MaterialTheme.colorScheme.outline) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { isExpanded = !isExpanded } + .padding(horizontal = 12.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + Icon( + imageVector = Icons.Default.KeyboardArrowRight, + contentDescription = null, + tint = faintColor, + modifier = Modifier.size(14.dp).rotate(chevronRotation), + ) + Text( + text = logsTitle, + style = titleStyle, + color = dimColor, + ) + Canvas(modifier = Modifier.size(6.dp)) { drawCircle(color = streamingColor) } + Text( + text = strings.widgets.logs.streaming, + style = MaterialTheme.typography.labelSmall.copy(fontFamily = monoFont), + color = streamingColor, + ) + Text( + text = strings.widgets.logs.clickToInspect, + style = MaterialTheme.typography.labelSmall.copy(fontFamily = monoFont), + color = faintColor, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + modifier = Modifier.clickable(onClick = onOpenPanel), + text = strings.widgets.logs.openInPanel, + style = MaterialTheme.typography.labelSmall.copy(fontFamily = monoFont), + color = dimColor, + ) + } + + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically( + expandFrom = Alignment.Top, + animationSpec = tween(durationMillis = 160), + ) + fadeIn(animationSpec = tween(durationMillis = 120)), + exit = shrinkVertically( + shrinkTowards = Alignment.Top, + animationSpec = tween(durationMillis = 130), + ) + fadeOut(animationSpec = tween(durationMillis = 100)), + ) { + MonitorLogsList( + entryList = entryList, + onViewDetail = onViewDetail, + modifier = Modifier.height(280.dp), + ) + } + } +} + @Composable private fun MonitorLogsList( entryList: List, diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/details/MonitorLogDetailsViewModel.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/details/MonitorLogDetailsViewModel.kt index 1fd86945e..774baf5e7 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/details/MonitorLogDetailsViewModel.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/details/MonitorLogDetailsViewModel.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -class MonitorLogDetailsViewModel( +internal class MonitorLogDetailsViewModel( scope: CoroutineScope? = null ) : ViewModel(scope) { val state = MutableStateFlow(State.ViewDetails()) diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/details/MonitorLogDetailsWidget.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/details/MonitorLogDetailsWidget.kt index aa1d374bb..69913ea14 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/details/MonitorLogDetailsWidget.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/commonMain/kotlin/com/apadmi/mockzilla/ui/ui/common/widgets/monitorlogs/details/MonitorLogDetailsWidget.kt @@ -84,9 +84,48 @@ fun MonitorLogDetailsWidget( } @Composable -fun MonitorLogDetailsContent( +fun MonitorLogDetailsWidgetEmptyPreview() = PreviewSurface { + Box(modifier = Modifier.size(300.dp)) { + MonitorLogDetailsEmptyContent() + } +} + +@Suppress("COMPLEX_EXPRESSION", "MAGIC_NUMBER") +@Preview +@Composable +fun MonitorLogDetailsWidgetPreview() { + val previewBody = """{"repairs":[{"id":"HSR-9455","repairStatus":"Upcoming","faultDescription":"Boiler pilot light"}]}""" + val previewStatus = HttpStatusCode.OK + val authHeader = "Authorization" to "Bearer token123" + val contentTypeHeader = "Content-Type" to "application/json" + val previewRequestHeaders = mapOf(authHeader) + val previewResponseHeaders = mapOf(contentTypeHeader) + val previewState = ViewDetails(selectedTab = Tab.Response) + val previewEvent = LogEvent( + timestamp = 1_716_474_257_201L, + url = "https://api.example.com/repairs", + requestBody = "", + requestHeaders = previewRequestHeaders, + responseHeaders = previewResponseHeaders, + responseBody = previewBody, + status = previewStatus, + delay = 342, + method = "GET", + isIntendedFailure = false, + ) + PreviewSurface { + LogDetailsContent( + logDetail = previewEvent, + state = previewState, + onTabSelected = {}, + ) + } +} + +@Composable +internal fun MonitorLogDetailsContent( logDetail: LogEvent?, - state: MonitorLogDetailsViewModel.State.ViewDetails, + state: ViewDetails, onTabSelected: (Tab) -> Unit, onClose: () -> Unit = {}, ) { @@ -99,7 +138,7 @@ fun MonitorLogDetailsContent( @Suppress("TOO_LONG_FUNCTION") @Composable -fun LogDetailsContent( +internal fun LogDetailsContent( logDetail: LogEvent, state: MonitorLogDetailsViewModel.State.ViewDetails, onTabSelected: (Tab) -> Unit, @@ -142,7 +181,9 @@ fun LogDetailsContent( } @Composable -fun MonitorLogDetailsEmptyContent(strings: Strings = LocalStrings.current) { +internal fun MonitorLogDetailsEmptyContent( + strings: Strings = LocalStrings.current, +) { EmptyState( title = strings.widgets.logDetails.emptyTitle, description = strings.widgets.logDetails.emptyDescription, @@ -158,46 +199,6 @@ fun MonitorLogDetailsEmptyContent(strings: Strings = LocalStrings.current) { ) } -@Preview -@Composable -fun MonitorLogDetailsWidgetEmptyPreview() = PreviewSurface { - Box(modifier = Modifier.size(300.dp)) { - MonitorLogDetailsEmptyContent() - } -} - -@Suppress("COMPLEX_EXPRESSION", "MAGIC_NUMBER") -@Preview -@Composable -fun MonitorLogDetailsWidgetPreview() { - val previewBody = """{"repairs":[{"id":"HSR-9455","repairStatus":"Upcoming","faultDescription":"Boiler pilot light"}]}""" - val previewStatus = HttpStatusCode.OK - val authHeader = "Authorization" to "Bearer token123" - val contentTypeHeader = "Content-Type" to "application/json" - val previewRequestHeaders = mapOf(authHeader) - val previewResponseHeaders = mapOf(contentTypeHeader) - val previewState = ViewDetails(selectedTab = Tab.Response) - val previewEvent = LogEvent( - timestamp = 1_716_474_257_201L, - url = "https://api.example.com/repairs", - requestBody = "", - requestHeaders = previewRequestHeaders, - responseHeaders = previewResponseHeaders, - responseBody = previewBody, - status = previewStatus, - delay = 342, - method = "GET", - isIntendedFailure = false, - ) - PreviewSurface { - LogDetailsContent( - logDetail = previewEvent, - state = previewState, - onTabSelected = {}, - ) - } -} - // ── Header bar ──────────────────────────────────────────────────────────────── @Suppress("MAGIC_NUMBER") diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopTest/kotlin/com/apadmi/mockzilla/ui/ui/widgets/metadata/MetaDataViewModelTests.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopTest/kotlin/com/apadmi/mockzilla/ui/ui/widgets/metadata/MetaDataViewModelTests.kt index a974b4535..070c8328e 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopTest/kotlin/com/apadmi/mockzilla/ui/ui/widgets/metadata/MetaDataViewModelTests.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopTest/kotlin/com/apadmi/mockzilla/ui/ui/widgets/metadata/MetaDataViewModelTests.kt @@ -23,7 +23,7 @@ import kotlin.test.assertNull import kotlin.test.assertTrue import kotlinx.coroutines.yield -class MetaDataViewModelTests : CoroutineTest() { +internal class MetaDataViewModelTests : CoroutineTest() { @RelaxedMockK lateinit var metaDataUseCaseMock: MetaDataUseCase diff --git a/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopTest/kotlin/com/apadmi/mockzilla/ui/ui/widgets/monitorlogs/MonitorLogsViewModelTests.kt b/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopTest/kotlin/com/apadmi/mockzilla/ui/ui/widgets/monitorlogs/MonitorLogsViewModelTests.kt index 34f9931bc..c4dbb5692 100644 --- a/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopTest/kotlin/com/apadmi/mockzilla/ui/ui/widgets/monitorlogs/MonitorLogsViewModelTests.kt +++ b/mockzilla-management-ui/mockzilla-management-ui-common/src/desktopTest/kotlin/com/apadmi/mockzilla/ui/ui/widgets/monitorlogs/MonitorLogsViewModelTests.kt @@ -16,7 +16,7 @@ import org.junit.Test import kotlin.test.assertEquals import kotlinx.coroutines.yield -class MonitorLogsViewModelTests : CoroutineTest() { +internal class MonitorLogsViewModelTests : CoroutineTest() { private val dummyActiveDevice = Device.dummy() private val dummyLogEvent = LogEvent( timestamp = 1, diff --git a/mockzilla-management-ui/mockzilla-mobile-ui/build.gradle.kts b/mockzilla-management-ui/mockzilla-mobile-ui/build.gradle.kts index aec1c6c36..b0e564532 100644 --- a/mockzilla-management-ui/mockzilla-mobile-ui/build.gradle.kts +++ b/mockzilla-management-ui/mockzilla-mobile-ui/build.gradle.kts @@ -133,6 +133,7 @@ kotlin { } compilerOptions { freeCompilerArgs.addAll(CompilerConfig.freeCompilerArgs) + freeCompilerArgs.add("-opt-in=com.apadmi.mockzilla.lib.InternalMockzillaApi") } } diff --git a/mockzilla-management/build.gradle.kts b/mockzilla-management/build.gradle.kts index 758e9dd2f..e446f6596 100644 --- a/mockzilla-management/build.gradle.kts +++ b/mockzilla-management/build.gradle.kts @@ -84,6 +84,12 @@ kotlin { /* Logging */ implementation(libs.kermit) } + jvmMain.dependencies { + implementation(libs.ktor.client.okhttp) + } + iosMain.dependencies { + implementation(libs.ktor.client.darwin) + } commonTest.dependencies { implementation(kotlin("test")) implementation(libs.kotlinx.coroutines.test) @@ -94,6 +100,7 @@ kotlin { } compilerOptions { freeCompilerArgs.addAll(CompilerConfig.freeCompilerArgs) + freeCompilerArgs.add("-opt-in=com.apadmi.mockzilla.lib.InternalMockzillaApi") } } diff --git a/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/MockzillaConnectionConfig.kt b/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/MockzillaConnectionConfig.kt index ca0ef8b8a..e4607551e 100644 --- a/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/MockzillaConnectionConfig.kt +++ b/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/MockzillaConnectionConfig.kt @@ -1,9 +1,16 @@ package com.apadmi.mockzilla.management /** - * Defines the info needed to create a connection to a device. (i.e. make a request) + * Defines the connection details needed to target a specific device running a Mockzilla server. */ interface MockzillaConnectionConfig { + /** + * The IP address of the device. + */ val ip: String + + /** + * The port the Mockzilla server is bound to on the device. + */ val port: String } diff --git a/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/MockzillaManagement.kt b/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/MockzillaManagement.kt index 276b94add..8f3287bf6 100644 --- a/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/MockzillaManagement.kt +++ b/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/MockzillaManagement.kt @@ -10,48 +10,150 @@ import com.apadmi.mockzilla.management.internal.MockzillaManagementRepository import com.apadmi.mockzilla.management.internal.MockzillaManagementRepositoryImpl import com.apadmi.mockzilla.management.internal.service.UpdateServiceImpl +/** + * A client for remotely manipulating a running Mockzilla server. Used by external tooling (such as + * the Mockzilla management desktop app or automated tests) to inspect and control mock endpoint + * behaviour at runtime without modifying application code. + * + * Create an instance via [constructInstance]. + */ interface MockzillaManagement { /** - * In cases where the wrapper isn't granular enough this gives access to handle manually make - * the raw requests to the server. + * Provides direct access to the underlying HTTP repository for cases where the higher-level + * services do not cover a required operation. Prefer using the typed service properties where + * possible. + * + * Note: requires `@OptIn(InternalMockzillaApi::class)` since [MockzillaManagementRepository] + * is an internal type. */ val underlyingRepository: MockzillaManagementRepository + + /** + * Service for modifying endpoint behaviour on a connected device at runtime. + */ val updateService: UpdateService + + /** + * Service for fetching device and application metadata from a connected device. + */ val metaDataService: MetaDataService + + /** + * Service for fetching monitor logs from a connected device. + */ val logsService: LogsService + + /** + * Service for clearing endpoint response caches on a connected device. + */ val cacheClearingService: CacheClearingService + + /** + * Service for querying endpoint configurations from a connected device. + */ val endpointsService: EndpointsService val appIconService: AppIconService + /** + * Clears endpoint response caches on a connected device. Clearing a cache causes the next + * request to that endpoint to return a fresh response rather than a cached one. + */ interface CacheClearingService { + /** + * Clears the response cache for every endpoint on the device at [connection]. + * + * @param connection The device to target. + * @return [Result.success] on success, [Result.failure] if the request could not be completed. + */ suspend fun clearAllCaches(connection: MockzillaConnectionConfig): Result + + /** + * Clears the response cache for the specified endpoints on the device at [connection]. + * + * @param connection The device to target. + * @param keys The keys of the endpoints whose caches should be cleared. + * @return [Result.success] on success, [Result.failure] if the request could not be completed. + */ suspend fun clearCaches( connection: MockzillaConnectionConfig, keys: List ): Result } + /** + * Queries endpoint configurations from a connected device. + */ interface EndpointsService { + /** + * Fetches the current configuration for all endpoints registered on the device at [connection]. + * + * @param connection The device to target. + * @return [Result.success] wrapping the list of endpoint configs, or [Result.failure] if the + * request could not be completed. + */ suspend fun fetchAllEndpointConfigs(connection: MockzillaConnectionConfig): Result> + + /** + * Fetches the dashboard options configuration for the endpoint identified by [key] on the + * device at [connection]. + * + * @param connection The device to target. + * @param key The key of the endpoint to query. + * @return [Result.success] wrapping the dashboard options config, or [Result.failure] if the + * request could not be completed. + */ suspend fun fetchDashboardOptionsConfig( connection: MockzillaConnectionConfig, key: EndpointConfiguration.Key ): Result } + /** + * Modifies endpoint behaviour on a connected device at runtime. Changes take effect immediately + * and are applied on top of the endpoint's static configuration — passing `null` for any value + * resets it to the endpoint's configured default. + */ interface UpdateService { + /** + * Overrides whether the specified endpoints return error responses on the device at + * [connection]. + * + * @param connection The device to target. + * @param keys The keys of the endpoints to update. + * @param shouldFail `true` to force error responses, `false` to force success responses, + * `null` to reset to each endpoint's configured default. + * @return [Result.success] on success, [Result.failure] if the request could not be completed. + */ suspend fun setShouldFail( connection: MockzillaConnectionConfig, keys: Collection, shouldFail: Boolean? ): Result + /** + * Overrides the response delay for the specified endpoints on the device at [connection]. + * + * @param connection The device to target. + * @param keys The keys of the endpoints to update. + * @param delayMs The delay to apply in milliseconds, or `null` to reset to each endpoint's + * configured default. + * @return [Result.success] on success, [Result.failure] if the request could not be completed. + */ suspend fun setDelay( connection: MockzillaConnectionConfig, keys: Collection, delayMs: Int? ): Result + /** + * Applies a dashboard preset to the endpoint identified by [key] on the device at + * [connection], overriding the endpoint's response until the override is cleared. + * + * @param connection The device to target. + * @param key The key of the endpoint to update. + * @param dashboardOverridePreset The preset to apply. + * @return [Result.success] on success, [Result.failure] if the request could not be completed. + */ suspend fun applyPreset( connection: MockzillaConnectionConfig, key: EndpointConfiguration.Key, @@ -59,21 +161,66 @@ interface MockzillaManagement { ): Result } + /** + * Fetches device and application metadata from a connected device. + */ interface MetaDataService { + /** + * Fetches the device and application metadata from the device at [connection]. + * + * @param connection The device to target. + * @param hideFromLogs When `true`, suppresses console logging for this call. Useful for + * frequently-polled calls to avoid cluttering the console output. + * @return [Result.success] wrapping the device metadata, or [Result.failure] if the + * request could not be completed. + */ suspend fun fetchMetaData( connection: MockzillaConnectionConfig, hideFromLogs: Boolean ): Result } + /** + * Fetches monitor logs from a connected device. + */ interface LogsService { + /** + * Fetches all buffered monitor logs from the device at [connection] and clears the buffer. + * Subsequent calls will not return the same log entries. + * + * @param connection The device to target. + * @param hideFromLogs When `true`, suppresses console logging for this call. Useful for + * frequently-polled calls to avoid cluttering the console output. + * @return [Result.success] wrapping the log response, or [Result.failure] if the + * request could not be completed. + */ suspend fun fetchMonitorLogsAndClearBuffer( connection: MockzillaConnectionConfig, hideFromLogs: Boolean ): Result } + /** + * Configuration for a [MockzillaManagement] instance. + * + * @property disableProxy When `true`, management API calls bypass any system-level HTTP proxy + * configured on the machine. + */ + data class Config( + val disableProxy: Boolean = false + ) + + /** + * Fetches the app icon from a connected device. + */ interface AppIconService { + /** + * Fetches the app icon from the app at [connection] as a byte array + * + * @param connection The device to target. + * @return [Result.success] wrapping the raw byts of the icon, or [Result.failure] if the + * request could not be completed. + */ suspend fun fetchAppIcon(connection: MockzillaConnectionConfig): Result } @@ -88,9 +235,18 @@ interface MockzillaManagement { ) : MockzillaManagement companion object { - val instance: MockzillaManagement by lazy { - val repo = MockzillaManagementRepositoryImpl.create() - Instance(repo, UpdateServiceImpl(repo), repo, repo, repo, repo, repo) + @Deprecated("This property is deprecated") + val instance: MockzillaManagement by lazy { constructInstance() } + + /** + * Creates a new [MockzillaManagement] instance. + * + * @param config Configuration for this instance. + * @return A fully initialised [MockzillaManagement] ready to connect to devices. + */ + fun constructInstance(config: Config = Config()): MockzillaManagement { + val repo = MockzillaManagementRepositoryImpl.create(config) + return Instance(repo, UpdateServiceImpl(repo), repo, repo, repo, repo, repo) } } } diff --git a/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/internal/MockzillaManagementRepositoryImpl.kt b/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/internal/MockzillaManagementRepositoryImpl.kt index 0992d43a2..33c8cab66 100644 --- a/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/internal/MockzillaManagementRepositoryImpl.kt +++ b/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/internal/MockzillaManagementRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.apadmi.mockzilla.management.internal +import com.apadmi.mockzilla.lib.InternalMockzillaApi import com.apadmi.mockzilla.lib.internal.models.ClearCachesRequestDto import com.apadmi.mockzilla.lib.internal.models.MockDataResponseDto import com.apadmi.mockzilla.lib.internal.models.MonitorLogsResponse @@ -34,6 +35,7 @@ import io.ktor.http.isSuccess import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +@InternalMockzillaApi interface MockzillaManagementRepository { suspend fun fetchMetaData(connection: MockzillaConnectionConfig, hideFromLogs: Boolean): Result suspend fun fetchAllEndpointConfigs(connection: MockzillaConnectionConfig): Result> @@ -161,10 +163,13 @@ MockzillaManagement.AppIconService { } companion object { - internal fun create(logger: KtorLogger) = MockzillaManagementRepositoryImpl( - KtorRequestRunner(KtorClientProvider.createKtorClient(logger = logger)) + internal fun create(config: MockzillaManagement.Config, logger: KtorLogger) = MockzillaManagementRepositoryImpl( + KtorRequestRunner(KtorClientProvider.createKtorClient( + disableProxy = config.disableProxy, + logger = logger + )) ) - fun create() = create(KtorLogger.SIMPLE) + fun create(config: MockzillaManagement.Config) = create(config, KtorLogger.SIMPLE) } } diff --git a/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.kt b/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.kt index f2b8bb8b4..7f20696de 100644 --- a/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.kt +++ b/mockzilla-management/src/commonMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.kt @@ -3,7 +3,6 @@ package com.apadmi.mockzilla.management.internal.ktor import com.apadmi.mockzilla.lib.internal.utils.JsonProvider import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig -import io.ktor.client.engine.HttpClientEngine import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger @@ -15,13 +14,14 @@ import io.ktor.serialization.kotlinx.json.json internal object CustomHeaders { const val HideFromLogs = "hide-from-logs" } + internal object KtorClientProvider { - fun createKtorClient(engine: HttpClientEngine? = null, logger: Logger = Logger.SIMPLE) = - engine?.let { - HttpClient(engine) { - httpClientConfig(logger) - } - } ?: HttpClient { httpClientConfig(logger) } + fun createKtorClient( + disableProxy: Boolean, + logger: Logger = Logger.SIMPLE + ) = createPlatformKtorClient(disableProxy) { + httpClientConfig(logger) + } private fun HttpClientConfig<*>.httpClientConfig(logger: Logger) { install(ContentNegotiation) { @@ -39,3 +39,8 @@ internal object KtorClientProvider { install(Resources) } } + +internal expect fun createPlatformKtorClient( + disableProxy: Boolean, + configure: HttpClientConfig<*>.() -> Unit +): HttpClient diff --git a/mockzilla-management/src/commonTest/kotlin/com/apadmi/mockzilla/management/internal/service/UpdateServiceIntegrationTests.kt b/mockzilla-management/src/commonTest/kotlin/com/apadmi/mockzilla/management/internal/service/UpdateServiceIntegrationTests.kt index d441e1b6f..0733b3e54 100644 --- a/mockzilla-management/src/commonTest/kotlin/com/apadmi/mockzilla/management/internal/service/UpdateServiceIntegrationTests.kt +++ b/mockzilla-management/src/commonTest/kotlin/com/apadmi/mockzilla/management/internal/service/UpdateServiceIntegrationTests.kt @@ -7,6 +7,7 @@ import com.apadmi.mockzilla.lib.models.EndpointConfiguration import com.apadmi.mockzilla.lib.models.MockzillaConfig import com.apadmi.mockzilla.lib.models.PartialMockzillaHttpResponse import com.apadmi.mockzilla.management.MockzillaConnectionConfig +import com.apadmi.mockzilla.management.MockzillaManagement import com.apadmi.mockzilla.management.internal.MockzillaManagementRepositoryImpl import com.apadmi.mockzilla.testutils.runIntegrationTest import io.ktor.client.plugins.logging.Logger @@ -20,7 +21,7 @@ class UpdateServiceIntegrationTests { private suspend fun getEndpointConfig( connection: MockzillaConnectionConfig - ) = MockzillaManagementRepositoryImpl.create(Logger.SIMPLE) + ) = MockzillaManagementRepositoryImpl.create(MockzillaManagement.Config(), Logger.SIMPLE) .fetchAllEndpointConfigs(connection) .getOrThrow() .first { it.key == dummyConfig.key } diff --git a/mockzilla-management/src/commonTest/kotlin/com/apadmi/mockzilla/testutils/IntegrationTestRunner.kt b/mockzilla-management/src/commonTest/kotlin/com/apadmi/mockzilla/testutils/IntegrationTestRunner.kt index 626dd63c6..f73d9d4a8 100644 --- a/mockzilla-management/src/commonTest/kotlin/com/apadmi/mockzilla/testutils/IntegrationTestRunner.kt +++ b/mockzilla-management/src/commonTest/kotlin/com/apadmi/mockzilla/testutils/IntegrationTestRunner.kt @@ -5,6 +5,7 @@ import com.apadmi.mockzilla.lib.models.MockzillaConfig import com.apadmi.mockzilla.lib.models.MockzillaRuntimeParams import com.apadmi.mockzilla.lib.stopMockzilla import com.apadmi.mockzilla.management.MockzillaConnectionConfig +import com.apadmi.mockzilla.management.MockzillaManagement.* import com.apadmi.mockzilla.management.internal.MockzillaManagementRepository import com.apadmi.mockzilla.management.internal.MockzillaManagementRepositoryImpl import io.ktor.client.plugins.logging.Logger @@ -35,7 +36,10 @@ internal fun runIntegrationTest( testBlock: TestBlock ) = runTest { /* Setup */ - val repo = MockzillaManagementRepositoryImpl.create(logger = Logger.SIMPLE) + val repo = MockzillaManagementRepositoryImpl.create( + config = Config(), + logger = Logger.SIMPLE + ) val runtimeParams = startTestingMockzilla(appName, appVersion, config) /* Run Test */ diff --git a/mockzilla-management/src/iosMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.ios.kt b/mockzilla-management/src/iosMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.ios.kt new file mode 100644 index 000000000..899a20c65 --- /dev/null +++ b/mockzilla-management/src/iosMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.ios.kt @@ -0,0 +1,20 @@ +package com.apadmi.mockzilla.management.internal.ktor + +import io.ktor.client.HttpClient +import io.ktor.client.engine.darwin.Darwin + +internal actual fun createPlatformKtorClient( + disableProxy: Boolean, + configure: io.ktor.client.HttpClientConfig<*>.() -> Unit +) = HttpClient(Darwin) { + engine { + if (disableProxy) { + configureSession { + // Empty dictionary means no proxy + connectionProxyDictionary = emptyMap() + } + } + } + + configure() +} diff --git a/mockzilla-management/src/jsMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.jsMain.kt b/mockzilla-management/src/jsMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.jsMain.kt new file mode 100644 index 000000000..27ae71a7f --- /dev/null +++ b/mockzilla-management/src/jsMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.jsMain.kt @@ -0,0 +1,11 @@ +package com.apadmi.mockzilla.management.internal.ktor + +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig + +internal actual fun createPlatformKtorClient( + disableProxy: Boolean, + configure: HttpClientConfig<*>.() -> Unit +) = HttpClient { + configure() +} diff --git a/mockzilla-management/src/jvmMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.jvm.kt b/mockzilla-management/src/jvmMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.jvm.kt new file mode 100644 index 000000000..0e270d979 --- /dev/null +++ b/mockzilla-management/src/jvmMain/kotlin/com/apadmi/mockzilla/management/internal/ktor/KtorClientProvider.jvm.kt @@ -0,0 +1,22 @@ +package com.apadmi.mockzilla.management.internal.ktor + +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.engine.okhttp.OkHttp +import okhttp3.OkHttpClient +import java.net.Proxy + +internal actual fun createPlatformKtorClient( + disableProxy: Boolean, + configure: HttpClientConfig<*>.() -> Unit +) = HttpClient(OkHttp) { + engine { + if (disableProxy) { + preconfigured = OkHttpClient.Builder() + .proxy(Proxy.NO_PROXY) + .build() + } + } + + configure() +} diff --git a/mockzilla/build.gradle.kts b/mockzilla/build.gradle.kts index 05f01973c..e552e1277 100644 --- a/mockzilla/build.gradle.kts +++ b/mockzilla/build.gradle.kts @@ -137,6 +137,7 @@ kotlin { } compilerOptions { freeCompilerArgs.addAll(CompilerConfig.freeCompilerArgs) + freeCompilerArgs.add("-opt-in=com.apadmi.mockzilla.lib.InternalMockzillaApi") } js { diff --git a/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/AndroidMockzilla.kt b/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.android.kt similarity index 96% rename from mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/AndroidMockzilla.kt rename to mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.android.kt index 53e482156..d5c76ee3e 100644 --- a/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/AndroidMockzilla.kt +++ b/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.android.kt @@ -1,3 +1,5 @@ +@file:JvmName("AndroidMockzillaKt") + package com.apadmi.mockzilla.lib import android.content.Context @@ -45,9 +47,8 @@ fun startMockzilla(config: MockzillaConfig, context: Context): MockzillaRuntimeP } /** - * Stops the Mockzilla server, + * Stops the running Mockzilla server. * - * @return */ actual fun stopMockzilla() = runBlocking { stopServer() diff --git a/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/discovery/ZeroConfDiscoveryServiceImpl.kt b/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/discovery/ZeroConfDiscoveryServiceImpl.kt index b59e964e3..f268926c3 100644 --- a/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/discovery/ZeroConfDiscoveryServiceImpl.kt +++ b/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/discovery/ZeroConfDiscoveryServiceImpl.kt @@ -14,7 +14,7 @@ import com.google.android.gms.common.GoogleApiAvailabilityLight import java.util.UUID -class ZeroConfDiscoveryServiceImpl( +internal class ZeroConfDiscoveryServiceImpl( private val logger: Logger, private val context: Context ) : ZeroConfDiscoveryService { diff --git a/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/AndroidErrorUtils.kt b/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/AndroidErrorUtils.kt index de31fa6de..12d602124 100644 --- a/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/AndroidErrorUtils.kt +++ b/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/AndroidErrorUtils.kt @@ -2,4 +2,4 @@ package com.apadmi.mockzilla.lib.internal.utils -actual typealias AddressAlreadyInUseException = java.net.BindException +internal actual typealias AddressAlreadyInUseException = java.net.BindException diff --git a/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/EmulatorUtils.kt b/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/EmulatorUtils.kt index 7f3f6424f..c747b2a0c 100644 --- a/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/EmulatorUtils.kt +++ b/mockzilla/src/androidMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/EmulatorUtils.kt @@ -10,7 +10,7 @@ import android.annotation.SuppressLint import java.io.* import java.lang.reflect.Method -val isProbablyRunningOnEmulator: Boolean by lazy { +internal val isProbablyRunningOnEmulator: Boolean by lazy { // Android SDK emulator return@lazy ((Build.MANUFACTURER == "Google" && Build.BRAND == "google" && ((Build.FINGERPRINT.startsWith("google/sdk_gphone_") diff --git a/mockzilla/src/androidUnitTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.kt b/mockzilla/src/androidUnitTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.android.kt similarity index 93% rename from mockzilla/src/androidUnitTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.kt rename to mockzilla/src/androidUnitTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.android.kt index 25bebb938..0b5a26113 100644 --- a/mockzilla/src/androidUnitTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.kt +++ b/mockzilla/src/androidUnitTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.android.kt @@ -1,3 +1,5 @@ +@file:JvmName("FileUtilsKt") + package com.apadmi.mockzilla.testutils import java.io.File diff --git a/mockzilla/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/discovery/ZeroConfDiscoveryService.kt b/mockzilla/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/discovery/ZeroConfDiscoveryService.kt index ef14c7804..77e870583 100644 --- a/mockzilla/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/discovery/ZeroConfDiscoveryService.kt +++ b/mockzilla/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/discovery/ZeroConfDiscoveryService.kt @@ -4,7 +4,7 @@ import com.apadmi.mockzilla.lib.config.ZeroConfConfig import com.apadmi.mockzilla.lib.models.MetaData import com.apadmi.mockzilla.lib.models.RunTarget -interface ZeroConfDiscoveryService { +internal interface ZeroConfDiscoveryService { suspend fun makeDiscoverable(metaData: MetaData, port: Int) suspend fun stop() } diff --git a/mockzilla/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrorUtils.kt b/mockzilla/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrorUtils.kt index 8a45a0f35..e60d889fe 100644 --- a/mockzilla/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrorUtils.kt +++ b/mockzilla/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrorUtils.kt @@ -2,9 +2,9 @@ package com.apadmi.mockzilla.lib.internal.utils -expect class AddressAlreadyInUseException : Throwable +internal expect class AddressAlreadyInUseException : Throwable -fun Throwable.isSomeMatchInChain(predicate: (Throwable) -> Boolean): Boolean { +internal fun Throwable.isSomeMatchInChain(predicate: (Throwable) -> Boolean): Boolean { var current: Throwable? = this while (current != null) { if (predicate(current)) { diff --git a/mockzilla/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/KtorMockzillaHttpRequest.kt b/mockzilla/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/KtorMockzillaHttpRequest.kt index 1b2fd1bc5..9b75c7677 100644 --- a/mockzilla/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/KtorMockzillaHttpRequest.kt +++ b/mockzilla/src/commonMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/KtorMockzillaHttpRequest.kt @@ -7,7 +7,7 @@ import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* -class KtorMockzillaHttpRequest internal constructor( +internal class KtorMockzillaHttpRequest( private val call: ApplicationCall, override val method: HttpMethod ) : MockzillaHttpRequest { diff --git a/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.kt b/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.kt index f733ff2c5..1b4eafbcc 100644 --- a/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.kt +++ b/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.kt @@ -1,3 +1,5 @@ +// This file intentionally breaks the convention and isn't named `Mockzilla.ios.kt` since +// We want the Swift interop to expose MockzillaKt as it's type package com.apadmi.mockzilla.lib import com.apadmi.mockzilla.lib.internal.PlatformConfig @@ -13,10 +15,10 @@ import com.apadmi.mockzilla.lib.models.PortConflictException import kotlinx.coroutines.runBlocking /** - * Internal method to start the Mockzilla server. Consumer apps should prefer using the top-level - * `startMockzilla()` function to avoid breaking changes. + * Starts the Mockzilla server. * * @param config The config with which to initialise mockzilla. + * @throws PortConflictException if the port specified in [config] is already in use. */ @Throws(PortConflictException::class) fun startMockzilla(config: MockzillaConfig): MockzillaRuntimeParams = runBlocking { @@ -37,9 +39,8 @@ fun startMockzilla(config: MockzillaConfig): MockzillaRuntimeParams = runBlockin } /** - * Stops the Mockzilla server, + * Stops the running Mockzilla server. * - * @return */ actual fun stopMockzilla() = runBlocking { stopServer() diff --git a/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/discovery/ZeroConfDiscoveryServiceImpl.kt b/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/discovery/ZeroConfDiscoveryServiceImpl.kt index 48023c695..012fc9cba 100644 --- a/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/discovery/ZeroConfDiscoveryServiceImpl.kt +++ b/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/discovery/ZeroConfDiscoveryServiceImpl.kt @@ -29,7 +29,7 @@ import kotlinx.cinterop.ptr import kotlinx.cinterop.refTo import kotlinx.cinterop.value -class ZeroConfDiscoveryServiceImpl( +internal class ZeroConfDiscoveryServiceImpl( private val logger: Logger, private val keychainSettings: KeychainSettings ) : ZeroConfDiscoveryService { diff --git a/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/persistance/KeychainSettings.kt b/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/persistance/KeychainSettings.kt index 6ce9b3354..6c84f19d3 100644 --- a/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/persistance/KeychainSettings.kt +++ b/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/persistance/KeychainSettings.kt @@ -93,7 +93,7 @@ import kotlinx.cinterop.value * be used as the service name. It's also possible to pass custom key-value pairs as attributes that will be added to * every key, if the default behavior does not fit your needs. */ -class KeychainSettings(vararg defaultProperties: Pair) { +internal class KeychainSettings(vararg defaultProperties: Pair) { @OptIn(ExperimentalForeignApi::class) private val defaultProperties = mapOf(kSecClass to kSecClassGenericPassword) + mapOf(*defaultProperties) diff --git a/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrorUtils.kt b/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrorUtils.kt index ea73aca21..e82bb3521 100644 --- a/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrorUtils.kt +++ b/mockzilla/src/iosMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrorUtils.kt @@ -4,4 +4,4 @@ package com.apadmi.mockzilla.lib.internal.utils import io.ktor.utils.io.errors.PosixException.AddressAlreadyInUseException as KtorAddressAlreadyInUseException -actual typealias AddressAlreadyInUseException = KtorAddressAlreadyInUseException +internal actual typealias AddressAlreadyInUseException = KtorAddressAlreadyInUseException diff --git a/mockzilla/src/iosTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.kt b/mockzilla/src/iosTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.ios.kt similarity index 100% rename from mockzilla/src/iosTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.kt rename to mockzilla/src/iosTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.ios.kt diff --git a/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/JsMockzilla.kt b/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.js.kt similarity index 97% rename from mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/JsMockzilla.kt rename to mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.js.kt index f871d994a..e58e864b0 100644 --- a/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/JsMockzilla.kt +++ b/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.js.kt @@ -59,6 +59,9 @@ suspend fun startMockzilla( } } +/** + * Stops the running Mockzilla server. + */ @OptIn(DelicateCoroutinesApi::class) actual fun stopMockzilla() = GlobalScope.promise { stopServer() diff --git a/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/msw/MswBridge.kt b/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/msw/MswBridge.kt index 2ca5618d1..8beb844f2 100644 --- a/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/msw/MswBridge.kt +++ b/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/msw/MswBridge.kt @@ -1,21 +1,25 @@ package com.apadmi.mockzilla.lib.internal.msw +import com.apadmi.mockzilla.lib.InternalMockzillaApi import org.w3c.fetch.Request import org.w3c.fetch.Response import kotlin.js.Promise +@InternalMockzillaApi @JsModule("msw/browser") @JsNonModule external object MswBrowser { fun setupWorker(vararg handlers: RestHandler): ServiceWorkerInstance } +@InternalMockzillaApi @JsModule("msw") @JsNonModule external object Msw { val http: Rest } +@InternalMockzillaApi external object Rest { fun all( path: String, @@ -53,21 +57,25 @@ external object Rest { ): RestHandler } +@InternalMockzillaApi external interface ResponseResolverInfo { val request: Request val requestId: String } +@InternalMockzillaApi external interface DefaultContext { fun status(status: Int): dynamic fun json(body: Any): dynamic fun text(body: String): dynamic } +@InternalMockzillaApi external interface StartServiceWorkerOptions { var onUnhandledRequest: String } +@InternalMockzillaApi external interface ServiceWorkerInstance { val context: ServiceWorkerContext fun start(options: StartServiceWorkerOptions): Promise @@ -76,8 +84,10 @@ external interface ServiceWorkerInstance { fun stop(): Promise } +@InternalMockzillaApi external interface ServiceWorkerContext { val isMockingEnabled: Boolean } +@InternalMockzillaApi external interface RestHandler diff --git a/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrotUtils.kt b/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrotUtils.kt index e009a8b10..6326aac25 100644 --- a/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrotUtils.kt +++ b/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrotUtils.kt @@ -4,7 +4,7 @@ package com.apadmi.mockzilla.lib.internal.utils import kotlinx.io.IOException -actual typealias AddressAlreadyInUseException = DummyException +internal actual typealias AddressAlreadyInUseException = DummyException // This will never actually happen since on JS multiple addresses aren't used -class DummyException : IOException() +internal class DummyException : IOException() diff --git a/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/JsMockzillaRequest.kt b/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/JsMockzillaRequest.kt index de33aa8e7..c15fadb67 100644 --- a/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/JsMockzillaRequest.kt +++ b/mockzilla/src/jsMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/JsMockzillaRequest.kt @@ -1,5 +1,6 @@ package com.apadmi.mockzilla.lib.internal.utils +import com.apadmi.mockzilla.lib.InternalMockzillaApi import com.apadmi.mockzilla.lib.models.MockzillaHttpRequest import io.ktor.http.HttpMethod @@ -11,6 +12,7 @@ import kotlinx.coroutines.await import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +@InternalMockzillaApi class JsMockzillaRequest(private val jsRequest: JsRequest) : MockzillaHttpRequest { private val lock = Mutex() private var bodyCache: String? = null diff --git a/mockzilla/src/jsTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.kt b/mockzilla/src/jsTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.js.kt similarity index 100% rename from mockzilla/src/jsTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.kt rename to mockzilla/src/jsTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.js.kt diff --git a/mockzilla/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/JvmMockzilla.kt b/mockzilla/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.jvm.kt similarity index 96% rename from mockzilla/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/JvmMockzilla.kt rename to mockzilla/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.jvm.kt index 9c5ec4b0c..6879f7d34 100644 --- a/mockzilla/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/JvmMockzilla.kt +++ b/mockzilla/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/Mockzilla.jvm.kt @@ -1,3 +1,5 @@ +@file:JvmName("JvmMockzillaKt") + package com.apadmi.mockzilla.lib import com.apadmi.mockzilla.BuildKonfig @@ -56,9 +58,8 @@ fun startMockzilla( } /** - * Stops the Mockzilla server, + * Stops the running Mockzilla server. * - * @return */ actual fun stopMockzilla() = runBlocking { stopServer() diff --git a/mockzilla/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrotUtils.kt b/mockzilla/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrotUtils.kt index de31fa6de..12d602124 100644 --- a/mockzilla/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrotUtils.kt +++ b/mockzilla/src/jvmMain/kotlin/com/apadmi/mockzilla/lib/internal/utils/ErrotUtils.kt @@ -2,4 +2,4 @@ package com.apadmi.mockzilla.lib.internal.utils -actual typealias AddressAlreadyInUseException = java.net.BindException +internal actual typealias AddressAlreadyInUseException = java.net.BindException diff --git a/mockzilla/src/jvmTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.kt b/mockzilla/src/jvmTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.jvm.kt similarity index 91% rename from mockzilla/src/jvmTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.kt rename to mockzilla/src/jvmTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.jvm.kt index bb81737b3..6bc9bf47e 100644 --- a/mockzilla/src/jvmTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.kt +++ b/mockzilla/src/jvmTest/kotlin/com/apadmi/mockzilla/testutils/FileUtils.jvm.kt @@ -1,3 +1,5 @@ +@file:JvmName("FileUtilsKt") + package com.apadmi.mockzilla.testutils import java.io.File diff --git a/samples/demo-android/src/main/res/xml/network_security_config.xml b/samples/demo-android/src/main/res/xml/network_security_config.xml index 48c1dfd62..adf44a108 100644 --- a/samples/demo-android/src/main/res/xml/network_security_config.xml +++ b/samples/demo-android/src/main/res/xml/network_security_config.xml @@ -1,3 +1,4 @@ + diff --git a/samples/demo-kmm/androidApp/src/main/AndroidManifest.xml b/samples/demo-kmm/androidApp/src/main/AndroidManifest.xml index e6ef83c4a..f59d83555 100644 --- a/samples/demo-kmm/androidApp/src/main/AndroidManifest.xml +++ b/samples/demo-kmm/androidApp/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ android:allowBackup="false" android:supportsRtl="true" android:label="demo-mockzilla-kmm" + android:networkSecurityConfig="@xml/network_security_config" android:theme="@style/AppTheme"> + + + + + + + + + + localhost + 127.0.0.1 + + \ No newline at end of file