moai-adk 0.35.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of moai-adk might be problematic. Click here for more details.
- moai_adk/__init__.py +10 -0
- moai_adk/__main__.py +199 -0
- moai_adk/cli/__init__.py +6 -0
- moai_adk/cli/commands/__init__.py +17 -0
- moai_adk/cli/commands/analyze.py +116 -0
- moai_adk/cli/commands/doctor.py +272 -0
- moai_adk/cli/commands/init.py +372 -0
- moai_adk/cli/commands/language.py +248 -0
- moai_adk/cli/commands/status.py +104 -0
- moai_adk/cli/commands/update.py +2686 -0
- moai_adk/cli/main.py +13 -0
- moai_adk/cli/prompts/__init__.py +5 -0
- moai_adk/cli/prompts/init_prompts.py +219 -0
- moai_adk/cli/spec_status.py +263 -0
- moai_adk/cli/ui/__init__.py +44 -0
- moai_adk/cli/ui/progress.py +422 -0
- moai_adk/cli/ui/prompts.py +389 -0
- moai_adk/cli/ui/theme.py +129 -0
- moai_adk/cli/worktree/__init__.py +27 -0
- moai_adk/cli/worktree/__main__.py +31 -0
- moai_adk/cli/worktree/cli.py +683 -0
- moai_adk/cli/worktree/exceptions.py +89 -0
- moai_adk/cli/worktree/manager.py +493 -0
- moai_adk/cli/worktree/models.py +65 -0
- moai_adk/cli/worktree/registry.py +422 -0
- moai_adk/core/PHASE2_OPTIMIZATIONS.md +467 -0
- moai_adk/core/__init__.py +1 -0
- moai_adk/core/analysis/__init__.py +9 -0
- moai_adk/core/analysis/session_analyzer.py +400 -0
- moai_adk/core/claude_integration.py +393 -0
- moai_adk/core/command_helpers.py +270 -0
- moai_adk/core/comprehensive_monitoring_system.py +1183 -0
- moai_adk/core/config/__init__.py +19 -0
- moai_adk/core/config/auto_spec_config.py +340 -0
- moai_adk/core/config/migration.py +244 -0
- moai_adk/core/config/unified.py +436 -0
- moai_adk/core/context_manager.py +273 -0
- moai_adk/core/diagnostics/__init__.py +19 -0
- moai_adk/core/diagnostics/slash_commands.py +159 -0
- moai_adk/core/enterprise_features.py +1404 -0
- moai_adk/core/error_recovery_system.py +1902 -0
- moai_adk/core/event_driven_hook_system.py +1371 -0
- moai_adk/core/git/__init__.py +31 -0
- moai_adk/core/git/branch.py +25 -0
- moai_adk/core/git/branch_manager.py +129 -0
- moai_adk/core/git/checkpoint.py +134 -0
- moai_adk/core/git/commit.py +67 -0
- moai_adk/core/git/conflict_detector.py +413 -0
- moai_adk/core/git/event_detector.py +79 -0
- moai_adk/core/git/manager.py +216 -0
- moai_adk/core/hooks/post_tool_auto_spec_completion.py +901 -0
- moai_adk/core/input_validation_middleware.py +1006 -0
- moai_adk/core/integration/__init__.py +22 -0
- moai_adk/core/integration/engine.py +157 -0
- moai_adk/core/integration/integration_tester.py +226 -0
- moai_adk/core/integration/models.py +88 -0
- moai_adk/core/integration/utils.py +211 -0
- moai_adk/core/issue_creator.py +305 -0
- moai_adk/core/jit_context_loader.py +956 -0
- moai_adk/core/jit_enhanced_hook_manager.py +1987 -0
- moai_adk/core/language_config.py +202 -0
- moai_adk/core/language_config_resolver.py +572 -0
- moai_adk/core/language_validator.py +543 -0
- moai_adk/core/mcp/setup.py +116 -0
- moai_adk/core/merge/__init__.py +9 -0
- moai_adk/core/merge/analyzer.py +605 -0
- moai_adk/core/migration/__init__.py +18 -0
- moai_adk/core/migration/alfred_to_moai_migrator.py +383 -0
- moai_adk/core/migration/backup_manager.py +277 -0
- moai_adk/core/migration/custom_element_scanner.py +358 -0
- moai_adk/core/migration/file_migrator.py +209 -0
- moai_adk/core/migration/interactive_checkbox_ui.py +488 -0
- moai_adk/core/migration/selective_restorer.py +470 -0
- moai_adk/core/migration/template_utils.py +74 -0
- moai_adk/core/migration/user_selection_ui.py +338 -0
- moai_adk/core/migration/version_detector.py +139 -0
- moai_adk/core/migration/version_migrator.py +228 -0
- moai_adk/core/performance/__init__.py +6 -0
- moai_adk/core/performance/cache_system.py +316 -0
- moai_adk/core/performance/parallel_processor.py +116 -0
- moai_adk/core/phase_optimized_hook_scheduler.py +879 -0
- moai_adk/core/project/__init__.py +1 -0
- moai_adk/core/project/backup_utils.py +70 -0
- moai_adk/core/project/checker.py +300 -0
- moai_adk/core/project/detector.py +293 -0
- moai_adk/core/project/initializer.py +387 -0
- moai_adk/core/project/phase_executor.py +716 -0
- moai_adk/core/project/validator.py +139 -0
- moai_adk/core/quality/__init__.py +6 -0
- moai_adk/core/quality/trust_checker.py +377 -0
- moai_adk/core/quality/validators/__init__.py +6 -0
- moai_adk/core/quality/validators/base_validator.py +19 -0
- moai_adk/core/realtime_monitoring_dashboard.py +1724 -0
- moai_adk/core/robust_json_parser.py +611 -0
- moai_adk/core/rollback_manager.py +918 -0
- moai_adk/core/session_manager.py +651 -0
- moai_adk/core/skill_loading_system.py +579 -0
- moai_adk/core/spec/confidence_scoring.py +680 -0
- moai_adk/core/spec/ears_template_engine.py +1247 -0
- moai_adk/core/spec/quality_validator.py +687 -0
- moai_adk/core/spec_status_manager.py +478 -0
- moai_adk/core/template/__init__.py +7 -0
- moai_adk/core/template/backup.py +174 -0
- moai_adk/core/template/config.py +191 -0
- moai_adk/core/template/languages.py +43 -0
- moai_adk/core/template/merger.py +233 -0
- moai_adk/core/template/processor.py +1200 -0
- moai_adk/core/template_engine.py +310 -0
- moai_adk/core/template_variable_synchronizer.py +417 -0
- moai_adk/core/unified_permission_manager.py +745 -0
- moai_adk/core/user_behavior_analytics.py +851 -0
- moai_adk/core/version_sync.py +429 -0
- moai_adk/foundation/__init__.py +56 -0
- moai_adk/foundation/backend.py +1027 -0
- moai_adk/foundation/database.py +1115 -0
- moai_adk/foundation/devops.py +1585 -0
- moai_adk/foundation/ears.py +431 -0
- moai_adk/foundation/frontend.py +870 -0
- moai_adk/foundation/git/commit_templates.py +557 -0
- moai_adk/foundation/git.py +376 -0
- moai_adk/foundation/langs.py +484 -0
- moai_adk/foundation/ml_ops.py +1162 -0
- moai_adk/foundation/testing.py +1524 -0
- moai_adk/foundation/trust/trust_principles.py +676 -0
- moai_adk/foundation/trust/validation_checklist.py +1573 -0
- moai_adk/project/__init__.py +0 -0
- moai_adk/project/configuration.py +1084 -0
- moai_adk/project/documentation.py +566 -0
- moai_adk/project/schema.py +447 -0
- moai_adk/statusline/__init__.py +38 -0
- moai_adk/statusline/alfred_detector.py +105 -0
- moai_adk/statusline/config.py +376 -0
- moai_adk/statusline/enhanced_output_style_detector.py +372 -0
- moai_adk/statusline/git_collector.py +190 -0
- moai_adk/statusline/main.py +322 -0
- moai_adk/statusline/metrics_tracker.py +78 -0
- moai_adk/statusline/renderer.py +343 -0
- moai_adk/statusline/update_checker.py +129 -0
- moai_adk/statusline/version_reader.py +741 -0
- moai_adk/templates/.claude/agents/moai/ai-nano-banana.md +714 -0
- moai_adk/templates/.claude/agents/moai/builder-agent.md +474 -0
- moai_adk/templates/.claude/agents/moai/builder-command.md +1172 -0
- moai_adk/templates/.claude/agents/moai/builder-plugin.md +637 -0
- moai_adk/templates/.claude/agents/moai/builder-skill.md +666 -0
- moai_adk/templates/.claude/agents/moai/expert-backend.md +899 -0
- moai_adk/templates/.claude/agents/moai/expert-database.md +777 -0
- moai_adk/templates/.claude/agents/moai/expert-debug.md +401 -0
- moai_adk/templates/.claude/agents/moai/expert-devops.md +720 -0
- moai_adk/templates/.claude/agents/moai/expert-frontend.md +734 -0
- moai_adk/templates/.claude/agents/moai/expert-performance.md +657 -0
- moai_adk/templates/.claude/agents/moai/expert-security.md +513 -0
- moai_adk/templates/.claude/agents/moai/expert-testing.md +733 -0
- moai_adk/templates/.claude/agents/moai/expert-uiux.md +1041 -0
- moai_adk/templates/.claude/agents/moai/manager-claude-code.md +432 -0
- moai_adk/templates/.claude/agents/moai/manager-docs.md +573 -0
- moai_adk/templates/.claude/agents/moai/manager-git.md +1060 -0
- moai_adk/templates/.claude/agents/moai/manager-project.md +891 -0
- moai_adk/templates/.claude/agents/moai/manager-quality.md +624 -0
- moai_adk/templates/.claude/agents/moai/manager-spec.md +809 -0
- moai_adk/templates/.claude/agents/moai/manager-strategy.md +780 -0
- moai_adk/templates/.claude/agents/moai/manager-tdd.md +784 -0
- moai_adk/templates/.claude/agents/moai/mcp-context7.md +458 -0
- moai_adk/templates/.claude/agents/moai/mcp-figma.md +1607 -0
- moai_adk/templates/.claude/agents/moai/mcp-notion.md +789 -0
- moai_adk/templates/.claude/agents/moai/mcp-playwright.md +469 -0
- moai_adk/templates/.claude/agents/moai/mcp-sequential-thinking.md +1032 -0
- moai_adk/templates/.claude/commands/moai/0-project.md +1386 -0
- moai_adk/templates/.claude/commands/moai/1-plan.md +1427 -0
- moai_adk/templates/.claude/commands/moai/2-run.md +943 -0
- moai_adk/templates/.claude/commands/moai/3-sync.md +1324 -0
- moai_adk/templates/.claude/commands/moai/9-feedback.md +314 -0
- moai_adk/templates/.claude/hooks/__init__.py +8 -0
- moai_adk/templates/.claude/hooks/moai/__init__.py +8 -0
- moai_adk/templates/.claude/hooks/moai/lib/__init__.py +85 -0
- moai_adk/templates/.claude/hooks/moai/lib/checkpoint.py +244 -0
- moai_adk/templates/.claude/hooks/moai/lib/common.py +131 -0
- moai_adk/templates/.claude/hooks/moai/lib/config_manager.py +446 -0
- moai_adk/templates/.claude/hooks/moai/lib/config_validator.py +639 -0
- moai_adk/templates/.claude/hooks/moai/lib/example_config.json +104 -0
- moai_adk/templates/.claude/hooks/moai/lib/git_operations_manager.py +590 -0
- moai_adk/templates/.claude/hooks/moai/lib/language_validator.py +317 -0
- moai_adk/templates/.claude/hooks/moai/lib/models.py +102 -0
- moai_adk/templates/.claude/hooks/moai/lib/path_utils.py +28 -0
- moai_adk/templates/.claude/hooks/moai/lib/project.py +768 -0
- moai_adk/templates/.claude/hooks/moai/lib/test_hooks_improvements.py +443 -0
- moai_adk/templates/.claude/hooks/moai/lib/timeout.py +160 -0
- moai_adk/templates/.claude/hooks/moai/lib/unified_timeout_manager.py +530 -0
- moai_adk/templates/.claude/hooks/moai/session_end__auto_cleanup.py +862 -0
- moai_adk/templates/.claude/hooks/moai/session_start__show_project_info.py +1083 -0
- moai_adk/templates/.claude/output-styles/moai/r2d2.md +560 -0
- moai_adk/templates/.claude/output-styles/moai/yoda.md +359 -0
- moai_adk/templates/.claude/settings.json +172 -0
- moai_adk/templates/.claude/skills/moai-ai-nano-banana/SKILL.md +307 -0
- moai_adk/templates/.claude/skills/moai-ai-nano-banana/examples.md +431 -0
- moai_adk/templates/.claude/skills/moai-ai-nano-banana/scripts/batch_generate.py +560 -0
- moai_adk/templates/.claude/skills/moai-ai-nano-banana/scripts/generate_image.py +362 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/SKILL.md +249 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/examples.md +406 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/README.md +44 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/api-documentation.md +130 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/code-documentation.md +152 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/multi-format-output.md +178 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/user-guides.md +147 -0
- moai_adk/templates/.claude/skills/moai-docs-generation/reference.md +328 -0
- moai_adk/templates/.claude/skills/moai-domain-backend/SKILL.md +320 -0
- moai_adk/templates/.claude/skills/moai-domain-backend/examples.md +718 -0
- moai_adk/templates/.claude/skills/moai-domain-backend/reference.md +464 -0
- moai_adk/templates/.claude/skills/moai-domain-database/SKILL.md +323 -0
- moai_adk/templates/.claude/skills/moai-domain-database/examples.md +830 -0
- moai_adk/templates/.claude/skills/moai-domain-database/modules/README.md +53 -0
- moai_adk/templates/.claude/skills/moai-domain-database/modules/mongodb.md +231 -0
- moai_adk/templates/.claude/skills/moai-domain-database/modules/postgresql.md +169 -0
- moai_adk/templates/.claude/skills/moai-domain-database/modules/redis.md +262 -0
- moai_adk/templates/.claude/skills/moai-domain-database/reference.md +545 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/SKILL.md +497 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/examples.md +968 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/reference.md +664 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/SKILL.md +455 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/examples.md +560 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/modules/accessibility-wcag.md +260 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/modules/component-architecture.md +228 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/modules/icon-libraries.md +401 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/modules/theming-system.md +373 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/reference.md +243 -0
- moai_adk/templates/.claude/skills/moai-formats-data/SKILL.md +492 -0
- moai_adk/templates/.claude/skills/moai-formats-data/examples.md +804 -0
- moai_adk/templates/.claude/skills/moai-formats-data/modules/README.md +98 -0
- moai_adk/templates/.claude/skills/moai-formats-data/modules/SKILL-MODULARIZATION-TEMPLATE.md +278 -0
- moai_adk/templates/.claude/skills/moai-formats-data/modules/caching-performance.md +459 -0
- moai_adk/templates/.claude/skills/moai-formats-data/modules/data-validation.md +485 -0
- moai_adk/templates/.claude/skills/moai-formats-data/modules/json-optimization.md +374 -0
- moai_adk/templates/.claude/skills/moai-formats-data/modules/toon-encoding.md +308 -0
- moai_adk/templates/.claude/skills/moai-formats-data/reference.md +585 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/SKILL.md +202 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/examples.md +732 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/best-practices-checklist.md +616 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-custom-slash-commands-official.md +729 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-hooks-official.md +560 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-iam-official.md +635 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-memory-official.md +543 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-settings-official.md +663 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-skills-official.md +113 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-sub-agents-official.md +238 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/complete-configuration-guide.md +175 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/skill-examples.md +1674 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/skill-formatting-guide.md +729 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/sub-agents/sub-agent-examples.md +1513 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/sub-agents/sub-agent-formatting-guide.md +1086 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/sub-agents/sub-agent-integration-patterns.md +1100 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference.md +209 -0
- moai_adk/templates/.claude/skills/moai-foundation-context/SKILL.md +441 -0
- moai_adk/templates/.claude/skills/moai-foundation-context/examples.md +1048 -0
- moai_adk/templates/.claude/skills/moai-foundation-context/reference.md +246 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/SKILL.md +420 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/examples.md +358 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/README.md +296 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/agents-reference.md +359 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/commands-reference.md +432 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/delegation-patterns.md +757 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/execution-rules.md +687 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/modular-system.md +665 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/progressive-disclosure.md +649 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/spec-first-tdd.md +864 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/token-optimization.md +708 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/trust-5-framework.md +981 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/reference.md +478 -0
- moai_adk/templates/.claude/skills/moai-foundation-philosopher/SKILL.md +315 -0
- moai_adk/templates/.claude/skills/moai-foundation-philosopher/examples.md +228 -0
- moai_adk/templates/.claude/skills/moai-foundation-philosopher/modules/assumption-matrix.md +80 -0
- moai_adk/templates/.claude/skills/moai-foundation-philosopher/modules/cognitive-bias.md +199 -0
- moai_adk/templates/.claude/skills/moai-foundation-philosopher/modules/first-principles.md +140 -0
- moai_adk/templates/.claude/skills/moai-foundation-philosopher/modules/trade-off-analysis.md +154 -0
- moai_adk/templates/.claude/skills/moai-foundation-philosopher/reference.md +157 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/SKILL.md +364 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/examples.md +1232 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/modules/best-practices.md +261 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/modules/integration-patterns.md +194 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/modules/proactive-analysis.md +229 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/modules/trust5-validation.md +169 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/reference.md +1266 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/scripts/quality-gate.sh +668 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/templates/github-actions-quality.yml +481 -0
- moai_adk/templates/.claude/skills/moai-foundation-quality/templates/quality-config.yaml +519 -0
- moai_adk/templates/.claude/skills/moai-lang-cpp/SKILL.md +649 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/SKILL.md +478 -0
- moai_adk/templates/.claude/skills/moai-lang-elixir/SKILL.md +612 -0
- moai_adk/templates/.claude/skills/moai-lang-flutter/SKILL.md +477 -0
- moai_adk/templates/.claude/skills/moai-lang-flutter/examples.md +1090 -0
- moai_adk/templates/.claude/skills/moai-lang-flutter/reference.md +686 -0
- moai_adk/templates/.claude/skills/moai-lang-go/SKILL.md +376 -0
- moai_adk/templates/.claude/skills/moai-lang-go/examples.md +919 -0
- moai_adk/templates/.claude/skills/moai-lang-go/reference.md +737 -0
- moai_adk/templates/.claude/skills/moai-lang-java/SKILL.md +385 -0
- moai_adk/templates/.claude/skills/moai-lang-java/examples.md +864 -0
- moai_adk/templates/.claude/skills/moai-lang-java/reference.md +291 -0
- moai_adk/templates/.claude/skills/moai-lang-kotlin/SKILL.md +382 -0
- moai_adk/templates/.claude/skills/moai-lang-kotlin/examples.md +1006 -0
- moai_adk/templates/.claude/skills/moai-lang-kotlin/reference.md +562 -0
- moai_adk/templates/.claude/skills/moai-lang-php/SKILL.md +644 -0
- moai_adk/templates/.claude/skills/moai-lang-python/SKILL.md +481 -0
- moai_adk/templates/.claude/skills/moai-lang-python/examples.md +977 -0
- moai_adk/templates/.claude/skills/moai-lang-python/reference.md +804 -0
- moai_adk/templates/.claude/skills/moai-lang-r/SKILL.md +579 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/SKILL.md +687 -0
- moai_adk/templates/.claude/skills/moai-lang-rust/SKILL.md +372 -0
- moai_adk/templates/.claude/skills/moai-lang-rust/examples.md +659 -0
- moai_adk/templates/.claude/skills/moai-lang-rust/reference.md +504 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/SKILL.md +497 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/examples.md +633 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/reference.md +423 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/SKILL.md +497 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/examples.md +918 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/reference.md +672 -0
- moai_adk/templates/.claude/skills/moai-lang-typescript/SKILL.md +368 -0
- moai_adk/templates/.claude/skills/moai-lang-typescript/examples.md +1089 -0
- moai_adk/templates/.claude/skills/moai-lang-typescript/reference.md +731 -0
- moai_adk/templates/.claude/skills/moai-library-mermaid/SKILL.md +300 -0
- moai_adk/templates/.claude/skills/moai-library-mermaid/advanced-patterns.md +465 -0
- moai_adk/templates/.claude/skills/moai-library-mermaid/examples.md +270 -0
- moai_adk/templates/.claude/skills/moai-library-mermaid/optimization.md +440 -0
- moai_adk/templates/.claude/skills/moai-library-mermaid/reference.md +228 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/SKILL.md +319 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/advanced-patterns.md +336 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/examples.md +592 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/advanced-deployment-patterns.md +182 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/advanced-patterns.md +17 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/configuration.md +57 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/content-architecture-optimization.md +162 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/deployment.md +52 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/framework-core-configuration.md +186 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/i18n-setup.md +55 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/mdx-components.md +52 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/optimization.md +303 -0
- moai_adk/templates/.claude/skills/moai-library-nextra/reference.md +379 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/SKILL.md +372 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/examples.md +575 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/modules/advanced-patterns.md +394 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/modules/optimization.md +278 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/modules/shadcn-components.md +457 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/modules/shadcn-theming.md +373 -0
- moai_adk/templates/.claude/skills/moai-library-shadcn/reference.md +74 -0
- moai_adk/templates/.claude/skills/moai-mcp-figma/SKILL.md +402 -0
- moai_adk/templates/.claude/skills/moai-mcp-figma/advanced-patterns.md +607 -0
- moai_adk/templates/.claude/skills/moai-mcp-notion/SKILL.md +300 -0
- moai_adk/templates/.claude/skills/moai-mcp-notion/advanced-patterns.md +537 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/SKILL.md +291 -0
- moai_adk/templates/.claude/skills/moai-platform-clerk/SKILL.md +390 -0
- moai_adk/templates/.claude/skills/moai-platform-convex/SKILL.md +398 -0
- moai_adk/templates/.claude/skills/moai-platform-firebase-auth/SKILL.md +379 -0
- moai_adk/templates/.claude/skills/moai-platform-firestore/SKILL.md +358 -0
- moai_adk/templates/.claude/skills/moai-platform-neon/SKILL.md +467 -0
- moai_adk/templates/.claude/skills/moai-platform-railway/SKILL.md +377 -0
- moai_adk/templates/.claude/skills/moai-platform-supabase/SKILL.md +466 -0
- moai_adk/templates/.claude/skills/moai-platform-vercel/SKILL.md +482 -0
- moai_adk/templates/.claude/skills/moai-plugin-builder/SKILL.md +474 -0
- moai_adk/templates/.claude/skills/moai-plugin-builder/examples.md +621 -0
- moai_adk/templates/.claude/skills/moai-plugin-builder/migration.md +341 -0
- moai_adk/templates/.claude/skills/moai-plugin-builder/reference.md +463 -0
- moai_adk/templates/.claude/skills/moai-plugin-builder/validation.md +373 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/SKILL.md +275 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/adaptive-mfa.md +233 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/akamai-integration.md +215 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/application-credentials.md +280 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/attack-protection-log-events.md +225 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/attack-protection-overview.md +140 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/bot-detection.md +144 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/breached-password-detection.md +187 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/brute-force-protection.md +189 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/certifications.md +282 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/compliance-overview.md +263 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/continuous-session-protection.md +307 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/customize-mfa.md +178 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/dpop-implementation.md +283 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/fapi-implementation.md +259 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/gdpr-compliance.md +313 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/guardian-configuration.md +269 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/highly-regulated-identity.md +272 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/jwt-fundamentals.md +248 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/mdl-verification.md +211 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/mfa-api-management.md +278 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/mfa-factors.md +226 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/mfa-overview.md +174 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/mtls-sender-constraining.md +316 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/ropg-flow-mfa.md +217 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/security-center.md +325 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/security-guidance.md +277 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/state-parameters.md +178 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/step-up-authentication.md +251 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/suspicious-ip-throttling.md +240 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/tenant-access-control.md +180 -0
- moai_adk/templates/.claude/skills/moai-security-auth0/modules/webauthn-fido.md +235 -0
- moai_adk/templates/.claude/skills/moai-workflow-jit-docs/SKILL.md +449 -0
- moai_adk/templates/.claude/skills/moai-workflow-jit-docs/advanced-patterns.md +379 -0
- moai_adk/templates/.claude/skills/moai-workflow-jit-docs/examples.md +544 -0
- moai_adk/templates/.claude/skills/moai-workflow-jit-docs/optimization.md +286 -0
- moai_adk/templates/.claude/skills/moai-workflow-jit-docs/reference.md +307 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/README.md +190 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/SKILL.md +390 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/__init__.py +520 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/complete_workflow_demo_fixed.py +574 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/complete_project_setup.py +317 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/complete_workflow_demo.py +663 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/config-migration-example.json +190 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/question-examples.json +175 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/quick_start.py +196 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/examples.md +547 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/__init__.py +17 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/advanced-patterns.md +158 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/ask_user_integration.py +340 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/batch_questions.py +713 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/config_manager.py +538 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/documentation_manager.py +1336 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/language_initializer.py +730 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/migration_manager.py +608 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/template_optimizer.py +1005 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/reference.md +275 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/schemas/config-schema.json +316 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/schemas/tab_schema.json +1434 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/config-template.json +71 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/doc-templates/product-template.md +44 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/doc-templates/structure-template.md +48 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/doc-templates/tech-template.md +92 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/config-manager-setup.json +109 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/language-initializer.json +228 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/menu-project-config.json +130 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/project-batch-questions.json +97 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/spec-workflow-setup.json +150 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/test_integration_simple.py +436 -0
- moai_adk/templates/.claude/skills/moai-workflow-spec/SKILL.md +534 -0
- moai_adk/templates/.claude/skills/moai-workflow-spec/examples.md +900 -0
- moai_adk/templates/.claude/skills/moai-workflow-spec/reference.md +704 -0
- moai_adk/templates/.claude/skills/moai-workflow-templates/SKILL.md +377 -0
- moai_adk/templates/.claude/skills/moai-workflow-templates/examples.md +552 -0
- moai_adk/templates/.claude/skills/moai-workflow-templates/modules/code-templates.md +124 -0
- moai_adk/templates/.claude/skills/moai-workflow-templates/modules/feedback-templates.md +100 -0
- moai_adk/templates/.claude/skills/moai-workflow-templates/modules/template-optimizer.md +138 -0
- moai_adk/templates/.claude/skills/moai-workflow-templates/reference.md +346 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/LICENSE.txt +202 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/SKILL.md +456 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/advanced-patterns.md +576 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/examples/ai-powered-testing.py +294 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/examples/console_logging.py +35 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/examples/element_discovery.py +40 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/examples/static_html_automation.py +34 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/examples.md +672 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/README.md +220 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/ai-debugging.md +845 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review.md +1416 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/performance-optimization.md +1234 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/smart-refactoring.md +1243 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/tdd-context7.md +1260 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/optimization.md +505 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/reference/playwright-best-practices.md +57 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/reference.md +440 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/scripts/with_server.py +218 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/templates/alfred-integration.md +376 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/workflows/enterprise-testing-workflow.py +571 -0
- moai_adk/templates/.claude/skills/moai-worktree/SKILL.md +411 -0
- moai_adk/templates/.claude/skills/moai-worktree/examples.md +606 -0
- moai_adk/templates/.claude/skills/moai-worktree/modules/integration-patterns.md +982 -0
- moai_adk/templates/.claude/skills/moai-worktree/modules/parallel-development.md +778 -0
- moai_adk/templates/.claude/skills/moai-worktree/modules/worktree-commands.md +646 -0
- moai_adk/templates/.claude/skills/moai-worktree/modules/worktree-management.md +782 -0
- moai_adk/templates/.claude/skills/moai-worktree/reference.md +357 -0
- moai_adk/templates/.git-hooks/pre-commit +128 -0
- moai_adk/templates/.git-hooks/pre-push +365 -0
- moai_adk/templates/.github/workflows/ci-universal.yml +513 -0
- moai_adk/templates/.github/workflows/security-secrets-check.yml +179 -0
- moai_adk/templates/.github/workflows/spec-issue-sync.yml +337 -0
- moai_adk/templates/.gitignore +222 -0
- moai_adk/templates/.mcp.json +13 -0
- moai_adk/templates/.moai/config/config.yaml +58 -0
- moai_adk/templates/.moai/config/questions/_schema.yaml +174 -0
- moai_adk/templates/.moai/config/questions/tab0-init.yaml +251 -0
- moai_adk/templates/.moai/config/questions/tab1-user.yaml +107 -0
- moai_adk/templates/.moai/config/questions/tab2-project.yaml +79 -0
- moai_adk/templates/.moai/config/questions/tab3-git.yaml +632 -0
- moai_adk/templates/.moai/config/questions/tab4-quality.yaml +182 -0
- moai_adk/templates/.moai/config/questions/tab5-system.yaml +96 -0
- moai_adk/templates/.moai/config/sections/git-strategy.yaml +116 -0
- moai_adk/templates/.moai/config/sections/language.yaml +11 -0
- moai_adk/templates/.moai/config/sections/project.yaml +13 -0
- moai_adk/templates/.moai/config/sections/quality.yaml +17 -0
- moai_adk/templates/.moai/config/sections/system.yaml +24 -0
- moai_adk/templates/.moai/config/sections/user.yaml +5 -0
- moai_adk/templates/.moai/config/statusline-config.yaml +92 -0
- moai_adk/templates/.moai/scripts/setup-glm.py +136 -0
- moai_adk/templates/CLAUDE.md +642 -0
- moai_adk/utils/__init__.py +30 -0
- moai_adk/utils/banner.py +38 -0
- moai_adk/utils/common.py +294 -0
- moai_adk/utils/link_validator.py +241 -0
- moai_adk/utils/logger.py +147 -0
- moai_adk/utils/safe_file_reader.py +206 -0
- moai_adk/utils/timeout.py +160 -0
- moai_adk/utils/toon_utils.py +256 -0
- moai_adk/version.py +22 -0
- moai_adk-0.35.1.dist-info/METADATA +3018 -0
- moai_adk-0.35.1.dist-info/RECORD +502 -0
- moai_adk-0.35.1.dist-info/WHEEL +4 -0
- moai_adk-0.35.1.dist-info/entry_points.txt +3 -0
- moai_adk-0.35.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
# Swift Development Examples
|
|
2
|
+
|
|
3
|
+
Production-ready code examples for iOS/macOS development with Swift 6.
|
|
4
|
+
|
|
5
|
+
## Complete Feature: User Authentication
|
|
6
|
+
|
|
7
|
+
### Full Authentication Flow
|
|
8
|
+
|
|
9
|
+
```swift
|
|
10
|
+
// MARK: - Domain Layer
|
|
11
|
+
|
|
12
|
+
/// Authentication errors with typed throws
|
|
13
|
+
enum AuthError: Error, LocalizedError {
|
|
14
|
+
case invalidCredentials
|
|
15
|
+
case networkError(underlying: Error)
|
|
16
|
+
case tokenExpired
|
|
17
|
+
case unauthorized
|
|
18
|
+
|
|
19
|
+
var errorDescription: String? {
|
|
20
|
+
switch self {
|
|
21
|
+
case .invalidCredentials: return "Invalid email or password"
|
|
22
|
+
case .networkError(let error): return "Network error: \(error.localizedDescription)"
|
|
23
|
+
case .tokenExpired: return "Session expired. Please login again"
|
|
24
|
+
case .unauthorized: return "Unauthorized access"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
struct User: Codable, Identifiable, Sendable {
|
|
30
|
+
let id: String
|
|
31
|
+
let email: String
|
|
32
|
+
let name: String
|
|
33
|
+
let avatarURL: URL?
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
struct AuthTokens: Codable, Sendable {
|
|
37
|
+
let accessToken: String
|
|
38
|
+
let refreshToken: String
|
|
39
|
+
let expiresAt: Date
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// MARK: - Data Layer
|
|
43
|
+
|
|
44
|
+
protocol AuthAPIProtocol: Sendable {
|
|
45
|
+
func login(email: String, password: String) async throws(AuthError) -> AuthTokens
|
|
46
|
+
func refreshToken(_ token: String) async throws(AuthError) -> AuthTokens
|
|
47
|
+
func fetchUser(token: String) async throws(AuthError) -> User
|
|
48
|
+
func logout(token: String) async throws(AuthError)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
actor AuthAPI: AuthAPIProtocol {
|
|
52
|
+
private let session: URLSession
|
|
53
|
+
private let baseURL: URL
|
|
54
|
+
|
|
55
|
+
init(baseURL: URL, session: URLSession = .shared) {
|
|
56
|
+
self.baseURL = baseURL
|
|
57
|
+
self.session = session
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func login(email: String, password: String) async throws(AuthError) -> AuthTokens {
|
|
61
|
+
let request = try makeRequest(
|
|
62
|
+
path: "/auth/login",
|
|
63
|
+
method: "POST",
|
|
64
|
+
body: ["email": email, "password": password]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
do {
|
|
68
|
+
let (data, response) = try await session.data(for: request)
|
|
69
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
70
|
+
throw AuthError.networkError(underlying: URLError(.badServerResponse))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
switch httpResponse.statusCode {
|
|
74
|
+
case 200:
|
|
75
|
+
return try JSONDecoder().decode(AuthTokens.self, from: data)
|
|
76
|
+
case 401:
|
|
77
|
+
throw AuthError.invalidCredentials
|
|
78
|
+
default:
|
|
79
|
+
throw AuthError.networkError(underlying: URLError(.badServerResponse))
|
|
80
|
+
}
|
|
81
|
+
} catch let error as AuthError {
|
|
82
|
+
throw error
|
|
83
|
+
} catch {
|
|
84
|
+
throw AuthError.networkError(underlying: error)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func refreshToken(_ token: String) async throws(AuthError) -> AuthTokens {
|
|
89
|
+
var request = try makeRequest(path: "/auth/refresh", method: "POST")
|
|
90
|
+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
91
|
+
|
|
92
|
+
do {
|
|
93
|
+
let (data, response) = try await session.data(for: request)
|
|
94
|
+
guard let httpResponse = response as? HTTPURLResponse,
|
|
95
|
+
httpResponse.statusCode == 200 else {
|
|
96
|
+
throw AuthError.tokenExpired
|
|
97
|
+
}
|
|
98
|
+
return try JSONDecoder().decode(AuthTokens.self, from: data)
|
|
99
|
+
} catch let error as AuthError {
|
|
100
|
+
throw error
|
|
101
|
+
} catch {
|
|
102
|
+
throw AuthError.networkError(underlying: error)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func fetchUser(token: String) async throws(AuthError) -> User {
|
|
107
|
+
var request = try makeRequest(path: "/auth/me", method: "GET")
|
|
108
|
+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
109
|
+
|
|
110
|
+
do {
|
|
111
|
+
let (data, response) = try await session.data(for: request)
|
|
112
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
113
|
+
throw AuthError.networkError(underlying: URLError(.badServerResponse))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
switch httpResponse.statusCode {
|
|
117
|
+
case 200:
|
|
118
|
+
return try JSONDecoder().decode(User.self, from: data)
|
|
119
|
+
case 401:
|
|
120
|
+
throw AuthError.unauthorized
|
|
121
|
+
default:
|
|
122
|
+
throw AuthError.networkError(underlying: URLError(.badServerResponse))
|
|
123
|
+
}
|
|
124
|
+
} catch let error as AuthError {
|
|
125
|
+
throw error
|
|
126
|
+
} catch {
|
|
127
|
+
throw AuthError.networkError(underlying: error)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func logout(token: String) async throws(AuthError) {
|
|
132
|
+
var request = try makeRequest(path: "/auth/logout", method: "POST")
|
|
133
|
+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
134
|
+
|
|
135
|
+
do {
|
|
136
|
+
let (_, response) = try await session.data(for: request)
|
|
137
|
+
guard let httpResponse = response as? HTTPURLResponse,
|
|
138
|
+
httpResponse.statusCode == 200 else {
|
|
139
|
+
throw AuthError.networkError(underlying: URLError(.badServerResponse))
|
|
140
|
+
}
|
|
141
|
+
} catch let error as AuthError {
|
|
142
|
+
throw error
|
|
143
|
+
} catch {
|
|
144
|
+
throw AuthError.networkError(underlying: error)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private func makeRequest(path: String, method: String, body: [String: Any]? = nil) throws -> URLRequest {
|
|
149
|
+
var request = URLRequest(url: baseURL.appendingPathComponent(path))
|
|
150
|
+
request.httpMethod = method
|
|
151
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
152
|
+
|
|
153
|
+
if let body = body {
|
|
154
|
+
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return request
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// MARK: - Presentation Layer
|
|
162
|
+
|
|
163
|
+
@Observable
|
|
164
|
+
@MainActor
|
|
165
|
+
final class AuthViewModel {
|
|
166
|
+
private let api: AuthAPIProtocol
|
|
167
|
+
private let keychain: KeychainProtocol
|
|
168
|
+
|
|
169
|
+
var user: User?
|
|
170
|
+
var isLoading = false
|
|
171
|
+
var error: AuthError?
|
|
172
|
+
var isAuthenticated: Bool { user != nil }
|
|
173
|
+
|
|
174
|
+
init(api: AuthAPIProtocol, keychain: KeychainProtocol) {
|
|
175
|
+
self.api = api
|
|
176
|
+
self.keychain = keychain
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
func login(email: String, password: String) async {
|
|
180
|
+
isLoading = true
|
|
181
|
+
error = nil
|
|
182
|
+
defer { isLoading = false }
|
|
183
|
+
|
|
184
|
+
do {
|
|
185
|
+
let tokens = try await api.login(email: email, password: password)
|
|
186
|
+
try keychain.save(tokens: tokens)
|
|
187
|
+
|
|
188
|
+
let user = try await api.fetchUser(token: tokens.accessToken)
|
|
189
|
+
self.user = user
|
|
190
|
+
} catch {
|
|
191
|
+
self.error = error
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
func restoreSession() async {
|
|
196
|
+
guard let tokens = try? keychain.loadTokens(),
|
|
197
|
+
tokens.expiresAt > Date() else {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
do {
|
|
202
|
+
user = try await api.fetchUser(token: tokens.accessToken)
|
|
203
|
+
} catch AuthError.unauthorized, AuthError.tokenExpired {
|
|
204
|
+
do {
|
|
205
|
+
let newTokens = try await api.refreshToken(tokens.refreshToken)
|
|
206
|
+
try keychain.save(tokens: newTokens)
|
|
207
|
+
user = try await api.fetchUser(token: newTokens.accessToken)
|
|
208
|
+
} catch {
|
|
209
|
+
try? keychain.deleteTokens()
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
self.error = error
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
func logout() async {
|
|
217
|
+
guard let tokens = try? keychain.loadTokens() else { return }
|
|
218
|
+
|
|
219
|
+
do {
|
|
220
|
+
try await api.logout(token: tokens.accessToken)
|
|
221
|
+
} catch {
|
|
222
|
+
// Log error but continue with local logout
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try? keychain.deleteTokens()
|
|
226
|
+
user = nil
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// MARK: - SwiftUI Views
|
|
231
|
+
|
|
232
|
+
struct LoginView: View {
|
|
233
|
+
@Environment(AuthViewModel.self) private var viewModel
|
|
234
|
+
@State private var email = ""
|
|
235
|
+
@State private var password = ""
|
|
236
|
+
|
|
237
|
+
var body: some View {
|
|
238
|
+
NavigationStack {
|
|
239
|
+
Form {
|
|
240
|
+
Section {
|
|
241
|
+
TextField("Email", text: $email)
|
|
242
|
+
.textContentType(.emailAddress)
|
|
243
|
+
.keyboardType(.emailAddress)
|
|
244
|
+
.autocapitalization(.none)
|
|
245
|
+
|
|
246
|
+
SecureField("Password", text: $password)
|
|
247
|
+
.textContentType(.password)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if let error = viewModel.error {
|
|
251
|
+
Section {
|
|
252
|
+
Text(error.localizedDescription)
|
|
253
|
+
.foregroundColor(.red)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
Section {
|
|
258
|
+
Button {
|
|
259
|
+
Task { await viewModel.login(email: email, password: password) }
|
|
260
|
+
} label: {
|
|
261
|
+
if viewModel.isLoading {
|
|
262
|
+
ProgressView()
|
|
263
|
+
.frame(maxWidth: .infinity)
|
|
264
|
+
} else {
|
|
265
|
+
Text("Sign In")
|
|
266
|
+
.frame(maxWidth: .infinity)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
.disabled(email.isEmpty || password.isEmpty || viewModel.isLoading)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
.navigationTitle("Login")
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Network Layer Example
|
|
279
|
+
|
|
280
|
+
### URLSession with Async/Await
|
|
281
|
+
|
|
282
|
+
```swift
|
|
283
|
+
actor NetworkClient {
|
|
284
|
+
private let session: URLSession
|
|
285
|
+
private let baseURL: URL
|
|
286
|
+
private let decoder: JSONDecoder
|
|
287
|
+
|
|
288
|
+
init(baseURL: URL, session: URLSession = .shared) {
|
|
289
|
+
self.baseURL = baseURL
|
|
290
|
+
self.session = session
|
|
291
|
+
self.decoder = JSONDecoder()
|
|
292
|
+
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
func get<T: Decodable>(_ path: String, query: [String: String] = [:]) async throws -> T {
|
|
296
|
+
var components = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: true)!
|
|
297
|
+
components.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
298
|
+
|
|
299
|
+
let (data, response) = try await session.data(from: components.url!)
|
|
300
|
+
try validateResponse(response)
|
|
301
|
+
return try decoder.decode(T.self, from: data)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func post<T: Decodable, B: Encodable>(_ path: String, body: B) async throws -> T {
|
|
305
|
+
var request = URLRequest(url: baseURL.appendingPathComponent(path))
|
|
306
|
+
request.httpMethod = "POST"
|
|
307
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
308
|
+
request.httpBody = try JSONEncoder().encode(body)
|
|
309
|
+
|
|
310
|
+
let (data, response) = try await session.data(for: request)
|
|
311
|
+
try validateResponse(response)
|
|
312
|
+
return try decoder.decode(T.self, from: data)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private func validateResponse(_ response: URLResponse) throws {
|
|
316
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
317
|
+
throw NetworkError.invalidResponse
|
|
318
|
+
}
|
|
319
|
+
guard 200..<300 ~= httpResponse.statusCode else {
|
|
320
|
+
throw NetworkError.statusCode(httpResponse.statusCode)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
enum NetworkError: Error {
|
|
326
|
+
case invalidResponse
|
|
327
|
+
case statusCode(Int)
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## SwiftUI Components
|
|
332
|
+
|
|
333
|
+
### Paginated List View
|
|
334
|
+
|
|
335
|
+
```swift
|
|
336
|
+
@Observable
|
|
337
|
+
@MainActor
|
|
338
|
+
final class PaginatedListViewModel<Item: Identifiable & Decodable> {
|
|
339
|
+
private(set) var items: [Item] = []
|
|
340
|
+
private(set) var isLoading = false
|
|
341
|
+
private(set) var error: Error?
|
|
342
|
+
private(set) var hasMorePages = true
|
|
343
|
+
|
|
344
|
+
private var currentPage = 1
|
|
345
|
+
private let pageSize = 20
|
|
346
|
+
private let fetchPage: (Int, Int) async throws -> (items: [Item], hasMore: Bool)
|
|
347
|
+
|
|
348
|
+
init(fetchPage: @escaping (Int, Int) async throws -> (items: [Item], hasMore: Bool)) {
|
|
349
|
+
self.fetchPage = fetchPage
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
func loadInitial() async {
|
|
353
|
+
guard !isLoading else { return }
|
|
354
|
+
|
|
355
|
+
isLoading = true
|
|
356
|
+
error = nil
|
|
357
|
+
currentPage = 1
|
|
358
|
+
|
|
359
|
+
do {
|
|
360
|
+
let result = try await fetchPage(currentPage, pageSize)
|
|
361
|
+
items = result.items
|
|
362
|
+
hasMorePages = result.hasMore
|
|
363
|
+
} catch {
|
|
364
|
+
self.error = error
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
isLoading = false
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
func loadMoreIfNeeded(currentItem: Item?) async {
|
|
371
|
+
guard let item = currentItem,
|
|
372
|
+
!isLoading,
|
|
373
|
+
hasMorePages else { return }
|
|
374
|
+
|
|
375
|
+
let thresholdIndex = items.index(items.endIndex, offsetBy: -5)
|
|
376
|
+
guard items.firstIndex(where: { $0.id == item.id as? Item.ID }) ?? 0 >= thresholdIndex else {
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
await loadNextPage()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private func loadNextPage() async {
|
|
384
|
+
isLoading = true
|
|
385
|
+
currentPage += 1
|
|
386
|
+
|
|
387
|
+
do {
|
|
388
|
+
let result = try await fetchPage(currentPage, pageSize)
|
|
389
|
+
items.append(contentsOf: result.items)
|
|
390
|
+
hasMorePages = result.hasMore
|
|
391
|
+
} catch {
|
|
392
|
+
currentPage -= 1
|
|
393
|
+
self.error = error
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
isLoading = false
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
struct PaginatedListView<Item: Identifiable & Decodable, RowContent: View>: View {
|
|
401
|
+
@State private var viewModel: PaginatedListViewModel<Item>
|
|
402
|
+
let rowContent: (Item) -> RowContent
|
|
403
|
+
|
|
404
|
+
init(
|
|
405
|
+
fetchPage: @escaping (Int, Int) async throws -> (items: [Item], hasMore: Bool),
|
|
406
|
+
@ViewBuilder rowContent: @escaping (Item) -> RowContent
|
|
407
|
+
) {
|
|
408
|
+
_viewModel = State(initialValue: PaginatedListViewModel(fetchPage: fetchPage))
|
|
409
|
+
self.rowContent = rowContent
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
var body: some View {
|
|
413
|
+
List {
|
|
414
|
+
ForEach(viewModel.items) { item in
|
|
415
|
+
rowContent(item)
|
|
416
|
+
.task {
|
|
417
|
+
await viewModel.loadMoreIfNeeded(currentItem: item)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if viewModel.isLoading {
|
|
422
|
+
HStack {
|
|
423
|
+
Spacer()
|
|
424
|
+
ProgressView()
|
|
425
|
+
Spacer()
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
.refreshable {
|
|
430
|
+
await viewModel.loadInitial()
|
|
431
|
+
}
|
|
432
|
+
.task {
|
|
433
|
+
await viewModel.loadInitial()
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Async Image with Cache
|
|
440
|
+
|
|
441
|
+
```swift
|
|
442
|
+
actor ImageCacheManager {
|
|
443
|
+
static let shared = ImageCacheManager()
|
|
444
|
+
|
|
445
|
+
private var cache = NSCache<NSURL, UIImage>()
|
|
446
|
+
private var inProgress: [URL: Task<UIImage, Error>] = [:]
|
|
447
|
+
|
|
448
|
+
private init() {
|
|
449
|
+
cache.countLimit = 100
|
|
450
|
+
cache.totalCostLimit = 50 * 1024 * 1024 // 50MB
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
func image(for url: URL) async throws -> UIImage {
|
|
454
|
+
// Check cache
|
|
455
|
+
if let cached = cache.object(forKey: url as NSURL) {
|
|
456
|
+
return cached
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Check in-progress
|
|
460
|
+
if let task = inProgress[url] {
|
|
461
|
+
return try await task.value
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Start download
|
|
465
|
+
let task = Task<UIImage, Error> {
|
|
466
|
+
let (data, _) = try await URLSession.shared.data(from: url)
|
|
467
|
+
guard let image = UIImage(data: data) else {
|
|
468
|
+
throw ImageError.invalidData
|
|
469
|
+
}
|
|
470
|
+
return image
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
inProgress[url] = task
|
|
474
|
+
|
|
475
|
+
do {
|
|
476
|
+
let image = try await task.value
|
|
477
|
+
cache.setObject(image, forKey: url as NSURL, cost: image.pngData()?.count ?? 0)
|
|
478
|
+
inProgress[url] = nil
|
|
479
|
+
return image
|
|
480
|
+
} catch {
|
|
481
|
+
inProgress[url] = nil
|
|
482
|
+
throw error
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
func clearCache() {
|
|
487
|
+
cache.removeAllObjects()
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
enum ImageError: Error {
|
|
492
|
+
case invalidData
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
struct CachedAsyncImage: View {
|
|
496
|
+
let url: URL?
|
|
497
|
+
var placeholder: Image = Image(systemName: "photo")
|
|
498
|
+
|
|
499
|
+
@State private var image: UIImage?
|
|
500
|
+
@State private var isLoading = false
|
|
501
|
+
|
|
502
|
+
var body: some View {
|
|
503
|
+
Group {
|
|
504
|
+
if let image = image {
|
|
505
|
+
Image(uiImage: image)
|
|
506
|
+
.resizable()
|
|
507
|
+
.aspectRatio(contentMode: .fill)
|
|
508
|
+
} else if isLoading {
|
|
509
|
+
ProgressView()
|
|
510
|
+
} else {
|
|
511
|
+
placeholder
|
|
512
|
+
.resizable()
|
|
513
|
+
.aspectRatio(contentMode: .fit)
|
|
514
|
+
.foregroundColor(.gray)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
.task(id: url) {
|
|
518
|
+
guard let url = url else { return }
|
|
519
|
+
isLoading = true
|
|
520
|
+
defer { isLoading = false }
|
|
521
|
+
|
|
522
|
+
do {
|
|
523
|
+
image = try await ImageCacheManager.shared.image(for: url)
|
|
524
|
+
} catch {
|
|
525
|
+
image = nil
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
## Testing Examples
|
|
533
|
+
|
|
534
|
+
### XCTest with async
|
|
535
|
+
|
|
536
|
+
```swift
|
|
537
|
+
@MainActor
|
|
538
|
+
final class AuthViewModelTests: XCTestCase {
|
|
539
|
+
var sut: AuthViewModel!
|
|
540
|
+
var mockAPI: MockAuthAPI!
|
|
541
|
+
var mockKeychain: MockKeychain!
|
|
542
|
+
|
|
543
|
+
override func setUp() {
|
|
544
|
+
mockAPI = MockAuthAPI()
|
|
545
|
+
mockKeychain = MockKeychain()
|
|
546
|
+
sut = AuthViewModel(api: mockAPI, keychain: mockKeychain)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
override func tearDown() {
|
|
550
|
+
sut = nil
|
|
551
|
+
mockAPI = nil
|
|
552
|
+
mockKeychain = nil
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
func testLoginSuccess() async {
|
|
556
|
+
// Given
|
|
557
|
+
mockAPI.mockTokens = AuthTokens(
|
|
558
|
+
accessToken: "test-token",
|
|
559
|
+
refreshToken: "refresh-token",
|
|
560
|
+
expiresAt: Date().addingTimeInterval(3600)
|
|
561
|
+
)
|
|
562
|
+
mockAPI.mockUser = User(id: "1", email: "test@example.com", name: "Test User", avatarURL: nil)
|
|
563
|
+
|
|
564
|
+
// When
|
|
565
|
+
await sut.login(email: "test@example.com", password: "password123")
|
|
566
|
+
|
|
567
|
+
// Then
|
|
568
|
+
XCTAssertNotNil(sut.user)
|
|
569
|
+
XCTAssertEqual(sut.user?.email, "test@example.com")
|
|
570
|
+
XCTAssertNil(sut.error)
|
|
571
|
+
XCTAssertFalse(sut.isLoading)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
func testLoginInvalidCredentials() async {
|
|
575
|
+
// Given
|
|
576
|
+
mockAPI.loginError = .invalidCredentials
|
|
577
|
+
|
|
578
|
+
// When
|
|
579
|
+
await sut.login(email: "test@example.com", password: "wrong")
|
|
580
|
+
|
|
581
|
+
// Then
|
|
582
|
+
XCTAssertNil(sut.user)
|
|
583
|
+
XCTAssertEqual(sut.error, .invalidCredentials)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
func testRestoreSessionWithValidToken() async {
|
|
587
|
+
// Given
|
|
588
|
+
let tokens = AuthTokens(
|
|
589
|
+
accessToken: "valid-token",
|
|
590
|
+
refreshToken: "refresh-token",
|
|
591
|
+
expiresAt: Date().addingTimeInterval(3600)
|
|
592
|
+
)
|
|
593
|
+
mockKeychain.savedTokens = tokens
|
|
594
|
+
mockAPI.mockUser = User(id: "1", email: "test@example.com", name: "Test User", avatarURL: nil)
|
|
595
|
+
|
|
596
|
+
// When
|
|
597
|
+
await sut.restoreSession()
|
|
598
|
+
|
|
599
|
+
// Then
|
|
600
|
+
XCTAssertNotNil(sut.user)
|
|
601
|
+
XCTAssertEqual(sut.user?.email, "test@example.com")
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
func testLogoutClearsUser() async {
|
|
605
|
+
// Given
|
|
606
|
+
sut.user = User(id: "1", email: "test@example.com", name: "Test User", avatarURL: nil)
|
|
607
|
+
mockKeychain.savedTokens = AuthTokens(
|
|
608
|
+
accessToken: "token",
|
|
609
|
+
refreshToken: "refresh",
|
|
610
|
+
expiresAt: Date().addingTimeInterval(3600)
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
// When
|
|
614
|
+
await sut.logout()
|
|
615
|
+
|
|
616
|
+
// Then
|
|
617
|
+
XCTAssertNil(sut.user)
|
|
618
|
+
XCTAssertNil(mockKeychain.savedTokens)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// MARK: - Mocks
|
|
623
|
+
|
|
624
|
+
class MockAuthAPI: AuthAPIProtocol {
|
|
625
|
+
var mockTokens: AuthTokens?
|
|
626
|
+
var mockUser: User?
|
|
627
|
+
var loginError: AuthError?
|
|
628
|
+
var fetchUserError: AuthError?
|
|
629
|
+
|
|
630
|
+
func login(email: String, password: String) async throws(AuthError) -> AuthTokens {
|
|
631
|
+
if let error = loginError { throw error }
|
|
632
|
+
guard let tokens = mockTokens else {
|
|
633
|
+
throw .networkError(underlying: URLError(.unknown))
|
|
634
|
+
}
|
|
635
|
+
return tokens
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
func refreshToken(_ token: String) async throws(AuthError) -> AuthTokens {
|
|
639
|
+
guard let tokens = mockTokens else {
|
|
640
|
+
throw .tokenExpired
|
|
641
|
+
}
|
|
642
|
+
return tokens
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
func fetchUser(token: String) async throws(AuthError) -> User {
|
|
646
|
+
if let error = fetchUserError { throw error }
|
|
647
|
+
guard let user = mockUser else {
|
|
648
|
+
throw .unauthorized
|
|
649
|
+
}
|
|
650
|
+
return user
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
func logout(token: String) async throws(AuthError) {
|
|
654
|
+
// No-op for testing
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
protocol KeychainProtocol: Sendable {
|
|
659
|
+
func save(tokens: AuthTokens) throws
|
|
660
|
+
func loadTokens() throws -> AuthTokens?
|
|
661
|
+
func deleteTokens() throws
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
class MockKeychain: KeychainProtocol {
|
|
665
|
+
var savedTokens: AuthTokens?
|
|
666
|
+
|
|
667
|
+
func save(tokens: AuthTokens) throws {
|
|
668
|
+
savedTokens = tokens
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
func loadTokens() throws -> AuthTokens? {
|
|
672
|
+
return savedTokens
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
func deleteTokens() throws {
|
|
676
|
+
savedTokens = nil
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Combine Testing
|
|
682
|
+
|
|
683
|
+
```swift
|
|
684
|
+
import Combine
|
|
685
|
+
import XCTest
|
|
686
|
+
|
|
687
|
+
final class SearchViewModelTests: XCTestCase {
|
|
688
|
+
var sut: SearchViewModel!
|
|
689
|
+
var mockService: MockSearchService!
|
|
690
|
+
var cancellables: Set<AnyCancellable>!
|
|
691
|
+
|
|
692
|
+
override func setUp() {
|
|
693
|
+
mockService = MockSearchService()
|
|
694
|
+
sut = SearchViewModel(searchService: mockService)
|
|
695
|
+
cancellables = []
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
override func tearDown() {
|
|
699
|
+
sut = nil
|
|
700
|
+
mockService = nil
|
|
701
|
+
cancellables = nil
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
func testSearchDebounces() {
|
|
705
|
+
// Given
|
|
706
|
+
let expectation = expectation(description: "Search debounce")
|
|
707
|
+
var searchCount = 0
|
|
708
|
+
|
|
709
|
+
mockService.searchHandler = { query in
|
|
710
|
+
searchCount += 1
|
|
711
|
+
return [SearchResult(id: "1", title: query)]
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
sut.$results
|
|
715
|
+
.dropFirst()
|
|
716
|
+
.sink { results in
|
|
717
|
+
if !results.isEmpty {
|
|
718
|
+
expectation.fulfill()
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
.store(in: &cancellables)
|
|
722
|
+
|
|
723
|
+
// When - rapid input
|
|
724
|
+
sut.searchText = "t"
|
|
725
|
+
sut.searchText = "te"
|
|
726
|
+
sut.searchText = "tes"
|
|
727
|
+
sut.searchText = "test"
|
|
728
|
+
|
|
729
|
+
// Then
|
|
730
|
+
wait(for: [expectation], timeout: 1.0)
|
|
731
|
+
XCTAssertEqual(searchCount, 1, "Should only search once after debounce")
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
class MockSearchService: SearchServiceProtocol {
|
|
736
|
+
var searchHandler: ((String) -> [SearchResult])?
|
|
737
|
+
|
|
738
|
+
func search(_ query: String) -> AnyPublisher<[SearchResult], Error> {
|
|
739
|
+
let results = searchHandler?(query) ?? []
|
|
740
|
+
return Just(results)
|
|
741
|
+
.setFailureType(to: Error.self)
|
|
742
|
+
.eraseToAnyPublisher()
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
## SwiftData Example
|
|
748
|
+
|
|
749
|
+
### Complete CRUD Operations
|
|
750
|
+
|
|
751
|
+
```swift
|
|
752
|
+
import SwiftData
|
|
753
|
+
import SwiftUI
|
|
754
|
+
|
|
755
|
+
@Model
|
|
756
|
+
final class Task {
|
|
757
|
+
var id: UUID
|
|
758
|
+
var title: String
|
|
759
|
+
var isCompleted: Bool
|
|
760
|
+
var priority: Priority
|
|
761
|
+
var dueDate: Date?
|
|
762
|
+
var createdAt: Date
|
|
763
|
+
|
|
764
|
+
@Relationship(deleteRule: .cascade, inverse: \Subtask.parentTask)
|
|
765
|
+
var subtasks: [Subtask]
|
|
766
|
+
|
|
767
|
+
init(title: String, priority: Priority = .medium, dueDate: Date? = nil) {
|
|
768
|
+
self.id = UUID()
|
|
769
|
+
self.title = title
|
|
770
|
+
self.isCompleted = false
|
|
771
|
+
self.priority = priority
|
|
772
|
+
self.dueDate = dueDate
|
|
773
|
+
self.createdAt = Date()
|
|
774
|
+
self.subtasks = []
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
enum Priority: String, Codable, CaseIterable {
|
|
778
|
+
case low, medium, high
|
|
779
|
+
|
|
780
|
+
var color: Color {
|
|
781
|
+
switch self {
|
|
782
|
+
case .low: return .green
|
|
783
|
+
case .medium: return .orange
|
|
784
|
+
case .high: return .red
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
@Model
|
|
791
|
+
final class Subtask {
|
|
792
|
+
var id: UUID
|
|
793
|
+
var title: String
|
|
794
|
+
var isCompleted: Bool
|
|
795
|
+
var parentTask: Task?
|
|
796
|
+
|
|
797
|
+
init(title: String) {
|
|
798
|
+
self.id = UUID()
|
|
799
|
+
self.title = title
|
|
800
|
+
self.isCompleted = false
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
@MainActor
|
|
805
|
+
final class TaskRepository: ObservableObject {
|
|
806
|
+
private let modelContext: ModelContext
|
|
807
|
+
|
|
808
|
+
init(modelContext: ModelContext) {
|
|
809
|
+
self.modelContext = modelContext
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
func fetchTasks(completed: Bool? = nil) throws -> [Task] {
|
|
813
|
+
var descriptor = FetchDescriptor<Task>(
|
|
814
|
+
sortBy: [
|
|
815
|
+
SortDescriptor(\.priority, order: .reverse),
|
|
816
|
+
SortDescriptor(\.createdAt, order: .reverse)
|
|
817
|
+
]
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
if let completed = completed {
|
|
821
|
+
descriptor.predicate = #Predicate { $0.isCompleted == completed }
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return try modelContext.fetch(descriptor)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
func addTask(_ task: Task) throws {
|
|
828
|
+
modelContext.insert(task)
|
|
829
|
+
try modelContext.save()
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
func updateTask(_ task: Task) throws {
|
|
833
|
+
try modelContext.save()
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
func deleteTask(_ task: Task) throws {
|
|
837
|
+
modelContext.delete(task)
|
|
838
|
+
try modelContext.save()
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
func toggleCompletion(_ task: Task) throws {
|
|
842
|
+
task.isCompleted.toggle()
|
|
843
|
+
try modelContext.save()
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
struct TaskListView: View {
|
|
848
|
+
@Environment(\.modelContext) private var modelContext
|
|
849
|
+
@Query(sort: \Task.createdAt, order: .reverse) private var tasks: [Task]
|
|
850
|
+
@State private var showAddTask = false
|
|
851
|
+
|
|
852
|
+
var body: some View {
|
|
853
|
+
NavigationStack {
|
|
854
|
+
List {
|
|
855
|
+
ForEach(tasks) { task in
|
|
856
|
+
TaskRow(task: task)
|
|
857
|
+
}
|
|
858
|
+
.onDelete(perform: deleteTasks)
|
|
859
|
+
}
|
|
860
|
+
.navigationTitle("Tasks")
|
|
861
|
+
.toolbar {
|
|
862
|
+
Button {
|
|
863
|
+
showAddTask = true
|
|
864
|
+
} label: {
|
|
865
|
+
Image(systemName: "plus")
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
.sheet(isPresented: $showAddTask) {
|
|
869
|
+
AddTaskView()
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
private func deleteTasks(at offsets: IndexSet) {
|
|
875
|
+
for index in offsets {
|
|
876
|
+
modelContext.delete(tasks[index])
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
struct TaskRow: View {
|
|
882
|
+
@Bindable var task: Task
|
|
883
|
+
|
|
884
|
+
var body: some View {
|
|
885
|
+
HStack {
|
|
886
|
+
Button {
|
|
887
|
+
task.isCompleted.toggle()
|
|
888
|
+
} label: {
|
|
889
|
+
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
|
|
890
|
+
.foregroundColor(task.isCompleted ? .green : .gray)
|
|
891
|
+
}
|
|
892
|
+
.buttonStyle(.plain)
|
|
893
|
+
|
|
894
|
+
VStack(alignment: .leading) {
|
|
895
|
+
Text(task.title)
|
|
896
|
+
.strikethrough(task.isCompleted)
|
|
897
|
+
|
|
898
|
+
if let dueDate = task.dueDate {
|
|
899
|
+
Text(dueDate, style: .date)
|
|
900
|
+
.font(.caption)
|
|
901
|
+
.foregroundColor(.secondary)
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
Spacer()
|
|
906
|
+
|
|
907
|
+
Circle()
|
|
908
|
+
.fill(task.priority.color)
|
|
909
|
+
.frame(width: 10, height: 10)
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
---
|
|
916
|
+
|
|
917
|
+
Version: 1.0.0
|
|
918
|
+
Last Updated: 2025-12-07
|