Skip to content

feat: add REST API convention building blocks for Open edX endpoints - #573

Open
Abdul-Muqadim-Arbisoft wants to merge 7 commits into
openedx:masterfrom
edly-io:feat/rest-api-conventions-building-blocks
Open

feat: add REST API convention building blocks for Open edX endpoints#573
Abdul-Muqadim-Arbisoft wants to merge 7 commits into
openedx:masterfrom
edly-io:feat/rest-api-conventions-building-blocks

Conversation

@Abdul-Muqadim-Arbisoft

@Abdul-Muqadim-Arbisoft Abdul-Muqadim-Arbisoft commented Aug 27, 2026

Copy link
Copy Markdown

Adds the reusable core of the FC-0118 REST API conventions work to this library, as discussed in the OEP-69 review (openedx/openedx-proposals#805 (comment)). Until now the reference implementation was split: pagination and JWT auth live here, but the error envelope, response shaping, and test helpers live inside edx-platform where plugins and other IDAs can't reach them. This PR moves the pieces that were agreed as feasible to extract without dragging platform coupling along:

errors.py: the ADR 0029 standardized error envelope: standardized_error_exception_handler, the Conflict (409) exception, the envelope formatters, the error-type URI catalog with a register_error_type extension helper, and a plain-DRF ErrorResponseSerializer for documenting error responses in OpenAPI schemas.
mixins.py: StandardizedErrorMixin, the per-view opt-in for the handler.
shaping.py: ADR 0036 response shaping: project() for ?fields= top-level field selection and MinimalViewMixin for the ?view=minimal preset.
paginators.py (existing): paginate_manually() and IterablePaginationMixin, so endpoints outside DRF's generic list machinery can return the ADR 0032 envelope without hand-writing the paginate/serialize/respond sequence.
routers.py: opaque-key lookup_value_regex constants (COURSE_KEY_LOOKUP_REGEX, USAGE_KEY_LOOKUP_REGEX), which every router-registered ViewSet keyed by an opaque key currently hand-writes.
url_converters.py: the ADR 0038 (URL structure standardization) shared Django path converters: CourseKeyConverter and UsageKeyConverter, registered once per service via register_url_converters() as <course_key:...> / <usage_key:...>. Conforming routes resolve opaque keys in the URLconf, views receive parsed keys, and malformed or deprecated (Org/Course/Run, i4x://) keys become routing-level 404s. Extracted here per the ADR's "Code examples" section and the ADR review request (openedx/openedx-platform#39003 (comment) — openedx/openedx-platform#39003 (comment)); non-deprecated-key APIs use the converters, while endpoints that must keep serving Old Mongo content keep the permissive routers.py lookup regexes. Behaviour verified identical to the edx-platform copy it replaces, on Django 4.2 and 5.2.
testing.py: assert_error_envelope(), one assertion for consumer test suites instead of the per-field loops the pilot tests repeat.
The one real obstacle was the handler's import of ignored_error_exception_handler from platform internals. That coupling is now inverted: the handler delegates to a base handler resolved from a new EDX_DRF_EXTENSIONS['STANDARDIZED_ERROR_BASE_HANDLER'] setting (dotted path or callable, defaulting to DRF's own exception_handler), so edx-platform keeps its error monitoring purely through configuration. A guard test enforces that nothing in this package imports from edx-platform.

No new dependencies (url_converters depends only on edx-opaque-keys, already in base.in). Bumps version to 10.8.0.

Move the reusable core of edx-platform's standardized error envelope
(openedx/core/lib/api/exceptions.py + mixins.py) into the library, per
edx-platform ADR 0039 (extract the REST API reference implementation):

* errors.py — standardized_error_exception_handler, the Conflict (409)
  exception, the envelope formatters (build_error_envelope, flatten_detail,
  normalize_validation_errors), the error-type URI catalog
  (ERROR_TYPE_BASE_URI, error_type_uri, classify_error) and its
  register_error_type extension helper, and ErrorResponseSerializer for
  OpenAPI response declarations.
* mixins.py — StandardizedErrorMixin, the per-view opt-in for the handler.

The handler's platform coupling is inverted: instead of importing
ignored_error_exception_handler from openedx.core.lib.request_utils, it
delegates to a base handler resolved from the new
EDX_DRF_EXTENSIONS['STANDARDIZED_ERROR_BASE_HANDLER'] setting (a dotted
path or callable, defaulting to DRF's exception_handler), so edx-platform
keeps its error-monitoring behavior via configuration.

A guard test enforces that nothing in this package imports from
edx-platform (ADR 0039 rule 9).
Generalize the response-shaping patterns the FC-0118 pilot APIs each
hand-wrote (apply_field_selection in contentstore v3, the ?view=minimal
helpers in enrollments v2) into a shared shaping module:

* project(data, fields) — keep only the named top-level keys of a
  serialized object; accepts an iterable or the raw ?fields= CSV value.
* MinimalViewMixin — the ?view=minimal response preset. Views set
  minimal_fields for a plain projection or override
  to_minimal_representation for shapes a projection cannot express (such
  as collapsing an embedded sub-object to its id), and apply it with
  shape_minimal() just before building the response. An unconfigured view
  raises ImproperlyConfigured rather than silently returning the full
  payload.
ADR 0032 requires the standard pagination envelope even on endpoints
whose data does not flow through DRF's generic list machinery — plain
ViewSet actions and views that assemble their items by hand (the
enrollments v2 list action being the reference case in edx-platform).

* paginate_manually(request, items, serialize, ...) — the shared spelling
  of the paginate-serialize-respond sequence those endpoints hand-write.
* IterablePaginationMixin — view-level wrapper; paginate_iterable()
  defaults to the view's get_serializer and DefaultPagination (the
  ADR 0032 seven-field envelope) via pagination_class.
DRF routers default lookup_value_regex to [^/.]+, which cannot match
Open edX opaque keys, so every router-registered ViewSet keyed by a
course or usage key hand-writes its own pattern (contentstore v3
course-details and grading, the v1 xblock view, cms course_runs).

Provide the shared spellings — COURSE_KEY_LOOKUP_REGEX and
USAGE_KEY_LOOKUP_REGEX — matching the patterns openedx/core/constants.py
has used in URL routes, with capturing groups made non-capturing since
router regexes are embedded inside the named lookup group. The patterns
delimit the key within the URL path; real validation stays with
opaque_keys in the view.
Give consumer test suites a single assertion that a response carries a
well-formed ADR 0029 error envelope, replacing the per-field loops the
FC-0118 pilot tests repeat (e.g. enrollments v2 test_envelope.py):
required fields present, body status matching the response status, the
type URI belonging to the https://docs.openedx.org/errors/ catalog,
instance matching the request path when available, and no legacy
DeveloperErrorViewMixin fields leaking through.
Add url_converters.py — CourseKeyConverter and UsageKeyConverter, the shared Django path converters edx-platform ADR 0038 (rule 9) requires for conforming API routes, plus register_url_converters() to register them once per service as <course_key:...> / <usage_key:...>. Views receive parsed keys; malformed keys and the deprecated Org/Course/Run and i4x:// forms become routing-level 404s. Endpoints that must keep serving Old Mongo content keep the permissive routers.py lookup regexes instead.

Extracted here per the ADR's 'Code examples' section (the converters depend only on edx-opaque-keys, already a base dependency, so the 10.8.0 'no new dependencies' claim holds) and the ADR review request (openedx-platform#39003, r3833688640). Behaviour verified identical to the edx-platform copy it replaces, on Django 4.2 and 5.2. Tests pin what the ADR records as verified: resolve()/reverse() round-trips (parsed and string forms), 404 for malformed and deprecated keys, NoReverseMatch for a deprecated key, and two converters in one route. The CCX case skips where the optional ccx-keys package is absent, since the converter accepts whichever key types the host installs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant