Использование метапрограммирования на Ruby для создания REST API из файла JSON
В этом посте мы рассмотрим AutoAPI, инструмент, который позволяет генерировать сервер sinatra на основе спецификаций конечных точек, записанных в JSON-файле.
Ruby известен своей мощной поддержкой метапрограммирования, которое позволяет коду модифицировать себя во время выполнения. Метапрограммирование — это метод, когда программа обрабатывает другие программы как данные, и Ruby в этом преуспевает.
AutoAPI в настоящее время работает только с конечными точками GET, но будет дополняться в будущем. Он также возвращает данные в формате JSON или статических HTML-файлов, с планируемой поддержкой других типов MIME.
В целях удобства и практики написания сценариев, AutoAPI реализован как shell-скрипт. Он запускается из любой папки и ищет файл endpoints.json
в текущей директории.
# Get the file and parse the entire file into a ruby hash
file = File.read("#{Dir.pwd}/endpoints.json")
endpoints_hash = JSON.parse(file)
Мы используем JSON для преобразования файла в ruby Hash. Это обеспечит нас качественными методами, которые мы сможем использовать позжепо необходимости.
Теперь, поскольку мы используем Sinatra в качестве нашего сервера, нам понадобится способ динамического определения новых конечных точек из файла. Sinatra — это DSL для быстрого создания веб-приложений на Ruby с минимальными усилиями.
Давайте сначала рассмотрим метод send
или public_send
. Назначение этих методов заключается в том, что они в основном выполняют вызов метода экземпляра класса. Например, если у нас есть класс
class Class
def hello
puts "hello"
end
end
Если бы мы сделали class.send(:hello)
, то наш результат был бы hello
.
Вы также можете передать параметры для send, если метод принимает какие-либо аргументы.
Чтобы создать конечную точку GET в Sinatra, мы должны написать
get '/hello' do
'Hello world!'
end
Мы могли бы разобрать это и обнаружить, что :get
— это имя метода, '/hello
— это имя пути, и все в блоке do мы будем называть do_block
. Поэтому мы могли бы также мысленно переписать это как
get('/hello') do
'Hello World'
end
или
send(:get, '/hello', &block)
где &block
— это do_block
, переданный методу send.
Все это означает, что нам нужно определить метод, который будет считывать имена конечных точек и их связанные методы для определения новых маршрутов. Это выглядит так
def create_endpoint(method, name, &block)
Sinatra::Application.instance_eval do
name = "/#{name}" if not name.start_with?(/\//)
send(method, name, &block)
end
end
Он принимает метод REST (GET, POST, PUT, DELETE), имя конечной точки и блок кода. Затем он использует instance_eval для создания нового маршрута на запущенном экземпляре Sinatra. Я добавил проверку, чтобы убедиться, что имя конечной точки предваряется косой чертой, потому что я столкнулся с этой случайной ошибкой, когда маршрут, казалось бы, определен, но недоступен, потому что он не начинается с косой черты. Наконец, мы просто отправляем метод, как показано выше. Просто, правда?? Честно говоря, это так просто.
Теперь мы должны указать, что файл endpoints.json
имеет определенную структуру.
{
"GET": {
"json_response": {
"header": {"Content-Type": "application/json"},
"response": {
"content": { "message": "Hello AutoAPI"},
"file": false
}
},
"json_response_file": {
"header": {"Content-Type": "application/json"},
"response": {
"file": true,
"content": "endpoints.json"
}
},
"html_file": {
"header": {"Content-Type": "text/html"},
"response": {
"file": true,
"content": "test.html"
}
}
}
}
Это пример файла конечных точек. Мы пройдемся по каждой конечной точке по одной. Таким образом, все глаголы REST действуют как ключи, таким образом, все маршруты GET находятся в значениях GET и т. д. и т. п. Наша первая конечная точка — json_response
. Она должна была проверить, могу ли я получить жестко закодированный ответ. Заголовок — это вложенный объект, содержащий то, что вы ожидаете в типичном запросе HTTP GET. Здесь мы передаем только Content-Type. Для ответа у нас есть ключ файла, который указывает, должна ли конечная точка отправлять файл или она должна отправлять значение содержимого как JSON. Этот файл также должен находиться в той же папке, что и скрипт при его запуске. В этом примере вторая конечная точка на самом деле просто возвращает файл endpoints.json
. Третья конечная точка имеет другой тип содержимого и возвращает файл HTML.
Для обработки этих конечных точек я написал следующий код
endpoints_hash.each do |method, paths|
paths.each do |path, params|
create_endpoint(method.downcase.to_s, "#{path.to_s}") do
content_type :json if params["Content-Type"] == "application/json"
content_type :html if params["Content-type"] == "text/html"
if params["response"]["file"]
send_file "#{Dir.pwd}/#{params["response"]["content"]}"
else
params["response"]["content"].to_json
end
end
end
end
Все, что он делает, это перебирает все конечные точки и пути и определяет маршруты для них. Обратите внимание, что мне нужно указать content_type, и я просто использую переданные заголовки, чтобы выяснить это. Код может быть немного ломающимся и находится в процессе разработки, но пока он работает так, как показано ниже
Для всех 3 определенных маршрутов нам удается получить ответ. И все это примерно с 29 строками кода.
Весь код доступен github: https://github.com/W3NDO/AutoAPI.