完成50%
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$document = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'APP.openapi.json') | ConvertFrom-Json
|
||||
$yaml = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'APP.openapi.yaml')
|
||||
$issues = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
function Add-Issue {
|
||||
param([string]$Message)
|
||||
$script:issues.Add($Message)
|
||||
}
|
||||
|
||||
function Get-Operation {
|
||||
param([string]$Path)
|
||||
$pathProperty = $document.paths.PSObject.Properties[$Path]
|
||||
if (-not $pathProperty -or -not $pathProperty.Value.get) {
|
||||
Add-Issue "JSON missing GET $Path"
|
||||
return $null
|
||||
}
|
||||
return $pathProperty.Value.get
|
||||
}
|
||||
|
||||
function Get-ResponseRef {
|
||||
param([object]$Operation, [string]$Label)
|
||||
if (-not $Operation) { return '' }
|
||||
$response = $Operation.responses.PSObject.Properties['200'].Value
|
||||
if (-not $response) {
|
||||
Add-Issue "JSON $Label missing 200 response"
|
||||
return ''
|
||||
}
|
||||
if ($response.'$ref') {
|
||||
$responseName = ([string]$response.'$ref').Split('/')[-1]
|
||||
$response = $document.components.responses.PSObject.Properties[$responseName].Value
|
||||
}
|
||||
$media = @($response.content.PSObject.Properties)
|
||||
if ($media.Count -eq 0) {
|
||||
Add-Issue "JSON $Label missing response content"
|
||||
return ''
|
||||
}
|
||||
return [string]$media[0].Value.schema.'$ref'
|
||||
}
|
||||
|
||||
function Get-Schema {
|
||||
param([string]$Name)
|
||||
$property = $document.components.schemas.PSObject.Properties[$Name]
|
||||
if (-not $property) {
|
||||
Add-Issue "JSON missing schema owner: $Name"
|
||||
return $null
|
||||
}
|
||||
return $property.Value
|
||||
}
|
||||
|
||||
function Assert-SecurityAndClient {
|
||||
param([object]$Operation, [string]$Label)
|
||||
if (-not $Operation) { return }
|
||||
$hasSaToken = $false
|
||||
foreach ($requirement in @($Operation.security)) {
|
||||
if ($requirement.PSObject.Properties.Name -contains 'SaToken') { $hasSaToken = $true }
|
||||
}
|
||||
if (-not $hasSaToken) { Add-Issue "JSON $Label must require SaToken" }
|
||||
$client = @($Operation.parameters | Where-Object { $_.name -eq 'clientid' -and $_.in -eq 'header' })
|
||||
if ($client.Count -ne 1 -or $client[0].required -ne $true -or $client[0].schema.type -ne 'string') {
|
||||
Add-Issue "JSON $Label must require one string clientid header"
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-Required {
|
||||
param([object]$Schema, [string]$SchemaName, [string[]]$Fields)
|
||||
if (-not $Schema) { return }
|
||||
foreach ($field in $Fields) {
|
||||
if ($field -notin @($Schema.required)) {
|
||||
Add-Issue "JSON $SchemaName.required missing: $field"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$listPath = '/genealogy/app/notifications'
|
||||
$countPath = '/genealogy/app/notifications/unread-count'
|
||||
$listOperation = Get-Operation $listPath
|
||||
$countOperation = Get-Operation $countPath
|
||||
Assert-SecurityAndClient $listOperation "GET $listPath"
|
||||
Assert-SecurityAndClient $countOperation "GET $countPath"
|
||||
|
||||
$listRef = Get-ResponseRef $listOperation "GET $listPath"
|
||||
if ($listRef -ne '#/components/schemas/RListNotificationVo') {
|
||||
Add-Issue "JSON GET $listPath must return RListNotificationVo; actual: $listRef"
|
||||
}
|
||||
$countRef = Get-ResponseRef $countOperation "GET $countPath"
|
||||
if ($countRef -ne '#/components/schemas/RNotificationUnreadCount') {
|
||||
Add-Issue "JSON GET $countPath must return RNotificationUnreadCount; actual: $countRef"
|
||||
}
|
||||
|
||||
$listEnvelope = Get-Schema 'RListNotificationVo'
|
||||
$notice = Get-Schema 'NotificationVo'
|
||||
$countEnvelope = Get-Schema 'RNotificationUnreadCount'
|
||||
Assert-Required $listEnvelope 'RListNotificationVo' @('code', 'data')
|
||||
Assert-Required $notice 'NotificationVo' @('noticeTitle', 'noticeContent', 'publishTime', 'readStatus')
|
||||
Assert-Required $countEnvelope 'RNotificationUnreadCount' @('code', 'data')
|
||||
|
||||
$listSemantics = ([string]$listOperation.description) + ' ' + ([string]$listEnvelope.properties.data.description)
|
||||
if ($listSemantics -notmatch '(?i)newest[ -]?first' -or
|
||||
$listSemantics -notmatch '(?i)complete active notification set' -or
|
||||
$listSemantics -notmatch '(?i)at most 200') {
|
||||
Add-Issue 'JSON notification list must document newest-first, complete active-set, and at-most-200 semantics'
|
||||
}
|
||||
|
||||
$countSemantics = ([string]$countOperation.description) + ' ' + ([string]$countEnvelope.properties.data.description)
|
||||
if ($countSemantics -notmatch '(?i)same active notification set' -or
|
||||
$countSemantics -notmatch '(?i)readStatus=UNREAD') {
|
||||
Add-Issue 'JSON unread count must document the same active-set and readStatus=UNREAD scope'
|
||||
}
|
||||
|
||||
if ($listEnvelope) {
|
||||
if ($listEnvelope.properties.code.type -ne 'integer') {
|
||||
Add-Issue 'JSON RListNotificationVo.code must be integer'
|
||||
}
|
||||
$data = $listEnvelope.properties.data
|
||||
if ($data.type -ne 'array' -or $data.items.'$ref' -ne '#/components/schemas/NotificationVo') {
|
||||
Add-Issue 'JSON RListNotificationVo.data must be NotificationVo[]'
|
||||
}
|
||||
if ([int]$data.maxItems -lt 1 -or [int]$data.maxItems -gt 200) {
|
||||
Add-Issue 'JSON RListNotificationVo.data must declare maxItems between 1 and 200'
|
||||
}
|
||||
}
|
||||
|
||||
if ($notice) {
|
||||
foreach ($field in @('noticeTitle', 'noticeContent')) {
|
||||
if ($notice.properties.$field.type -ne 'string') {
|
||||
Add-Issue "JSON NotificationVo.$field must be string"
|
||||
}
|
||||
if ([int]$notice.properties.$field.minLength -lt 1) {
|
||||
Add-Issue "JSON NotificationVo.$field must declare minLength >= 1"
|
||||
}
|
||||
}
|
||||
if ([int]$notice.properties.noticeTitle.maxLength -lt 1 -or [int]$notice.properties.noticeTitle.maxLength -gt 50) {
|
||||
Add-Issue 'JSON NotificationVo.noticeTitle must declare maxLength between 1 and 50'
|
||||
}
|
||||
if ([int]$notice.properties.noticeContent.maxLength -lt 1 -or [int]$notice.properties.noticeContent.maxLength -gt 1000) {
|
||||
Add-Issue 'JSON NotificationVo.noticeContent must declare maxLength between 1 and 1000'
|
||||
}
|
||||
$contentDescription = [string]$notice.properties.noticeContent.description
|
||||
if ($contentDescription -notmatch '(?i)plain[ -]?text' -or $contentDescription -notmatch '(?i)complete|untruncated') {
|
||||
Add-Issue 'JSON NotificationVo.noticeContent must declare complete, untruncated plain-text semantics'
|
||||
}
|
||||
$publishTime = $notice.properties.publishTime
|
||||
if ($publishTime.type -ne 'string' -or $publishTime.format -ne 'date-time') {
|
||||
Add-Issue 'JSON NotificationVo.publishTime must be date-time string'
|
||||
}
|
||||
$timePattern = [string]$publishTime.pattern
|
||||
try {
|
||||
$timeRegex = [regex]::new($timePattern)
|
||||
if (-not $timePattern -or
|
||||
-not $timeRegex.IsMatch('2026-07-22T21:00:00Z') -or
|
||||
-not $timeRegex.IsMatch('2026-07-22T21:00:00.123+08:00') -or
|
||||
$timeRegex.IsMatch('2026-07-22T21:00:00')) {
|
||||
Add-Issue 'JSON NotificationVo.publishTime pattern must require an RFC3339 timezone suffix'
|
||||
}
|
||||
} catch {
|
||||
Add-Issue 'JSON NotificationVo.publishTime pattern must be a valid regular expression'
|
||||
}
|
||||
$readStatus = $notice.properties.readStatus
|
||||
$readValues = @($readStatus.enum | Sort-Object)
|
||||
if ($readStatus.type -ne 'string' -or ($readValues -join ',') -ne 'READ,UNREAD') {
|
||||
Add-Issue 'JSON NotificationVo.readStatus must be enum READ/UNREAD'
|
||||
}
|
||||
}
|
||||
|
||||
if ($countEnvelope) {
|
||||
if ($countEnvelope.properties.code.type -ne 'integer') {
|
||||
Add-Issue 'JSON RNotificationUnreadCount.code must be integer'
|
||||
}
|
||||
$countData = $countEnvelope.properties.data
|
||||
if ($countData.type -ne 'integer' -or $countData.format -ne 'int32' -or
|
||||
[int64]$countData.minimum -ne 0 -or [int64]$countData.maximum -ne 200) {
|
||||
Add-Issue 'JSON RNotificationUnreadCount.data must be int32 in range 0..200'
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($yamlFact in @(
|
||||
' /genealogy/app/notifications:',
|
||||
' /genealogy/app/notifications/unread-count:',
|
||||
'#/components/schemas/RListNotificationVo',
|
||||
'#/components/schemas/RNotificationUnreadCount',
|
||||
' NotificationVo:',
|
||||
' RListNotificationVo:',
|
||||
' RNotificationUnreadCount:',
|
||||
' - noticeTitle',
|
||||
' - noticeContent',
|
||||
' - publishTime',
|
||||
' - readStatus'
|
||||
)) {
|
||||
if (-not $yaml.Contains($yamlFact)) { Add-Issue "YAML fact is missing: $yamlFact" }
|
||||
}
|
||||
|
||||
if ($issues.Count -gt 0) {
|
||||
$lines = New-Object System.Collections.Generic.List[string]
|
||||
$lines.Add('NOTIFICATION-READ-OPENAPI-CONTRACT BLOCKED')
|
||||
foreach ($issue in $issues) { $lines.Add("- $issue") }
|
||||
$lines.Add('- The first adapter drops every server ID, senderPhone, and biz target field; N02 reads only the current immutable in-memory snapshot.')
|
||||
$lines.Add('- The service must document newest-first ordering, the active retention/window policy, and count/list scope consistency.')
|
||||
$lines.Add('- Replace both protected exports from one backend version; do not hand-edit APP.openapi.json or APP.openapi.yaml.')
|
||||
throw ($lines -join [Environment]::NewLine)
|
||||
}
|
||||
|
||||
Write-Output 'NOTIFICATION-READ-OPENAPI-CONTRACT PASS'
|
||||
Reference in New Issue
Block a user