diff --git a/guides/conventions.md b/guides/conventions.md new file mode 100644 index 0000000000..b3e533df80 --- /dev/null +++ b/guides/conventions.md @@ -0,0 +1,5 @@ +# Conventions + +1. [Application Namespaces](conventions/namespaces.md) +2. [Rails Conventions](conventions/rails.md) +3. [File Structure](conventions/file-structure.md) diff --git a/guides/conventions/file-structure.md b/guides/conventions/file-structure.md new file mode 100644 index 0000000000..2bacf0b8c8 --- /dev/null +++ b/guides/conventions/file-structure.md @@ -0,0 +1,10 @@ +[Conventions](/guides/conventions.md) / + +# File Structure + +- [assets](file-structure/assets.md) +- [components](file-structure/components.md) +- [controllers](file-structure/controllers.md) +- [forms](file-structure/forms.md) +- [helpers](file-structure/helpers.md) +- [models](file-structure/models.md) diff --git a/guides/conventions/file-structure/assets.md b/guides/conventions/file-structure/assets.md new file mode 100644 index 0000000000..c819f5d456 --- /dev/null +++ b/guides/conventions/file-structure/assets.md @@ -0,0 +1,20 @@ +[Conventions](/guides/conventions.md) / [File Structure](/guides/conventions/file-structure.md) / + +# Assets + +Images, videos, and stylesheets are stored in the `assets` directory. + +``` +. +└ app + └─ assets + ├─ images + └─ stylesheets + ├─ [namespace] + │ └─ application.scss + └─ application.scss +``` + +The top-level `application.scss` is the shared stylesheet for all namespaces. + +The `find/application.scss` stylesheet should be included into the `layouts/find/application.html.erb` layout alongside the top-level `application.scss` stylesheet. diff --git a/guides/conventions/file-structure/components.md b/guides/conventions/file-structure/components.md new file mode 100644 index 0000000000..821f4cedfd --- /dev/null +++ b/guides/conventions/file-structure/components.md @@ -0,0 +1,19 @@ +[Conventions](/guides/conventions.md) / [File Structure](/guides/conventions/file-structure.md) / + +# View Components + +We use [View Components](https://viewcomponent.org/) to create reusable components in our Rails applications. + +``` +. +└ app + └─ components + ├─ [namespace] (Namespace-specific components) + │ ├─ [component_name].rb + │ └─ [component_name].html.erb + ├─ [model_name] (Model-specific components) + │ ├─ [component_name].rb + │ └─ [component_name].html.erb + ├─ [component_name].rb + └─ [component_name].html.erb +``` diff --git a/guides/conventions/file-structure/controllers.md b/guides/conventions/file-structure/controllers.md new file mode 100644 index 0000000000..9603ab144d --- /dev/null +++ b/guides/conventions/file-structure/controllers.md @@ -0,0 +1,67 @@ +[Conventions](/guides/conventions.md) / [File Structure](/guides/conventions/file-structure.md) / + +# Controllers + +We organise our controllers within our namespaces. + +We should follow resourceful controller conventions. + +``` +. +└ app + └─ controllers + ├─ [namespace] (Namespace-specific controllers) + │ ├─ [controller_name]_controller.rb + │ └─ application_controller.rb + └─ application_controller.rb +``` + +Each namespace has its own `ApplicationController` as a base for all controllers within that namespace. + +```ruby +# Top-level application controller. +# Shared functionality across all namespaces. +class ApplicationController < ActionController::Base + include Pundit::Authorization +end + +# Find application controller. +# Base controller for all Find controllers. +class Find::ApplicationController < ApplicationController +end + +# The API application controller inherits from ActionController::API. +# Base controller for all API controllers. +class API::ApplicationController < ActionController::API +end + +# The base controller for all v1 API controllers. +class API::V1::ApplicationController < API::ApplicationController +end +``` + +This allows us to set base functionality across groups of controllers. + +The pattern described in [ADR#6 Controller Structure](/guides/adr/0006-controller-structure.md) details a scalable pattern to managing nested resourceful controllers. + +```ruby +class Find::RecruitmentCycles::CoursesController < Find::ApplicationController + # GET /recruitment_cycles/:recruitment_cycle_id/courses + def index + @courses = recruitment_cycle.courses + end + + # GET /recruitment_cycles/:recruitment_cycle_id/courses/:id + def show + @course = recruitment_cycle.courses.find(params[:id]) + end + + private + + def recruitment_cycle + @recruitment_cycle ||= RecruitmentCycle.find(params[:recruitment_cycle_id]) + end +end +``` + +The `params[:id]` should always be the ID of the resource being acted upon. Parent resources should always be prefixed, e.g. `params[:recruitment_cycle_id]`. diff --git a/guides/conventions/file-structure/file-structure.md b/guides/conventions/file-structure/file-structure.md new file mode 100644 index 0000000000..2bacf0b8c8 --- /dev/null +++ b/guides/conventions/file-structure/file-structure.md @@ -0,0 +1,10 @@ +[Conventions](/guides/conventions.md) / + +# File Structure + +- [assets](file-structure/assets.md) +- [components](file-structure/components.md) +- [controllers](file-structure/controllers.md) +- [forms](file-structure/forms.md) +- [helpers](file-structure/helpers.md) +- [models](file-structure/models.md) diff --git a/guides/conventions/file-structure/forms.md b/guides/conventions/file-structure/forms.md new file mode 100644 index 0000000000..1e7ffdae9e --- /dev/null +++ b/guides/conventions/file-structure/forms.md @@ -0,0 +1,26 @@ +[Conventions](/guides/conventions.md) / [File Structure](/guides/conventions/file-structure.md) / + +# Form Objects + +We use form objects to encapsulate the validation and parameter handling of forms in our application. + +We organise our form objects within our namespaces and shared forms at the top-level. + +``` +. +└ app + └─ forms + ├─ [namespace] (Namespace-specific forms) + │ └─ [form_name]_form.rb + └─ application_form.rb +``` + +We have a single `ApplicationForm` base Form Object class. This class is used to set the foundations of shared logic across all form objects. + +```ruby +# Top-level application form. +# Shared functionality across all form objects. +class ApplicationForm + include ActiveModel::Model +end +``` diff --git a/guides/conventions/file-structure/helpers.md b/guides/conventions/file-structure/helpers.md new file mode 100644 index 0000000000..0a90600f42 --- /dev/null +++ b/guides/conventions/file-structure/helpers.md @@ -0,0 +1,40 @@ +[Conventions](/guides/conventions.md) / [File Structure](/guides/conventions/file-structure.md) / + +# View Helpers + +View helpers are used to define presentational helpers, especially those that contain HTML. Many times, you'll find yourself needing to manipulate primitive data from models, in this case, put the presentational methods on the models themselves. + +``` +. +└ app + └─ helpers + ├─ [namespace] (Namespace-specific forms) + │ └─ [helper_name]_helper.rb + └─ application_helper.rb +``` + +There is no inheritance between helpers, so you can define a helper method in any helper file and use it in any view. + +A helper file should be a collection of related helper methods. A helper file is structured around either a behavioural context or a resourceful context. + +Below is an example of a behavioural helper file: + +```ruby +module LinkHelper + def active_link_to(text, href) + link_to text, user_path(user), class: class_names("active", current_page?(href)) + end +end +``` + +Below is an example of a resourceful helper file, these helpers should be nested within the namespace of the resource: + +```ruby +module CourseHelper + def course_status_tag(course) + govuk_tag(course.status, color: "green") + end +end +``` + +All resourceful helpers should be prefixed with the resource name. diff --git a/guides/conventions/file-structure/models.md b/guides/conventions/file-structure/models.md new file mode 100644 index 0000000000..58fec339d1 --- /dev/null +++ b/guides/conventions/file-structure/models.md @@ -0,0 +1,31 @@ +[Conventions](/guides/conventions.md) / [File Structure](/guides/conventions/file-structure.md) / + +# Models + +We follow standard Rails conventions for models. Models are stored in the `app/models` directory. + +The `app/models` directory should contain only ActiveRecord models. Non-ActiveRecord PORO classes should be in the `app/lib` directory. + +``` +. +└ app + └─ models + ├─ [namespace] (Namespace-specific models) + │ └─ [model_name].rb + └─ application_record.rb +``` + +Models should contain the following logic: + +- Associations +- Validations +- Scopes +- Computed getter methods (Presentational methods) + +We should refrain from adding business logic to models. Business logic belongs in service objects classes. + +We should also refrain from adding callbacks to models. Callbacks are traditionally used to trigger side-effects and that logic belongs in service objects. + +Additionally, we should be aware of all mutation points of models. By calculating computed values within service objects, we can test side-effects and coupled attributes in isolation. + +We should limit scopes and define complex queries in query objects. diff --git a/guides/conventions/namespaces.md b/guides/conventions/namespaces.md new file mode 100644 index 0000000000..044b1cf8c7 --- /dev/null +++ b/guides/conventions/namespaces.md @@ -0,0 +1,26 @@ +[Conventions](/guides/conventions.md) / + +# Namespaces + +We use namespaces to organise our code into service-specific modules. This provides us with visibility of separation of concerns. + +We benefit from this separtation in the following ways: + +- Code under a namespace is intended to be used only by the service it belongs to. + + We can easily detect when code is used incorrectly outside of its namespace. + + This limits the blast radius of changes to a single service. For example, we can confidently make changes to a `Find` component and expect that it will only effect the "Find" service. + +- Explicit shared code. + + If code is shared, it is in the top-level namespace. + + We can visually determine how much code is shared between services by looking at the non-namespaced files. + +## Our namespaces + +- `Find` +- `Publish` +- `Support` +- `API` diff --git a/guides/conventions/rails.md b/guides/conventions/rails.md new file mode 100644 index 0000000000..cd69158276 --- /dev/null +++ b/guides/conventions/rails.md @@ -0,0 +1,66 @@ +[Conventions](/guides/conventions.md) / + +# Rails Architecture + +This document describes the Rails conventions of this application. + +## 1. File Structure + +Learn more about each of the directories' purposes in the [File Structure](conventions/file-structure.md) guide. + +``` +. +└ app + ├─ assets + ├─ components (View Components) + ├─ controllers + ├─ forms (Form Objects) + ├─ helpers + ├─ javascript + ├─ jobs + ├─ lib + ├─ mailers + ├─ models + ├─ policies + ├─ serializers + ├─ services (Service Objects) + ├─ validators + ├─ views + └─ wizards +``` + +The following directories are deprecated and should be removed: + +- `app/decorators` + + We should put decorator-type methods in models themselves. + +- `app/view_objects` + + We should use View Components (`app/components`) instead. + +## 2. Environments + +There are 2 kinds of environments we need to maintain: + +1. Rails environment (`RAILS_ENV`) + + There are 3 Rails environments: + + - `production` + - `test` + - `development` + +2. Application environment (`APP_ENV`) - This is commonly known as Hosting environment in other BAT applications. + + This environment is used to determine the settings of the application. + + There are 5 Application environments: + + - `review` + - `qa` + - `staging` + - `sandbox` + - `production` + + The `loadtest` and `rollover` environments are deprecated and should be removed. We can use `staging` temporarily for these use-cases.