As I became more familiar with the Ghost platform, I found myself looking for a client implementation in Ruby for the Ghost API. Unfortunately, I didn’t succeed on my search. Thus, I decided to roll out my own implementation. If you want to go straight head and check the code, go to the github repo.
Before diving into the implementation, let’s mention some essential details of the Ghost API.
APIs
The Ghost API is split between two types:
- Public API: Basically, it reflects the behavior of a blog. It gives access to public posts and tags, among other data.
- Private API: Provides access to sensitive data in accordance with the permissions of the user who did the request.
Authentication
Ghost currently supports two types of authentication:
- Client Authentication: Provides access to the public API of the ghost blog. It authenticates against the public API by using a
client_id
and aclient_secret
to gain access for non-secure applications. - User Authentication: Allows to make HTTP request to write to the API or read private data. It requires the exchange of user credentials to obtain a bearer token, which will be used, in turn, for subsequent requests.
Implementation
The current version the REST API client in Ruby only supports gathering public data from a given blog.
Client
The client handles making request to the blog to get the information. The current implementation is relatively simple and is only capable of performing GET requests.
class Client
attr_reader :base_url, :client_id, :client_secret, :default_query
def initialize(base_url, client_id, client_secret)
@base_url = URI.join(base_url, 'ghost/', 'api/', 'v0.1/')
@client_id = client_id
@client_secret = client_secret
@http = HTTPClient.new(base_url: @base_url)
@default_query = Support::HashWithIndifferentAccess.new(
client_id: @client_id,
client_secret: @client_secret
)
end
def posts
Controllers::PostsController.new(self)
end
def tags
Controllers::TagsController.new(self)
end
def get(endpoint, query)
response = @http.get(endpoint, query, {}, follow_redirect: true)
content = Support::HashWithIndifferentAccess.new(
JSON.parse(response.body)
)
[response.status_code, content]
end
end
The get
’s result is stored as a HashWithIndifferentAccess
, which is an object derived from the Hash
class, that allows access to the same keys either by string or symbol. The implementation is based on the one used by RoR.
As it is shown above, the client provides a get method to perform a request directly against the Ghost API on the given blog. But producing directly the correct request can be a bit cumbersome, thus the client provides access to controllers for managing tags and posts, with a more easy-to-use interface.
Controllers
The goal of a controller is to encapsulate the preparation of the request parameters. To achieve this, it brings a fluent interface to generate API requests. If you are familiar with Rails and ActiveModel, then you should be right at home :).
As the gem evolves, I can envision the controller building all types of requests and handling creation and edition of posts and tags as well, but the current release is limited to only access readable data.
BaseController
, as hinted by its name, contains the common blocks for the controllers, such as, common fluent methods used to build the API request as well as the calling methods to perform the requests, either to fetch a single resource or multiple resources.
class BaseController
attr_reader :client, :params
def initialize(client)
@client = client
@params = Support::HashWithIndifferentAccess.new
end
def all
fetch_list.map { |r| @resource_klass.generate(r) }
end
def limit(limit)
where(limit: limit)
end
def page(page)
where(page: page)
end
def order(order_str)
where(order: order_str)
end
def fields(fields_str)
where(fields: fields_str)
end
def filter(filter_query)
where(filter: filter_query)
end
def include(resources_str)
where(include: resources_str)
end
def where(hash)
@params.merge!(hash)
self
end
def find_by(kvp)
@params.keys.reject { |k| k == :include }.each do |key|
@params.delete(key)
end
content = fetch_single(kvp)
resource_klass.generate(content)
end
private
def fetch_single(kvp)
endpoint = format_endpoint(kvp)
query = client.default_query.merge(@params)
status, content = client.get(endpoint, query)
if error?(status)
raise_fetch_single_error(kvp, status, content['errors'])
end
content
end
def fetch_list
query = client.default_query.merge(@params)
status, content = client.get(endpoint, query)
raise_fetch_list_error(status, content['errors']) if error?(status)
content[@endpoint]
end
def error?(status)
status >= 400
end
def format_endpoint(kvp)
return [endpoint, kvp[:id]].join('/') if kvp.key?(:id)
return [endpoint, 'slug', kvp[:slug]].join('/') if kvp.key?(:slug)
raise Errors::InvalidEndpointError,
"Invalid endpoint for #{endpoint}. Should be either :id or :slug"
end
end
Below, we introduce an example of an resource-specific controller, in this case the PostsController
class. It handles some specific methods for the posts API, such as formats
and is able to raise detailed request errors.
class PostsController < BaseController
attr_reader :endpoint, :resource_klass
def initialize(client)
super
@endpoint = 'posts'
@resource_klass = Resources::Post
end
def formats(formats)
where(formats: formats)
end
private
def raise_fetch_single_error(kvp, status, errors)
key = kvp.key?(:id) ? :id : :slug
message = "Unable to fetch post with #{key} = #{kvp[key]}"
raise Errors::RequestError.new(message,
status,
errors)
end
def raise_fetch_list_error(status, errors)
raise Errors::RequestError.new('Unable to fetch posts',
status,
errors)
end
end
Similar to this, TagsController
object defines the methods and errors particular to tags in the Ghost API.
Resources
A resource is a class that holds the data received from an API request and provide accessor
methods to facilitate working with the data itself. They inherit from the BaseResource
class, shown below, which defines a class method to generate a new instance from a hash parameter.
class BaseResource
include Support::Hydratable
def self.generate(hash)
res_instance = new
res_instance.hydrate(hash)
res_instance
end
def self.hash_value?(data, key)
data.key?(key) && data[key].is_a?(Hash)
end
end
As can be seen above, the class definition includes the Hydratable
module. This module provides the hydrate
method that allows to map key value pairs in the hash parameter with existing accessor
methods in the resource instance.
Below, we show the code for a particular resource. The Post
implementation adds several accessor methods, such as title
and published_at
, and aliases the page
and featured
accessors to better comply with ruby code guidelines.
class Post < BaseResource
attr_accessor :id, :title, :slug, :html, :page,
:status, :published_at, :created_at, :author,
:visibility, :featured, :plaintext, :tags
alias page? page
alias featured? featured
def self.generate(hash)
inst = super(hash)
inst.author = User.generate(hash[:author]) if hash_value?(data, :author)
inst.tags = hash[:tags].map { |t| Tag.generate(t) } if hash.key?(:tags)
inst
end
end
When generating a Post
instance from hash data, it will automatically generate the corresponding resources objects for author
and tags
if these are present in the hash parameter.
Usage
So far, we have seen a fair amount of code and analyzed the API client design. It is time for fun part!! Let’s see what we can do with the gem.
Instantiate a client
client = GhostRb::Client.new(BASE_URL, CLIENT_ID, CLIENT_SECRET)
In a real scenario, you should probably be getting the values to instantiate the a Client
object from environment variables or any sort of secrets configuration.
PostsController instance
We can create a controller from scratch as follow:
ctrl = GhostRb::Controllers::PostsControllers.new(client)
I guess that’s to much to write. There has to be an easier way.
Fortunately, we can get a post controller instance doing the following instead:
ctrl = client.posts
Requesting a single post
Using our previous defined ctrl
variable, it is as easy as:
post = ctrl.include('author,tags').find_by(id: 'id')
# Using the GhostRb::Resources::Post
puts post.title
puts post.author # Will return a User object with the public data for the author
puts post.tags # Will return an Array of Tag objects
The previous code example showed how to get a post by id and include the author and tags information. If we prefer to get a single post by the slug instead, the change is rather simple:
post = ctrl.find_by(slug: 'post-slug')
Get 5 first published posts
The following request is going to return an array of Post objects sorted by publish date.
posts = ctrl.order('published_at asc').limit(5).all
If you like better, you could get the same result by writing the following:
posts = ctrl.where(order: 'published_at asc', limit: 5).all
Conclusion
To sum up, we mentioned first some key aspects of the Ghost API, relevant to our client implementation. Then we proceeded to highlight the main details of the Ruby client and analyzed some key points. Finally, we showcased some ways to use the gem to read data from a blog API.
To conclude, I must say that getting the API client done is a huge step towards several ideas I have been thinking about lately, since they require gathering data from the blog programmatically. It is a very simple implementation for now, but as different ideas come to fruition I am sure the ruby client will be expanded.
Besides that, it was a great exercise on its own and a way to contribute back to the community. So fork it and contribute back if you can. Pull requests are more than welcome.
Finally, I hope it has been worth for you as it was for me to create it. Maybe you learnt a little bit. Me, I did it for sure. Stay tuned for more!!