-
Notifications
You must be signed in to change notification settings - Fork 94
Add README documentation for lib/common library components #27784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AdamKing0126
wants to merge
5
commits into
master
Choose a base branch
from
ak/common-library-readme-updates
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+249
−12
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
e907c66
Add README documentation for common library components
AdamKing0126 539d8d4
Initial plan for applying review feedback to README documentation
Copilot 8c6c788
Apply reviewer feedback to lib/common README documentation
Copilot cbc7a43
Revert accidentally committed Gemfile.lock and db/schema.rb changes
Copilot f910f07
Merge branch 'master' into ak/common-library-readme-updates
AdamKing0126 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,94 @@ | ||
| ## Common::Exceptions | ||
| ## Common | ||
|
|
||
| Common::Exceptions is a library intended to help make exception classes serializable. | ||
| This library provides shared utilities, base classes, and infrastructure used across vets-api. The components here are designed to be composable — most can be used independently or together. | ||
|
|
||
| It is particularly suited for helping to render JSONAPI style errors. It is divided into | ||
| two types, `internal` exceptions that are raised within the various models and controllers | ||
| of your Rails application and `external` exceptions that are raised by backend services. | ||
| ## Components | ||
|
|
||
| The external exceptions will need to be setup properly using custom middlewares. | ||
| ### Common::Exceptions | ||
|
|
||
| ## Purpose | ||
| A library of serializable exception classes designed to render JSONAPI-compliant error responses. Exceptions are divided into internal exceptions (raised within models and controllers) and external exceptions (raised by backend service middleware). Messages and HTTP status codes are configured via i18n locales. | ||
|
|
||
| To be able to jump out of the call stack and render an error response as part of an | ||
| orchestration layer for various backend service integrations. | ||
| See [exceptions/README.md](exceptions/README.md) for the full class hierarchy and usage. | ||
|
|
||
| ## Usage | ||
| ### Common::Client | ||
|
|
||
| Each exception class has a unique way of being invoked. To customize the messages for these | ||
| error classes an i18n locales file is available. | ||
| A library for building backend service integrations. Provides a base client class, configuration conventions, Faraday middleware for key casing and error handling, and optional Redis-backed token session management. | ||
|
|
||
| See [client/README.md](client/README.md) for setup and usage. | ||
|
|
||
| ### Common::Models | ||
|
|
||
| Base classes and mixins for non-ActiveRecord models: attribute declaration via Virtus, filterable and sortable collections with optional Redis caching, a Redis-backed persistence class, and an immutable value object base via Dry::Struct. | ||
|
|
||
| See [models/README.md](models/README.md) for the full class reference. | ||
|
|
||
| ### Common::FileHelpers | ||
|
|
||
| Utility methods for working with temporary files, particularly in the context of file uploads and ClamAV virus scanning. | ||
|
|
||
| ```ruby | ||
| path = Common::FileHelpers.random_file_path('.pdf') | ||
| path = Common::FileHelpers.generate_random_file(file_bytes, '.pdf') | ||
| path = Common::FileHelpers.generate_clamav_temp_file(file_bytes, file_name) | ||
| Common::FileHelpers.delete_file_if_exists(path) | ||
| ``` | ||
|
|
||
| `generate_clamav_temp_file` writes to `clamav_tmp/` specifically, which is the directory ClamAV has permission to scan. | ||
|
|
||
| ### Common::HashHelpers | ||
|
|
||
| Utilities for deep transformations on nested hashes, arrays, and `ActionController::Parameters`. | ||
|
|
||
| ```ruby | ||
| Common::HashHelpers.deep_transform_parameters!(params) { |k| k.underscore } | ||
| Common::HashHelpers.deep_remove_blanks(hash) # removes blank values (but not false) | ||
| Common::HashHelpers.deep_compact(hash) # removes nil values | ||
| Common::HashHelpers.deep_to_h(open_struct) # recursively converts OpenStructs to hashes | ||
| ``` | ||
|
|
||
| ### Common::PdfHelpers | ||
|
|
||
| Utilities for working with PDF files. Currently provides PDF unlocking/decryption via HexaPDF. Raises `Common::Exceptions::UnprocessableEntity` on invalid password or corrupt PDF, with the error detail drawn from i18n. | ||
|
|
||
| ```ruby | ||
| Common::PdfHelpers.unlock_pdf(input_file, password, output_file) | ||
| ``` | ||
|
|
||
| ### Common::S3Helpers | ||
|
|
||
| Handles S3 file uploads, using `Aws::S3::TransferManager` when available and falling back to a basic upload otherwise. | ||
|
|
||
| ```ruby | ||
| Common::S3Helpers.upload_file( | ||
| s3_resource: s3, | ||
| bucket: 'my-bucket', | ||
| key: 'path/to/file.pdf', | ||
| file_path: '/tmp/local_file.pdf', | ||
| content_type: 'application/pdf', | ||
| server_side_encryption: 'AES256' | ||
| ) | ||
| ``` | ||
|
|
||
| Pass `return_object: true` to get the `Aws::S3::Object` back instead of `true`. | ||
|
|
||
| ### Common::VirusScan | ||
|
|
||
| ClamAV-based virus scanning for uploaded files. Scans files from `clamav_tmp/` directly, or copies files from other locations when the `clamav_scan_file_from_other_location` Flipper flag is enabled. Emits structured audit log entries for every scan attempt, including file metadata, scan result, duration, and upload context. | ||
|
|
||
| ```ruby | ||
| is_safe = Common::VirusScan.scan(file_path, upload_context: 'form_526') | ||
| ``` | ||
|
|
||
| Returns `true` only when the file is confirmed clean. Returns `false` when the file is not confirmed clean, including when it is infected and when scanning is skipped or blocked (for example, if the file is outside `clamav_tmp/` and `clamav_scan_file_from_other_location` is disabled). | ||
|
|
||
| Mock mode can be enabled via `Settings.clamav.mock`, but because Settings/Parameter Store values may arrive as strings or integers, callers should cast it to a real boolean before use (for example, `ActiveModel::Type::Boolean.new.cast(Settings.clamav.mock)`). | ||
|
|
||
| ### Common::ConvertToPdf | ||
|
|
||
| Converts image files to PDF using MiniMagick. Always writes the uploaded content to a ClamAV temp file first. For files that are already PDFs, returns the path to that temp-file copy (the caller is responsible for cleaning it up). For image files, converts to PDF and returns the output path (the intermediate temp file is cleaned up automatically). Raises `IOError` for unsupported file types. | ||
|
|
||
| ```ruby | ||
| output_path = Common::ConvertToPdf.new(uploaded_file).run | ||
| ``` | ||
|
|
||
| The output file is written to a random temp path. The caller is responsible for cleaning it up after use. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| ## Common::Exceptions | ||
|
|
||
| This directory contains the full exception hierarchy used across vets-api. All exceptions are designed to be serializable as JSONAPI-compliant error responses. | ||
|
|
||
| ## Purpose | ||
|
|
||
| To provide a consistent, structured set of exception classes that can be raised anywhere in the application and reliably rendered as well-formed error responses. Each exception maps to an i18n key in `config/locales/exceptions.en.yml`, which controls the title, detail, HTTP status code, and whether the error gets reported to the error tracking backend. | ||
|
|
||
| ## Class Hierarchy | ||
|
|
||
| ### Base Classes | ||
|
|
||
| **`BaseError`** — the root class all exceptions inherit from. Defines the interface (`errors`, `status_code`, `message`) and i18n lookup conventions. Subclasses must implement `errors`. Also exposes `log_to_sentry?` (legacy naming — controls whether the exception is reported to the error tracking backend). | ||
|
|
||
| **`SerializableError`** — a plain Ruby object (not an exception itself) that wraps error attributes into a JSONAPI-compatible structure. Fields include `title`, `detail`, `code`, `status`, `source`, `links`, and `meta`. Used internally by exception classes when building their `errors` array. | ||
|
|
||
| **`BackendServiceException`** — raised by middleware when a backend service returns an error response. Accepts an i18n key (e.g. `RX139`, `EVSS144`) to look up the appropriate response shape. Falls back to `VA900` for unmapped errors. Stores the original status and body from the upstream response for debugging. | ||
|
|
||
| ### 4xx Client Errors | ||
|
|
||
| | Class | HTTP Status | Description | | ||
| |-------|-------------|-------------| | ||
| | `AmbiguousRequest` | 400 | Request cannot be disambiguated | | ||
|
AdamKing0126 marked this conversation as resolved.
|
||
| | `BadRequest` | 400 | Generic bad request | | ||
| | `FilterNotAllowed` | 400 | A filter parameter is not permitted for this resource | | ||
| | `InvalidField` | 400 | A field name in the request is not recognized | | ||
| | `InvalidFieldValue` | 400 | A field value in the request is invalid | | ||
| | `InvalidFiltersSyntax` | 400 | Filter parameter syntax is malformed | | ||
| | `InvalidPaginationParams` | 400 | Pagination parameters are out of range or invalid | | ||
| | `InvalidResource` | 400 | The requested resource type is not valid | | ||
| | `InvalidSortCriteria` | 400 | Sort parameter refers to a non-sortable field | | ||
| | `MaxArraySizeExceeded` | 400 | An array parameter exceeds the allowed size | | ||
| | `NoQueryParamsAllowed` | 400 | Query parameters were provided but are not allowed | | ||
| | `ParameterMissing` | 400 | A required parameter is absent | | ||
| | `ParametersMissing` | 400 | Multiple required parameters are absent | | ||
| | `ValidationErrors` | 422 | Model validation failed; wraps ActiveModel errors | | ||
| | `ValidationErrorsBadRequest` | 400 | Validation failure rendered as 400 instead of 422 | | ||
| | `DetailedSchemaErrors` | 422 | Schema validation failure with field-level detail | | ||
| | `SchemaValidationErrors` | 422 | JSON schema validation failure | | ||
| | `UnprocessableEntity` | 422 | Generic unprocessable entity | | ||
| | `UpstreamUnprocessableEntity` | 422 | Upstream service returned 422 | | ||
| | `Unauthorized` | 401 | Authentication required | | ||
| | `TokenValidationError` | 401 | Token is invalid or expired | | ||
| | `MessageAuthenticityError` | 403 | CSRF or message authenticity check failed | | ||
| | `Forbidden` | 403 | Authenticated but not authorized | | ||
| | `UnexpectedForbidden` | 403 | Forbidden response was not expected in this context | | ||
| | `RecordNotFound` | 404 | A specific record could not be found | | ||
| | `ResourceNotFound` | 404 | A resource type or endpoint could not be found | | ||
| | `RoutingError` | 404 | No route matched the request | | ||
| | `PayloadTooLarge` | 413 | Request body exceeds the allowed size | | ||
| | `TooManyRequests` | 429 | Rate limit exceeded | | ||
| | `FailedDependency` | 424 | A required dependency failed | | ||
|
|
||
| ### 5xx Server and Service Errors | ||
|
|
||
| | Class | HTTP Status | Description | | ||
| |-------|-------------|-------------| | ||
| | `InternalServerError` | 500 | Generic server error | | ||
| | `NotImplemented` | 501 | Endpoint or feature is not yet implemented | | ||
| | `BadGateway` | 502 | Upstream service returned an unexpected response | | ||
| | `ServiceUnavailable` | 503 | Upstream service is not available | | ||
| | `ServiceOutage` | 503 | A known service outage is in effect | | ||
| | `GatewayTimeout` | 504 | Upstream service did not respond in time | | ||
| | `Timeout` | 500 | Generic timeout | | ||
| | `ClientDisconnected` | 499 | Client disconnected before the response was sent | | ||
| | `ExternalServerInternalServerError` | 500 | An external service returned a 500 | | ||
|
AdamKing0126 marked this conversation as resolved.
|
||
| | `OpenIdServiceError` | `%{status}` | OpenID Connect service error with caller-supplied HTTP status | | ||
| | `ServiceError` | 500 | Generic service-level error from a backend integration | | ||
| | `UpstreamPartialFailure` | 502 | Upstream returned partial data; used to signal degraded responses | | ||
| | `PrescriptionRefillResponseMismatch` | 502 | Prescription service response did not match expected shape | | ||
|
|
||
| ## Usage | ||
|
|
||
| Raise exceptions directly, or via middleware. Each class has a unique initializer — check the individual file for accepted arguments. | ||
|
|
||
| ```ruby | ||
| raise Common::Exceptions::RecordNotFound, @prescription.id | ||
| raise Common::Exceptions::UnprocessableEntity.new(detail: 'Password is incorrect', source: 'MyService') | ||
| raise Common::Exceptions::BackendServiceException.new('RX139', { detail: message }, status, body) | ||
| ``` | ||
|
|
||
| To customize the message, detail, or error reporting behavior for any exception, add or update its key in `config/locales/exceptions.en.yml`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| ## Common model classes | ||
|
|
||
| This directory provides base classes and mixins under the `Common` namespace for building serializable, sortable, filterable, and Redis-backed data models across vets-api. | ||
|
|
||
| ## Purpose | ||
|
|
||
| To establish consistent conventions for non-ActiveRecord models: how they declare attributes, how collections of them are paginated and filtered, and how short-lived objects are stored and retrieved from Redis. | ||
|
|
||
| ## Classes | ||
|
|
||
| ### `Common::Base` | ||
|
|
||
| The foundation for non-ActiveRecord model objects. Includes `ActiveModel::Serialization`, `Virtus.model`, and `Comparable`. Subclasses declare typed attributes via Virtus and can mark them as `sortable` or `filterable` for use with `Common::Collection`. | ||
|
|
||
| ```ruby | ||
| class Prescription < Common::Base | ||
| attribute :prescription_id, Integer, sortable: { order: 'ASC', default: true } | ||
| attribute :status, String, filterable: %w[eq not_eq] | ||
| attribute :dispensed_date, Common::UTCTime, sortable: { order: 'DESC' } | ||
| end | ||
| ``` | ||
|
|
||
| Tracks attribute changes (`changed?`, `changed`, `changes`) relative to the values at initialization. | ||
|
|
||
| ### `Common::Collection` | ||
|
|
||
| A wrapper around an array of model objects that adds filtering, sorting, and pagination. Accepts a `cache_key` to transparently back the collection with Redis (1-hour TTL by default). | ||
|
|
||
| ```ruby | ||
| collection = Common::Collection.fetch(Prescription, cache_key: cache_key, ttl: 3600) do | ||
| { data: prescriptions, metadata: {}, errors: {} } | ||
| end | ||
|
|
||
| collection.find_by(status: { eq: 'active' }) | ||
| .sort('-dispensed_date') | ||
| .paginate(page: 1, per_page: 10) | ||
| ``` | ||
|
|
||
| Filtering operators: `eq`, `not_eq`, `lteq`, `gteq`, `match`. Only attributes declared as `filterable` on the model class can be used. Sort fields must be declared as `sortable`. | ||
|
|
||
| ### `Common::RedisStore` | ||
|
|
||
| An ActiveModel-compatible base class for objects that are persisted to Redis rather than a relational database. Subclasses configure their namespace, TTL, and key attribute using class-level declarations. | ||
|
|
||
| ```ruby | ||
| class MySession < Common::RedisStore | ||
| redis_store 'my_session' | ||
| redis_ttl 3600 | ||
| redis_key :token | ||
|
|
||
| attribute :token, String | ||
| attribute :user_id, Integer | ||
| end | ||
|
|
||
| session = MySession.find(token) | ||
| session = MySession.find_or_build(token) | ||
| session.save | ||
| session.destroy | ||
| ``` | ||
|
|
||
| Requires `redis_store` and `redis_key` to be set. Objects are serialized to JSON via Oj. Invalid objects are automatically removed from Redis on retrieval. | ||
|
|
||
| ### `Common::Resource` | ||
|
|
||
| A thin subclass of `Dry::Struct` for immutable, typed value objects. Prefer this over `Common::Base` when the object should be strictly typed and immutable by design. | ||
|
|
||
| ```ruby | ||
| class AddressInfo < Common::Resource | ||
| attribute :street, Types::Strict::String | ||
| attribute :city, Types::Strict::String | ||
| end | ||
| ``` | ||
|
|
||
| ## Supporting Classes | ||
|
|
||
| **`Common::CacheAside`** — a concern for `Common::RedisStore` subclasses that implements the cache-aside pattern. Wraps a block, checks the cache first, and stores the result if the response reports itself as cacheable (`response.cache?`). | ||
|
|
||
| **`Common::UTCTime`** — a Virtus attribute type that coerces all time values to UTC. Use it in `Common::Base` subclasses to ensure consistent time handling. | ||
|
|
||
| **`Common::Ascending` / `Common::Descending`** — internal wrappers used by `Common::Collection#sort` to support multi-field sorting with mixed directions. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.