~edwardloveall/scribe

f05a12a880cbb1399925d9dc12317f6f5ce8a285 — Edward Loveall 17 days ago 1dcded9 main
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`.
M CHANGELOG => CHANGELOG +4 -0
@@ 1,5 1,9 @@
2022-05-21

* Show error page for missing posts

2022-05-21

* Remove the need for a fake DATABASE_URL

2022-04-04

A spec/actions/articles/show_spec.cr => spec/actions/articles/show_spec.cr +15 -0
@@ 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

M spec/classes/page_converter_spec.cr => spec/classes/page_converter_spec.cr +38 -44
@@ 17,8 17,8 @@ describe PageConverter do
        }
      ]
    JSON
    data_json = default_data_json(title, paragraph_json)
    data = PostResponse::Data.from_json(data_json)
    data_json = default_post_json(title, paragraph_json)
    data = PostResponse::Post.from_json(data_json)

    page = PageConverter.new.convert(data)



@@ 26,52 26,48 @@ describe PageConverter do
  end

  it "sets the author" do
    data_json = <<-JSON
    post_json = <<-JSON
      {
        "post": {
          "title": "This is a story",
          "createdAt": 0,
          "creator": {
            "id": "abc123",
            "name": "Author"
          },
          "content": {
            "bodyModel": {
              "paragraphs": []
            }
        "title": "This is a story",
        "createdAt": 0,
        "creator": {
          "id": "abc123",
          "name": "Author"
        },
        "content": {
          "bodyModel": {
            "paragraphs": []
          }
        }
      }
    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.id.should eq "abc123"
  end

  it "sets the publish date/time" do
    data_json = <<-JSON
    post_json = <<-JSON
      {
        "post": {
          "title": "This is a story",
          "createdAt": 1000,
          "creator": {
            "id": "abc123",
            "name": "Author"
          },
          "content": {
            "bodyModel": {
              "paragraphs": []
            }
        "title": "This is a story",
        "createdAt": 1000,
        "creator": {
          "id": "abc123",
          "name": "Author"
        },
        "content": {
          "bodyModel": {
            "paragraphs": []
          }
        }
      }
    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)
  end


@@ 98,8 94,8 @@ describe PageConverter do
        }
      ]
    JSON
    data_json = default_data_json(title, paragraph_json)
    data = PostResponse::Data.from_json(data_json)
    post_json = default_post_json(title, paragraph_json)
    data = PostResponse::Post.from_json(post_json)

    page = PageConverter.new.convert(data)



@@ 115,23 111,21 @@ def default_paragraph_json
  "[]"
end

def default_data_json(
def default_post_json(
  title : String = "This is a story",
  paragraph_json : String = default_paragraph_json
)
  <<-JSON
    {
      "post": {
        "title": "#{title}",
        "createdAt": 1628974309758,
        "creator": {
          "id": "abc123",
          "name": "Author"
        },
        "content": {
          "bodyModel": {
            "paragraphs": #{paragraph_json}
          }
      "title": "#{title}",
      "createdAt": 1628974309758,
      "creator": {
        "id": "abc123",
        "name": "Author"
      },
      "content": {
        "bodyModel": {
          "paragraphs": #{paragraph_json}
        }
      }
    }

D spec/setup/clean_database.cr => spec/setup/clean_database.cr +0 -3
@@ 1,3 0,0 @@
Spec.before_each do
  AppDatabase.truncate
end

A spec/support/action_helpers.cr => spec/support/action_helpers.cr +12 -0
@@ 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

M src/actions/articles/show.cr => src/actions/articles/show.cr +10 -2
@@ 6,8 6,8 @@ class Articles::Show < BrowserAction
    case post_id
    in Monads::Just
      response = client_class.post_data(post_id.value!)
      page = PageConverter.new.convert(response.data)
      html ShowPage, page: page
      page = PageConverter.new.convert(response.data.post)
      render_page(page)
    in Monads::Nothing, Monads::Maybe
      html(
        Errors::ParseErrorPage,


@@ 18,6 18,14 @@ class Articles::Show < BrowserAction
    end
  end

  def render_page(page : Page)
    html ShowPage, page: page
  end

  def render_page(page : MissingPage)
    raise MissingPageError.new
  end

  def client_class
    if use_local?
      LocalClient

M src/actions/errors/show.cr => src/actions/errors/show.cr +4 -0
@@ 4,6 4,10 @@ class Errors::Show < Lucky::ErrorAction
  default_format :html
  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)
    if html?
      error_html "Sorry, we couldn't find that page.", status: 404

M src/classes/page_converter.cr => src/classes/page_converter.cr +12 -8
@@ 1,20 1,24 @@
class PageConverter
  def convert(data : PostResponse::Data) : Page
    title, content = title_and_content(data)
    author = data.post.creator
    created_at = Time.unix_ms(data.post.createdAt)
  def convert(post : PostResponse::Post) : Page
    title, content = title_and_content(post)
    author = post.creator
    created_at = Time.unix_ms(post.createdAt)
    gist_store = gist_store(content)
    Page.new(
      title: title,
      author: author,
      created_at: Time.unix_ms(data.post.createdAt),
      created_at: Time.unix_ms(post.createdAt),
      nodes: ParagraphConverter.new.convert(content, gist_store)
    )
  end

  def title_and_content(data : PostResponse::Data) : {String, Array(PostResponse::Paragraph)}
    title = data.post.title
    paragraphs = data.post.content.bodyModel.paragraphs
  def convert(post : Nil) : MissingPage
    MissingPage.new
  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 }
    {title, non_content_paragraphs}
  end

A src/models/missing_page.cr => src/models/missing_page.cr +5 -0
@@ 0,0 1,5 @@
class MissingPage
end

class MissingPageError < Exception
end

M src/models/post_response.cr => src/models/post_response.cr +1 -1
@@ 8,7 8,7 @@ class PostResponse
  end

  class Data < Base
    property post : Post
    property post : Post?
  end

  class Post < Base

M src/version.cr => src/version.cr +1 -1
@@ 1,3 1,3 @@
module Scribe
  VERSION = "2022-05-21"
  VERSION = "2022-06-17"
end