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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/dev/case-v1p1-conformance-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ compeito の現在のゴールは **OpenCASE / OpenSALT との実用的な相互
- **未定義サブパスの 404 / 未捕捉の 500 を imsx_StatusInfo 形式で返す**(旧 C14 / C15)。`main.py` に `StarletteHTTPException` ハンドラ(CASE API パスの 404 → `unknownobject`)とグローバル `Exception` ハンドラ(CASE API パスの 500 → `internal_server_error`)を追加。CASE API 以外は既定挙動を維持。
- **エラー封筒の `imsx_codeMinorFieldName` を実フィールド名に**(旧 C11)。`imsx_error_response` に `field_name` 引数を追加し、sort / orderBy / filter / fields / limit / offset と request-validation 由来のフィールド名を渡す(既定は `sourcedId`)。
- **`ext:` associationType の文字種検証**(旧 C12)。import 受理を公式パターン `^ext:[a-zA-Z0-9.\-_]+$` で検証し、不一致(`ext:日本語` / `ext:` / 空白入り等)は invalid associationType として skip + warning。
- **`caseVersion` の import 検証**(旧 C8)。想定外の値(`1.0` / `1.1` 以外)は警告を出す。値そのものは保持する(round-trip 忠実度を保つため emit 固定はしない)。

## certification 着手項目(未対応 / 意図的差異)

