StrapiRuby
StrapiRuby is a Ruby wrapper around Strapi REST API, version 4. It has not been tested with previous versions.
Strapi is an open-source, Node.js based, Headless CMS to easily build customizable APIs.
I think it's one of the actual coolest solution for integrating a CMS into Rails for example, so let's dive in!
Table of contents
Installation
Add this line to your application's Gemfile:
# Gemfile
gem "strapi_ruby"
Then if you use Rails, run in your terminal to generate a config initializer. Otherwise copy paste and fill the config block.
bundle
rake strapi_ruby:config
# config/initializer/strapi_ruby.rb
# Don't
StrapiRuby.configure do |config|
config.strapi_server_uri = "http://localhost:1337/api"
config.strapi_token = "YOUR_TOKEN"
end
# Do
StrapiRuby.configure do |config|
config.strapi_server_uri = ENV["STRAPI_SERVER_URI"]
config.strapi_token = ENV["STRAPI_SERVER_TOKEN"]
end
IMPORTANT
- Always store sensible values in environment variables or Rails credentials
- Don't forget the trailing
/api
in your uri and don't finish it with a trailing slash.
And you're ready to fetch some data!
StrapiRuby.get(resource: :restaurants)
# => https://localhost:1337/api/restaurants
Usage
API
When passing most of the arguments and options, you can use either Symbol
or String
for single fields/items, and an Array
of Symbol
or String
.
API methods will return an OpenStruct which is similar to a Hash but you can access keys with dot notation. All fields of the OpenStruct have been recursively converted to OpenStruct as well so it's easy to navigate, as seen below
# These are similar
answer = StrapiRuby.get(resource: :articles)
answer = StrapiRuby.get(resource: "articles")
# Grab data or meta
data = answer.data
= answer.
# Access a specific attribute
answer = StrapiRuby.get(resource: :articles, id: 2)
article = answer.data
title = article.attributes.title
# If an error occur, it will be raised to be rescued and displayed in the answer.
data = answer.data # => nil
= answer. # => nil
error = answer.error. # => ErrorType:ErrorMessage
endpoint = answer.endpoint
# => "https://localhost:1337/api/restaurants?filters[title][$contains]=this+does+not+exists
.get
# Display all items of a collection as an array
answer = StrapiRuby.get(resource: :restaurants)
# Get a specific element
StrapiRuby.get(resource: :restaurants, id: 1)
.post
# Create an item of a collection, return item created
StrapiRuby.post(resource: :articles,
data: {title: "This is a brand article",
content: "created by a POST request"})
.put
# Update a specific item, return item updated
StrapiRuby.put(resource: :articles,
id: 23,
data: {content: "'I've edited this article via a PUT request'"})
.delete
# Delete an item, return item deleted
StrapiRuby.delete(resource: :articles, id: 12)
.escape_empty_answer
Basic Example: Rails
# pages_controller.rb
def home
@articles = StrapiRuby.get(resource: :articles)
end
# home.html.erb
<% StrapiRuby.escape_empty_answer(@articles) do %>
<ul>
<% @articles.data.each do |article| %>
<li>
<%= article.attributes.title %>
</li>
<% end %>
</ul>
<% end %>
Strapi Parameters
strapi_ruby
` API functions wraps all parameters offered by the Strapi REST Api V4.
The query is built using a transverse hash function similar to Javascript qs
library used by Strapi.
Instead parameters should be passed as a hash to their key and you can use symbols instead of strings.
Only exceptions are for the operators
of the filters used as keys. Also, Integers, eg. for ID, must be passed as strings.
Full parameters documentation from Strapi is available here.
You can also use their interactive query builder. Just remember to convert the result correctly the resulting JS object to a hash with correct keys and values.
populate
# Populate one level deep all relations
StrapiRuby.get(resource: :articles, populate: :*)
# => /articles?populate=*
# --------------------------------
# Populate one level deep a specific field
StrapiRuby.get(resource: :articles, populate: [:categories])
# => /articles?populate[0]=categories
# --------------------------------
# Populate two level deep
StrapiRuby.get(resource: :articles, populate: { author: { populate: [:company] } })
# => /articles??populate[author][populate][0]=company
# --------------------------------
# Populate a 2-level component and its media
StrapiRuby.get(resource: :articles, populate: [
"seoData",
"seoData.sharedImage",
"seoData.sharedImage.media",
])
# => articles?populate[0]=seoData&populate[1]=seoData.sharedImage&populate[2]=seoData.sharedImage.media
# --------------------------------
# Deeply populate a dynamic zone with 2 components
StrapiRuby.get(resource: :articles, populate: {
testDZ: {
populate: :*,
},
})
# => /articles?populate[testDZ][populate]=*
# Using detailed population strategy
StrapiRuby.get(resource: :articles, populate: {
testDz: {
on: {
"test.test-compo" => {
fields: [:testString],
populate: :*,
},
"test.test-compo2" => {
fields: [:testInt],
},
},
},
})
# => /articles?populate[testDz][on][test.test-compo][fields][0]=testString&populate[testDz][on][test.test-compo][populate]=*&populate[testDz][on][test.test-compo2][fields][0]=testInt
fields
# Select one field
StrapiRuby.get(resource: :articles, fields: :title)
# => /articles?fields[0]=title
# --------------------------------
# Select multiple fields
StrapiRuby.get(resource: :articles, fields: [:title, :body])
# => /articles?fields[0]=title&fields[1]=body
sort
# Sort by a single key
StrapiRuby.get(resource: :articles, sort: [])
# => articles?sort[0]=title&sort[1]=slug
# --------------------------------
# You can pass sort order and also sort by multiple keys
StrapiRuby.get(resource: :articles, sort: ["createdAt:desc", "title:asc"])
# => articles?sort[0]=created:desc&sort[1]=title:asc
filters
Use a String
and not a Symbol
when using operator
.
Operator | Description |
---|---|
$eq |
Equal |
$eqi |
Equal (case-insensitive) |
$ne |
Not Equal |
$nei |
Not Equal (case-insensitive) |
$lt |
Less than |
$lte |
Less than (case-insensitive) |
$gt |
Greater than |
$gte |
Greater than (case-insensitive) |
$in |
In |
$notIn |
Not in |
$contains |
Contains |
$notContains |
Does not contain |
$containsi |
Contains (case-insensitive) |
$notContainsi |
Does not contain (case-insensitive) |
$null |
Is null |
$notNull |
Is not Null |
$between |
Is between |
$startsWith |
Starts with |
$startsWithi |
Starts with (case-insensitive) |
$endsWith |
Ends with |
$endsWithi |
Ends with (case-insensitive) |
$or |
Or |
$and |
And |
$not |
Not |
# Simple usage
StrapiRuby.get(resource: :users, filters: { username: { "$eq" => "John" } })
# => /users?filters[username][$eq]=John
# --------------------------------
# Using $in operator to match multiples values
StrapiRuby.get(resource: :restaurants,
filters: {
id: {
"$in" => ["3", "6", "8"],
},
})
# => /restaurants?filters[id][$in][0]=3&filters[id][$in][1]=6&filters[id][$in][2]=8
# --------------------------------
# Complex filtering with $and and $or
StrapiRuby.get(resource: :books,
filters: {
"$or" => [
{
date: {
"$eq" => "2020-01-01",
},
},
{
date: {
"$eq" => "2020-01-02",
},
},
],
author: {
name: {
"$eq" => "Kai doe",
},
},
})
# => /books?filters[$or][0][date][$eq]=2020-01-01&filters[$or][1][date][$eq]=2020-01-02&filters[author][name][$eq]=Kai%20doe
# --------------------------------
# Deep filtering on relation's fields
StrapiRuby.get(resource: :restaurants,
filters: {
chef: {
restaurants: {
stars: {
"$eq" => 5,
},
},
},
})
# => /restaurants?filters[chef][restaurants][stars][$eq]=5
Pagination by page: page & page_size
Only one pagination method is possible.
StrapiRuby.get(resource: :articles, page: 1, page_size: 10)
# => /articles?pagination[page]=1&pagination[pageSize]=10
Pagination by offset: start & limit
Only one pagination method is possible.
StrapiRuby.get(resource: :articles, start: 0, limit: 10)
# => /articles?pagination[start]=0&pagination[limit]=10
locale
I18n plugin should be installed.
StrapiRuby.get(resource: :articles, locale: :fr)
#=>?/articles?locale=fr
publication_state
Use :preview
or :live
StrapiRuby.get(resource: :articles, publication_state: :preview)
#=>?/articles?publicationState=preview
Use raw query
If you wanna pass a raw query you decide to build, just use raw as an option. It cannot be combined with any other Strapi parameters.
StrapiRuby.get(resource: articles:, raw: "?fields=title&sort=createdAt:desc")
# => /articles?fields=title&sort=createdAt:desc"
Configuration
You can pass more options via the config block.
Show Endpoint
This option is for accessing the resulting endpoint in a successful error, ie. strapi_server_uri
+ its query.
It defaults to false
.
# Pass this as a parameter to the config block
StrapiRuby.configure do |config|
#...
config.show_endpoint = true
#...
end
# Or as an option to one of the API functions
StrapiRuby.get(resource: :articles, show_endpoint: true)
# You can access it in the answer
StrapiRuby.get(resource: :articles, show_endpoint: true).endpoint
# => https://localhost:1337/api/restaurants
Or directly in the options parameters
DateTime Conversion
By default, any createdAt
, publishedAt
and updatedAt
fields in the answer will be recursively converted to DateTime
instances, making it easy to use #strftime
method.
But if you don't want this conversion, pass it to the configure block.
StrapiRuby.configure do |config|
#...
config.convert_to_datetime = false
#...
end
Markdown Conversion
Selected fields will automatically be converted to HTML using redcarpet
gem. This is very useful to get data ready for the views.
# You can pass this in your config file:
StrapiRuby.configure do |config|
#...
config.convert_to_html = [:body]
#...
end
# Or as an option to one of the API functions
StrapiRuby.get(resource: :articles, fields: :body, convert_to_html: [:body])
Faraday Block
Passing a Proc
You can pass a proc when configuring Strapi Ruby just as you'd pass a block when creating a new instance of a Faraday. Check Faraday documentation
StrapiRuby.configure do |config|
#...
config.faraday = Proc.new do |faraday|
faraday.headers['X-Custom-Header'] = 'Custom-Value'
end
#...
end
Default Faraday::Connection used by the gem
Default options used by this gem are url_encode
and Faraday.default_adapter
, but you can override them.
Default Faraday::Connection headers
Default headers cannot be overriden but will be merged with your added configuration.
default_headers = { "Content-Type" => "application/json",
"Authorization" => "Bearer #{strapi_token}",
"User-Agent" => "StrapiRuby/#{StrapiRuby::VERSION}" }
Handling Errors
Depending on your utilisation, there are multiple ways to handle errors.
Errors Classes
# Config Error
class ConfigurationError < StandardError
# Client Error
class ClientError < StandardError
# Client Error Specific Error
class ConnectionError < ClientError
class UnauthorizedError < ClientError
class ForbiddenError < ClientError
class NotFoundError < ClientError
class UnprocessableEntityError < ClientError
class ServerError < ClientError
class BadRequestError < ClientError
class JSONParsingError < ClientError
Graceful degradation
One way to handle errors and gracefuly degrade is using .escape_empty_answer
and use a block to nest your data accessing code.
Errors will still be logged in red in console.
Definition
# Definition
module StrapiRuby
def escape_empty_answer(answer)
return answer.error. if answer.data.nil?
yield
end
end
Example : Usage in a Rails view
<% StrapiRuby.escape_empty_answer(answer) do %>
<%= answer.title %>
<%= answer.body %>
<% end %>
Or you may want to handle specific errors like this:
# some_controller.rb
begin
answer = StrapiRuby.get(resource: "articles")
rescue NotFoundError e =>
# Do something to avoid an embarassing situation
rescue ClientError e =>
# Do something to avoid an embarassing situation
end
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/saint-james-fr/strapi_ruby. This project is intended to be a safe, welcoming space for collaboration.
Tests
Run bundle exec rspec
to run the tests.
Inside spec/integration.rb
you'll have access to integration tests.
You'll need to configure environment variables within the repo and run a strapi server to run these tests sucessfully.
See Strapi documentation for more details about installing a Strapi Server here
License
The gem is available as open source under the terms of the MIT License.