The problem:
I wanted to create a library of markdown files, which would be rendered to a user on my website as individual pages. It was all about documents about network services e.g. SMTP, DNS, HTTP etc., each one having its own markdown file. The amount of services/files was unknown, as there were lots that have been already added and more that would be added in the future. No much thinking is necessary for 5 actions but imagine If there are… 100 of them.
The solution:
I chose Markdown format because I am not working with a database at the moment and I hate the over-engineering of things. Choosing redcarpet and rouge was an easy thing as they have a strong community to support them. I used dynamic actions/method creation in order to reduce useless code bloat. At the end, I just have to drop a markdown file in the directory mentioned and that’s it! *note: as with any other problem, there isn’t only one solution. The purpose there is to use dynamic actions intentionally to solve this one.
The markdown files
First of all I created a directory in the /public/md/ Rails path. I copied my .md files there, keeping the name of the service as the filename.
The routes
The usual path was to create a GET route for each service, setting up its own action in the controller and thus bloat and code smells. So I created a route for all of the services:
resources :services do
get '/:service', on: :collection, action: 'service'
end
The controller
Next, I started building the controller. First the markdown processing:
# Reads markdown files from public directory path
def read_md_data(service)
path = File.join(Rails.root, 'public/md/' + service + '.md')
return File.read(path)
rescue NilClass
return nil
end
# Renders text to syntax highlighted text
def create_content(data)
return Redcarpet::Markdown.new(
::PageRender, fenced_code_blocks: true)
.render(data)
.html_safe
rescue NoMethodError, NilClass
return ''
end
Then I added the respond_to_missing?
override and I used the method_missing
method in order to accept services as service_<name>
:
# Overrides default respond_to_missing? method so it returns false # when it doesn't respond to a missing service method
def respond_to_missing?(method_name, include_private = true)
!method_name.match(/^service_[a-z0-9]+$/).nil?
end
# Takes actions to render markdown text to the browser
def method_missing(method_name, *arguments)
service = method_name.match(/^service_([a-z0-9]+)$/)[1]
data = read_md_data(service)
@content = create_content(data)
render :service_page
rescue NoMethodError, NilClass
@content = 'ERROR: There was an issue while rendering content'
end
Next thing, I built the main service action. I chose this RegEx because there are services which carry a number next to the name e.g. POP3:
# Calls a dynamic method "service_<name> If validated properly"
def service
if params[:service] && params[:service].match(/^[a-z0-9]+$/)
self.send("service_#{params[:service]}")
else
raise StandardError
end
rescue StandardError
@content = "ERROR: Page for #{params[:service]} is missing"
render :service_page
end
Finally, the action_missing
method:
# Calls service method If overridden respond_to_missing? == true
def action_missing(name)
self.send(name) if self.send(:respond_to_missing?, name)
end
All the controller code in one block:
class ServicesController < ApplicationController
# Calls a dynamic method "service_<name> If validated properly"
def service
if params[:service] && params[:service].match(/^[a-z0-9]+$/)
self.send("service_#{params[:service]}")
else
raise StandardError
end
rescue StandardError
@content = "ERROR: Page for #{params[:service]} is missing"
render :service_page
end
# Calls service method If overridden respond_to_missing? returns true
def action_missing(name)
self.send(name) if self.send(:respond_to_missing?, name)
end
private
# Reads markdown files from public directory path
def read_md_data(service)
path = File.join(Rails.root, 'public/md/' + service + '.md')
return File.read(path)
rescue NilClass
return nil
end
# Renders text to syntax highlighted text
def create_content(data)
return Redcarpet::Markdown.new(
::PageRender, fenced_code_blocks: true)
.render(data)
.html_safe
rescue NoMethodError, NilClass
return ''
end
# Overrides default respond_to_missing? method so it returns false # when it doesn't respond to a missing service method
def respond_to_missing?(method_name, include_private = true)
!method_name.match(/^service_[a-z0-9]+$/).nil?
end
# Takes actions to render markdown text to the browser
def method_missing(method_name, *arguments)
service = method_name.match(/^service_([a-z0-9]+)$/)[1]
data = read_md_data(service)
@content = create_content(data)
render :service_page
rescue NoMethodError, NilClass
@content = 'ERROR: There was an issue while rendering content'
end
end
The view
The view is pretty simple (bootstrap 4 used):
<div class="container-fluid">
<h2> <strong>Services</strong> </h2>
<hr>
</div>
<div class="container">
<div class="row">
<div class="col">
<%= @content %>
</div>
</div>
</div>
The verdict
I know that it’s not perfect and the code can be reduced more in size, but that’s my first version of the implementation. To be honest, it didn’t take more than an hour for me to set this up so I will revise the code again and make the appropriate changes. Hope this helps someone.