Add support for missing posts

Posts, like 8661f4724aa9, can go missing if the account or post was
removed. In this case, the API returns data like this:

```json
{
  "data": {
    "post": null
  }
}
```

When this happens, we can detect it because the parsed response now has
a nil value: `response.data.post == nil` and construct an `EmptyPage`
instead of a `Page`. The `Articles::Show` action can then render
conditionally based on if the response from `PageConverter` is a `Page`
or an `EmptyPage`.
This commit is contained in:
Edward Loveall 2022-06-17 13:26:35 -04:00
parent 1dcded9153
commit f05a12a880
No known key found for this signature in database
GPG Key ID: A7606DFEC2BA731F
11 changed files with 102 additions and 59 deletions

View File

@ -1,5 +1,9 @@
2022-05-21 2022-05-21
* Show error page for missing posts
2022-05-21
* Remove the need for a fake DATABASE_URL * Remove the need for a fake DATABASE_URL
2022-04-04 2022-04-04

View File

@ -0,0 +1,15 @@
require "../../spec_helper"
include ActionHelpers
describe Articles::Show do
context "if the article is missing" do
it "should raise a MissingPageError" do
context = action_context(path: "/abc123")
expect_raises(MissingPageError) do
Articles::Show.new(context, params).call
end
end
end
end

View File

@ -17,8 +17,8 @@ describe PageConverter do
} }
] ]
JSON JSON
data_json = default_data_json(title, paragraph_json) data_json = default_post_json(title, paragraph_json)
data = PostResponse::Data.from_json(data_json) data = PostResponse::Post.from_json(data_json)
page = PageConverter.new.convert(data) page = PageConverter.new.convert(data)
@ -26,52 +26,48 @@ describe PageConverter do
end end
it "sets the author" do it "sets the author" do
data_json = <<-JSON post_json = <<-JSON
{ {
"post": { "title": "This is a story",
"title": "This is a story", "createdAt": 0,
"createdAt": 0, "creator": {
"creator": { "id": "abc123",
"id": "abc123", "name": "Author"
"name": "Author" },
}, "content": {
"content": { "bodyModel": {
"bodyModel": { "paragraphs": []
"paragraphs": []
}
} }
} }
} }
JSON JSON
data = PostResponse::Data.from_json(data_json) post = PostResponse::Post.from_json(post_json)
page = PageConverter.new.convert(data) page = PageConverter.new.convert(post)
page.author.name.should eq "Author" page.author.name.should eq "Author"
page.author.id.should eq "abc123" page.author.id.should eq "abc123"
end end
it "sets the publish date/time" do it "sets the publish date/time" do
data_json = <<-JSON post_json = <<-JSON
{ {
"post": { "title": "This is a story",
"title": "This is a story", "createdAt": 1000,
"createdAt": 1000, "creator": {
"creator": { "id": "abc123",
"id": "abc123", "name": "Author"
"name": "Author" },
}, "content": {
"content": { "bodyModel": {
"bodyModel": { "paragraphs": []
"paragraphs": []
}
} }
} }
} }
JSON JSON
data = PostResponse::Data.from_json(data_json) post = PostResponse::Post.from_json(post_json)
page = PageConverter.new.convert(data) page = PageConverter.new.convert(post)
page.created_at.should eq Time.utc(1970, 1, 1, 0, 0, 1) page.created_at.should eq Time.utc(1970, 1, 1, 0, 0, 1)
end end
@ -98,8 +94,8 @@ describe PageConverter do
} }
] ]
JSON JSON
data_json = default_data_json(title, paragraph_json) post_json = default_post_json(title, paragraph_json)
data = PostResponse::Data.from_json(data_json) data = PostResponse::Post.from_json(post_json)
page = PageConverter.new.convert(data) page = PageConverter.new.convert(data)
@ -115,23 +111,21 @@ def default_paragraph_json
"[]" "[]"
end end
def default_data_json( def default_post_json(
title : String = "This is a story", title : String = "This is a story",
paragraph_json : String = default_paragraph_json paragraph_json : String = default_paragraph_json
) )
<<-JSON <<-JSON
{ {
"post": { "title": "#{title}",
"title": "#{title}", "createdAt": 1628974309758,
"createdAt": 1628974309758, "creator": {
"creator": { "id": "abc123",
"id": "abc123", "name": "Author"
"name": "Author" },
}, "content": {
"content": { "bodyModel": {
"bodyModel": { "paragraphs": #{paragraph_json}
"paragraphs": #{paragraph_json}
}
} }
} }
} }

