In my rails app, I'm trying to create a booking form external parties(parks
) can point their customers to to make a booking for the respective park. The booking form works with the url/route with the subdomain book
https://book.myapp.com/en/parks/:park_id/park_availability
Goal
I would like substitute my domain (myapp.com
) with the website of the park
, such that I get
https://book.parkapp.com/en/park_availability
Unfortunately I get the error message(s) upon creation park
NameError (uninitialized constant #<Class:0x0000563d7bf62250>::Heroku):
when using an existing park
{park.website}'s server IP address could not be found.
Outline attempted approach
Park
has a website
column. In routes.rb
I tried setting up the constraints and apply them on the park_availability
action. Park
model I attempted to add the domain (Park.website
) to my Heroku application, once the park is saved.Park controller
I try to find the @park
, before the park_availability
action.Code
routes.rb
class CustomDomainConstraint
# Implement the .matches? method and pass in the request object
def self.matches? request
matching_site?(request)
end
def self.matching_site? request
# handle the case of the user's domain being either www. or a root domain with one query
if request.subdomain == 'www'
req = request.host[4..-1]
else
req = request.host
end
# first test if there exists a Site with a domain which matches the request,
# if not, check the subdomain. If none are found, the the 'match' will not match anything
Park.where(:website => req).any?
end
end
Rails.application.routes.draw do
resources :parks do
match ':website/park_availability' => 'parks#park_availability', on: :member, :constraints => CustomDomainConstraint, via: :all
end
end
park.rb
class Park < ApplicationRecord
after_save do |park|
heroku_environments = %w(production staging)
if park.website && (heroku_environments.include? Rails.env)
added = false
heroku = Heroku::API.new(api_key: ENV['HEROKU_API_KEY'])
heroku.get_domains(ENV['APP_NAME']).data[:body].each do |domain|
added = true if domain['domain'] == park.website
end
unless added
heroku.post_domain(ENV['APP_NAME'], park.website)
heroku.post_domain(ENV['APP_NAME'], "www.#{park.website}")
end
end
end
parks_controller.rb
class ParksController < ApplicationController
before_action :find_park, only:[:park_availability]
def park_availability
#working code...
end
private
def find_park
# generalise away the potential www. or root variants of the domain name
if request.subdomain == 'www'
req = request.host[4..-1]
else
req = request.host
end
# test if there exists a Park with the requested domain,
@park = Park.find_by(website: req)
# if a matching site wasn't found, redirect the user to the www.<website>
redirect_to :back
end
end
To get around error in question you can try writing classname as ::Heroku
, that will make ruby to look for it in correct scope.
But Heroku legacy api has been disabled, so you should use their new platform-api:
heroku = PlatformAPI.connect(ENV['HEROKU_API_KEY']) # note that you may also have to use a new key
domains = heroku.domain.list(ENV['APP_NAME'])
...
heroku.domain.create(ENV['APP_NAME'], ...)
also to check for domain presence you can use domain.info
api method instead of fetching all domains that are not needed.
Keep in mind that callbacks are not the best place to make any external calls: if the api call fails for some reason (temporary network problems, api outage, server restart, etc) - whole transaction will be rolled back, item will not be saved and you can loose data. Better way is to enqueue a background job there, which will handle domains later, can retry if needed and so on.