Skip to content
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

Local content item support #4366

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open

Conversation

KludgeKML
Copy link
Contributor

@KludgeKML KludgeKML commented Nov 1, 2024

⚠️ This repo is Continuously Deployed: make sure you follow the guidance ⚠️

What

Adds more structured support for loading content items from the local machine. By setting an environment variable and putting a content item in a particular directory, it will be loaded from that file instead of from a call to the content store.

Why

There's currently a restricted version of this code working for Landing Pages (it only loads the blocks from the fixture, but includes fake values for the rest of the content item). This scaffolding has proved useful during Landing Pages development, but it's limited in scope and only works for Landing Pages. By replacing it with a more robust option, we can improve developer experience in the absence of a local version of content store, and make it easier to demo experimental content items on Heroku.

https://trello.com/c/FytCkByy/383-improve-contentitem-offline-loading

How

We add a new singleton class (ContentItemLoader) that is responsible for calls to GdsApi.content_store.get_content - this allows us to have a cache of items (simplifying the current Format Constraint code, which caches responses in the request env field), and to have a single point where those calls can be intercepted and swapped out for local code if the required environment variable is set. Setting ALLOW_LOCAL_CONTENT_ITEM_OVERRIDE to true will cause the loader to look in /config/local-content-items/path/to/the/item for any call to the content store (falling back to an actual call if the file isn't present).

See Also:

@govuk-ci govuk-ci temporarily deployed to govuk-frontend-app-pr-4366 November 1, 2024 09:55 Inactive
@govuk-ci govuk-ci temporarily deployed to govuk-frontend-app-pr-4366 November 4, 2024 13:05 Inactive
@govuk-ci govuk-ci temporarily deployed to govuk-frontend-app-pr-4366 November 4, 2024 13:07 Inactive
@KludgeKML KludgeKML marked this pull request as ready for review November 5, 2024 15:54
Copy link
Contributor

@leenagupte leenagupte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea in principle. I have a few inline comments that I think need to be fixed before this will work.

lib/content_item_loader.rb Outdated Show resolved Hide resolved

To support this, set:

`ALLOW_LOCAL_CONTENT_ITEM_OVERRIDE=true`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be good if there was an accompanying change in govuk-docker that sets this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the govuk-docker PR. This env var should also be added to the --live option in the startup script.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (and added it to app.json)

# SCAFFOLDING: can be removed when basic content items are available
# from content-store
def old_scaffolding_content_item
ContentItemLoaderGdsApi.content_store.content_item(request.path).to_h
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right. There isn't actually a class called ContentItemLoaderGdsApi is there?

The tests are passing, so either this is fine somehow, or this bit of code is not being tested.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, not sure what was going on here. I've fixed it now!

Copy link
Contributor

@leenagupte leenagupte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a bit more time to review, so had a closer look at the ContentItemLoader class and added a few inline suggestions for refactoring it.

There's also a request for some tests around the scaffolding if this is going to become a permanent feature.

There's nothing major though and the overall approach is great 🎉


To support this, set:

`ALLOW_LOCAL_CONTENT_ITEM_OVERRIDE=true`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the govuk-docker PR. This env var should also be added to the --live option in the startup script.

end

def local_file?(base_path)
File.exist?(local_json_filename(base_path)) || File.exist?(local_yaml_filename(base_path))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that you're doing this check twice. Once here and once in load_local_file.

It probably means that this class could do with a refactor.

Maybe split out the if clause in the load method and start from there?

e.g.

    def load(base_path)
      cache[base_path] ||= if use_local_file? && yaml_file?(base_path)
                             load_yaml_file(base_path)
                           elsif use_local_file? && json_file?(base_path)
                             load_json_file(base_path)
                           else
                             begin
                               GdsApi.content_store.content_item(base_path)
                             rescue GdsApi::HTTPErrorResponse, GdsApi::InvalidUrl => e
                               e
                             end
                           end
    end

Then you wouldn't need to redo the checks in the load_file methods.

It'd also allow you to split up the load_local_file method into the bits that load the file and create the gds-api response

    def load_yaml_file(base_path)
      file_name = yaml_filename(base_path)
      Rails.logger.debug("Loading content item #{base_path} from #{file_name}")
      contents = YAML.load(File.read(file_name)).to_json

      gds_api_response(contents)
    end

    def load_json_file(base_path)
      file_name = json_filename(base_path)
      Rails.logger.debug("Loading content item #{base_path} from #{file_name}")
      contents = File.read(file_name)

      gds_api_response(contents)
    end

    def gds_api_response(body)
      GdsApi::Response.new(OpenStruct.new(code: 200, body:, headers: { cache_control: "max-age=0, public", expires: "" }))
    end

I think also that limiting the use of local in the method names will make them a bit shorter and easier to read.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the methods apart from load should be made private as they're not being used or tested elsewhere.

# SCAFFOLDING: can be removed when basic content items are available
# from content-store
def old_scaffolding_content_item
result = ContentItemLoader.load(request.path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's great that you've fixed this. I think it would be good to add a test for this scaffolding too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The good news is that the existing request tests for landing_page already cover the old scaffolding (tests written... checks blame ... one @leenagupte)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can't have been thorough enough because nothing caught the attempted call to ContentItemLoaderGdsApi.content_store.content_item(request.path).to_h which doesn't exist!

@govuk-ci govuk-ci temporarily deployed to govuk-frontend-app-pr-4366 November 14, 2024 11:41 Inactive
@govuk-ci govuk-ci temporarily deployed to govuk-frontend-app-pr-4366 November 14, 2024 11:42 Inactive
- ContentItemLoader replaces direct calls to GdsApi.content_store.content_item,
  (of which there are only 4 in the rest of the codebase), and provides two
  functions.
- First, it centralises caching of calls, so that that can be removed
  from the FormatRoutingConstraint classes and using request.env to save those
  values can be simplified.
- Second, if the ALLOW_LOCAL_CONTENT_ITEM_OVERRIDE env var is set true, it
  allows loading content items from a file in /lib/data/local-content-items,
  which gives developers an extra option for local development or preview apps
  when working with content types that have not crystalised yet or where
  publishing support is not yet present. It should not be used in production.
- The load method is slightly odd in that it will either return an API response
  or an exception (note: return the exception, not raise it!). This allows us
  to cache errors in a similar way to the way the routing constraints used to.
  It might be there are better ways to handle this, but for the moment this is
  a minimal change to maintain the current behaviour.
@govuk-ci govuk-ci temporarily deployed to govuk-frontend-app-pr-4366 November 14, 2024 12:32 Inactive
- Because the cache is at class level, it's very aggressive and would otherwise interfere with what people would normally expect about tests (ie that in two unrelated tests you could use the same slug to point to different things). So we default here to just clearing the cache before each test.
- Do it manually in shared tests, which have their own setup that might conflict with "before" blocks in the system specs that call them.
- Now that ContentItemLoader is handling the caching, we can remove that layer of code / responsibility from the constraints.
- ContentItemLoader either returns the adapter response or the error that it caught, so querying for those classes gives us a "was it or wasn't it an error" check.
- We also simplify the spec tests slightly to use more idiomatic RSpec.
- Now that ContentItemLoader handles caching we can use that rather than the stuff that the format constraints were putting into the request env.
- LandingPageController's scaffolding is still needed for the moment, but we can remove it soon (since part of this project is about replacing that scaffolding with a more generally useful one).
- Content items in controllers are now always loaded from `content_item_path`, which defaults to the request path, and is overridden if necessary for items with multiple paths on one content item.
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.

3 participants