Expand All @@ -31,7 +32,6 @@ compeito の現在のゴールは **OpenCASE / OpenSALT との実用的な相互
| C5 | **`Link` ページネーションヘッダ** | 未実装(next/prev/first/last)。`X-Total-Count` は実装済み | P2 | `GET /CFDocuments` に RFC 8288 形式の `Link` を追加。既存クエリ(sort/filter/fields)を保持してリンク生成 |
| C6 | **filter の網羅性** | scalar + `subject`(JSONB) 対応。ネストのドット記法(`licenseURI.identifier` 等)未対応。ordering は大小区別のまま(等価は大小無視に対応済) | P2 | dot-notation のリンクフィールド filter、必要なら collation 指定の case-insensitive ordering |
| C7 | **不明な `sort` / `fields` → 400** | binding 散文は「不明 sort は既定順」「不明 field は全件返す(空 field のみ invalid_selection_field)」。compeito は 400 で明示エラー(typo 可視で親切) | P2 | strict/compat モードでのみ binding 散文どおりの寛容挙動に切替(既定は現状維持を推奨) |
| C8 | **`caseVersion` を "1.1" に強制しない** | 保存値をそのまま emit | P3 | import 時に "1.1" 検証、または emit 時に固定 |
| C9 | **UUID 不正 → 400 / `limit`=0 許容 / `limit`・`offset` の上限 cap** | 実用優先の挙動(OpenAPI は invalid を unknownobject 扱い、`minimum:1` 等) | P3 | strict モードでのみ OpenAPI どおりに(既定は現状維持) |
| C10 | **拡張 list エンドポイント** | `CFItemTypes` 等の list は compeito 拡張(公式 list は `CFDocuments` のみ)。`sort/filter/fields` も `CFDocuments` のみ対応 | — | 仕様超過なので certification 上は無害。必要なら他 list にも query を展開 |
| C13 | **スキーマ層の出力時検証なし** | Pydantic スキーマで identifier の UUID パターン・associationType / targetType の enum を検証していない(import 側で防いでいるため実害は低い) | P3 | strict 出力モード導入時に field_validator で同梱 |
Expand Down
4 changes: 2 additions & 2 deletions docs/spec/db-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ creator: VARCHAR -- Required in CASE v1.1 but nullable here to a
publisher: VARCHAR
description: TEXT
framework_type: VARCHAR -- v1.1 new. Standard value "CourseCodes" (free-form string per OpenAPI).
case_version: VARCHAR -- v1.1 new. OpenAPI enum: ["1.1"]. Only "1.1" is valid.
case_version: VARCHAR -- v1.1 new. OpenAPI enum: ["1.1"]. Import also accepts "1.0" (v1.0 sources); other values are kept with a warning.
language: VARCHAR(10)
version: VARCHAR
adoption_status: VARCHAR
Expand Down Expand Up @@ -301,7 +301,7 @@ creator: VARCHAR -- CASE v1.1 では required だが、CSV イ
publisher: VARCHAR
description: TEXT
framework_type: VARCHAR -- v1.1 new. 標準値は "CourseCodes"(OpenAPI 上は自由文字列)
case_version: VARCHAR -- v1.1 new. OpenAPI では enum: ["1.1"]。値は "1.1" のみ有効
case_version: VARCHAR -- v1.1 new. OpenAPI では enum: ["1.1"]。import は "1.0"(v1.0 ソース)も受理し、その他の値は警告のうえ保持
language: VARCHAR(10)
version: VARCHAR
adoption_status: VARCHAR
Expand Down
4 changes: 2 additions & 2 deletions docs/spec/import-logic.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ Mapping from the external CFPackage's CFDocument object to DB columns:
- `publisher` → `publisher`.
- `description` → `description`.
- `frameworkType` → `framework_type` (v1.1 new).
- `caseVersion` → `case_version` (v1.1 new; only `"1.1"` is valid).
- `caseVersion` → `case_version` (v1.1 new). `"1.0"` and `"1.1"` are accepted without a warning (`"1.0"` because compeito ingests v1.0 sources); any other value emits a warning ("CFDocument '{id}': unexpected caseVersion '{val}' …") and the value is **kept as-is** (not rewritten, to preserve round-trip fidelity).
- `language` → `language` (validate length ≤ 10; too long → NULL with a warning — same rule as CSV import).
- `version` → `version`.
- `adoptionStatus` → `adoption_status`.
Expand Down Expand Up @@ -863,7 +863,7 @@ CSVインポートと同様に、既存ドキュメント更新時は Step 3 で
- `publisher` → `publisher`
- `description` → `description`
- `frameworkType` → `framework_type`(v1.1 new)
- `caseVersion` → `case_version`(v1.1 new。値は `"1.1"` のみ有効
- `caseVersion` → `case_version`(v1.1 new。`"1.0"` と `"1.1"` は警告なしで受理する(`"1.0"` は v1.0 ソース取り込みのため)。それ以外の値は警告を出力し(「CFDocument '{id}': unexpected caseVersion '{val}' …」)、値はそのまま保持する(round-trip 忠実度を保つため書き換えない)。
- `language` → `language`(10文字以下であることを検証する。超過の場合は NULL として保存し警告出力。CSV インポートと同一ルール)
- `version` → `version`
- `adoptionStatus` → `adoption_status`
Expand Down
2 changes: 1 addition & 1 deletion docs/spec/web-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ OpenSALT の `/uri/{uuid}` ページを参考にしつつ、デザインは Tail
| licenseURI | 任意 | ネスト表示(title, identifier, uri)。CFItem と同一形式 |
| officialSourceURL | 任意 | URL(リンク) |
| frameworkType | 任意 | テキスト(v1.1 new。外部インポート由来で設定される場合がある) |
| caseVersion | 任意 | テキスト(v1.1 new。値は "1.1" のみ) |
| caseVersion | 任意 | テキスト(v1.1 new。OpenAPI enum は "1.1"。import は "1.0" も受理し、その他の値は警告のうえ保持) |
| subject | 任意 | 配列をカンマ区切りで表示 |
| subjectURI | 任意 | 各要素のネスト表示(title, identifier, uri)。配列 |
| CFPackageURI | 必須 | ネスト表示(title, identifier, uri) |
Expand Down
26 changes: 24 additions & 2 deletions src/services/case_import_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,28 @@ def _validate_language(
return val


# Accepted caseVersion values. The CASE v1.1 OpenAPI enum is just "1.1"; we also
# accept "1.0" because compeito ingests v1.0 sources (normalized to v1.1 output).
_VALID_CASE_VERSIONS = {"1.0", "1.1"}


def _validate_case_version(
val: str | None,
context: str,
warnings: list[str],
) -> str | None:
"""Warn on an unexpected caseVersion; the stored value is left unchanged.

A warning (not a rewrite) keeps round-trip fidelity intact while still
surfacing malformed values such as "2.0" / "v1.1" / typos.
"""
if val is not None and val not in _VALID_CASE_VERSIONS:
warnings.append(
f"{context}: unexpected caseVersion '{val}' (expected one of {sorted(_VALID_CASE_VERSIONS)}); kept as-is"
)
return val


# ---------------------------------------------------------------------------
# HTTP fetch
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -907,7 +929,7 @@ def _create_document(
publisher=data.get("publisher"),
description=data.get("description"),
framework_type=data.get("frameworkType"),
case_version=data.get("caseVersion"),
case_version=_validate_case_version(data.get("caseVersion"), f"CFDocument '{ident}'", warnings),
language=lang,
version=data.get("version"),
adoption_status=data.get("adoptionStatus"),
Expand Down Expand Up @@ -960,7 +982,7 @@ def _update_document(
if data.get("frameworkType") is not None:
doc.framework_type = data["frameworkType"]
if data.get("caseVersion") is not None:
doc.case_version = data["caseVersion"]
doc.case_version = _validate_case_version(data["caseVersion"], f"CFDocument '{doc.identifier}'", warnings)
if data.get("notes") is not None:
doc.notes = data["notes"]
if data.get("extensions") is not None:
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/test_case_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
_normalize_v1p0_package,
_parse_sequence_number,
_validate_association,
_validate_case_version,
_validate_cf_package,
fetch_cf_package,
import_case_from_dict,
Expand Down Expand Up @@ -378,6 +379,30 @@ def test_empty_title(self):
_validate_cf_package(pkg)


class TestCaseVersionValidation:
def test_v11_no_warning(self):
warnings: list[str] = []
assert _validate_case_version("1.1", "CFDocument 'x'", warnings) == "1.1"
assert warnings == []

def test_v10_no_warning(self):
warnings: list[str] = []
assert _validate_case_version("1.0", "CFDocument 'x'", warnings) == "1.0"
assert warnings == []

def test_none_no_warning(self):
warnings: list[str] = []
assert _validate_case_version(None, "CFDocument 'x'", warnings) is None
assert warnings == []

def test_unexpected_value_warns_but_keeps_value(self):
warnings: list[str] = []
# Value is preserved (round-trip fidelity); only a warning is emitted.
assert _validate_case_version("2.0", "CFDocument 'x'", warnings) == "2.0"
assert len(warnings) == 1
assert "caseVersion" in warnings[0]


class TestAssociationValidation:
def test_valid_association(self):
assoc = _make_association()
Expand Down
Loading