View File

@ -1,3 +0,0 @@
Spec.before_each do
AppDatabase.truncate
end

View File

@ -0,0 +1,12 @@
module ActionHelpers
private def action_context(path = "/")
io = IO::Memory.new
request = HTTP::Request.new("GET", path)
response = HTTP::Server::Response.new(io)
HTTP::Server::Context.new(request, response)
end
private def params
{} of String => String
end
end

View File

@ -6,8 +6,8 @@ class Articles::Show < BrowserAction
case post_id case post_id
in Monads::Just in Monads::Just
response = client_class.post_data(post_id.value!) response = client_class.post_data(post_id.value!)
page = PageConverter.new.convert(response.data) page = PageConverter.new.convert(response.data.post)
html ShowPage, page: page render_page(page)
in Monads::Nothing, Monads::Maybe in Monads::Nothing, Monads::Maybe
html( html(
Errors::ParseErrorPage, Errors::ParseErrorPage,
@ -18,6 +18,14 @@ class Articles::Show < BrowserAction
end end
end end
def render_page(page : Page)
html ShowPage, page: page
end
def render_page(page : MissingPage)
raise MissingPageError.new
end
def client_class def client_class
if use_local? if use_local?
LocalClient LocalClient

View File

@ -4,6 +4,10 @@ class Errors::Show < Lucky::ErrorAction
default_format :html default_format :html
dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError] dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError]
def render(error : MissingPageError)
error_html message: "This article is missing.", status: 404
end
def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError) def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError)
if html? if html?
error_html "Sorry, we couldn't find that page.", status: 404 error_html "Sorry, we couldn't find that page.", status: 404

View File

@ -1,20 +1,24 @@
class PageConverter class PageConverter
def convert(data : PostResponse::Data) : Page def convert(post : PostResponse::Post) : Page
title, content = title_and_content(data) title, content = title_and_content(post)
author = data.post.creator author = post.creator
created_at = Time.unix_ms(data.post.createdAt) created_at = Time.unix_ms(post.createdAt)
gist_store = gist_store(content) gist_store = gist_store(content)
Page.new( Page.new(
title: title, title: title,
author: author, author: author,
created_at: Time.unix_ms(data.post.createdAt), created_at: Time.unix_ms(post.createdAt),
nodes: ParagraphConverter.new.convert(content, gist_store) nodes: ParagraphConverter.new.convert(content, gist_store)
) )
end end
def title_and_content(data : PostResponse::Data) : {String, Array(PostResponse::Paragraph)} def convert(post : Nil) : MissingPage
title = data.post.title MissingPage.new
paragraphs = data.post.content.bodyModel.paragraphs end
def title_and_content(post : PostResponse::Post) : {String, Array(PostResponse::Paragraph)}
title = post.title
paragraphs = post.content.bodyModel.paragraphs
non_content_paragraphs = paragraphs.reject { |para| para.text == title } non_content_paragraphs = paragraphs.reject { |para| para.text == title }
{title, non_content_paragraphs} {title, non_content_paragraphs}
end end

View File

@ -0,0 +1,5 @@
class MissingPage
end
class MissingPageError < Exception
end

View File

@ -8,7 +8,7 @@ class PostResponse
end end
class Data < Base class Data < Base
property post : Post property post : Post?
end end
class Post < Base class Post < Base

View File

@ -1,3 +1,3 @@
module Scribe module Scribe
VERSION = "2022-05-21" VERSION = "2022-06-17"
end end