Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/perron/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
require "perron/resource/sweeper"
require "perron/resource/adjacency"
require "perron/resource/table_of_content"
require "perron/resource/updatable"

module Perron
class Resource
Expand Down
46 changes: 46 additions & 0 deletions lib/perron/resource/updatable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module Perron
class Resource
module Updatable
extend ActiveSupport::Concern

included do
def metadata
@metadata ||= Proxy.new(super, self)
end
end

class Proxy
def initialize(metadata, resource)
@metadata = metadata
@resource = resource
end

def update(attributes)
attributes = attributes.deep_symbolize_keys

separator = Perron::Resource::Separator.new(@resource.raw_content)
frontmatter = separator.frontmatter.to_h.merge(attributes)
yaml = frontmatter.transform_keys(&:to_s).to_yaml.sub(/\A---\n/, "")
body = separator.content

File.write(@resource.file_path, "---\n#{yaml}---\n\n#{body}\n")

@resource.instance_variable_set(:@frontmatter, nil)
attributes.each { |key, value| @metadata[key] = value }

self
end

def respond_to_missing?(name, include_private = false)
@metadata.respond_to?(name, include_private) || super
end

def method_missing(name, *arguments, &block)
@metadata.public_send(name, *arguments, &block)
end
end
end
end
end
8 changes: 5 additions & 3 deletions test/dummy/app/models/content/post.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
class Content::Post < Perron::Resource
include Perron::Resource::Updatable

CATEGORIES = {
ruby: { title: "Ruby", description: "Ruby articles" },
rails: { title: "Rails", description: "Rails articles" },
css: { title: "CSS", description: "CSS articles" }
ruby: {title: "Ruby", description: "Ruby articles"},
rails: {title: "Rails", description: "Rails articles"},
css: {title: "CSS", description: "CSS articles"}
}

configure do |config|
Expand Down
85 changes: 85 additions & 0 deletions test/perron/resource/updatable_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require "test_helper"

class Perron::Resource::UpdatableTest < ActiveSupport::TestCase
FIXTURE = "test/dummy/app/content/posts/2023-05-15-sample-post.md"

def setup
@tmp = Rails.root.join("tmp/updatable_metadata_#{SecureRandom.hex(4)}.md").to_s
FileUtils.cp(FIXTURE, @tmp)
@post = Content::Post.new(@tmp)
end

def teardown
FileUtils.rm_f(@tmp)
end

def original_frontmatter
Perron::Resource::Separator.new(File.read(FIXTURE)).frontmatter.to_h
end

def current_frontmatter
Perron::Resource::Separator.new(File.read(@tmp)).frontmatter.to_h
end

def original_body
Perron::Resource::Separator.new(File.read(FIXTURE)).content
end

def current_body
Perron::Resource::Separator.new(File.read(@tmp)).content
end

test "updating an existing key changes only that key" do
@post.metadata.update(title: "Changed Title")

updated = current_frontmatter
original = original_frontmatter

assert_equal "Changed Title", updated[:title]
assert_equal original.except(:title), updated.except(:title)
assert_equal original_body, current_body
end

test "only the changed key line differs in the file" do
@post.metadata.update(title: "Changed Title")

original_lines = File.readlines(FIXTURE)
updated_lines = File.readlines(@tmp)

diff = original_lines.zip(updated_lines).reject { |a, b| a == b }
assert_equal 1, diff.size
assert_match(/^title: Changed Title$/, diff.first.last)
end

test "updating a new key inserts it without disturbing others" do
@post.metadata.update(published_at: Date.new(2024, 1, 1))

updated = current_frontmatter
original = original_frontmatter

assert_equal Date.new(2024, 1, 1), updated[:published_at]
assert_equal original, updated.except(:published_at)
assert_equal original_body, current_body
end

test "multiline values round-trip without altering other keys" do
@post.metadata.update(description: "line one\nline two")

updated = current_frontmatter
original = original_frontmatter

assert_equal "line one\nline two", updated[:description]
assert_equal original.except(:description), updated.except(:description)
assert_equal original_body, current_body
end

test "in-memory metadata reflects the update immediately" do
@post.metadata.update(title: "In Memory")

assert_equal "In Memory", @post.metadata.title
end

test "update returns self for chaining" do
assert_equal @post.metadata, @post.metadata.update(title: "X")
end
end
Loading