REST интерфейс генератор фреймворка vibe.d

в 17:33, , рубрики: dlang, rest api, vibe.d

В этой статье я поэтапно покажу как пользоваться rest interface генератором встроенным во фреймворк vibe.d. Если вы интересуетесь D и его особенностями, то данная статья, надеюсь, немного прояснит новичкам как на практике использовать D и познать всю его мощь.
Для этого нами понадобится

  1. Настроить eclipse для работы с vibe.d.новичкам
  2. Запустить «Hello World» на vibe.d. новичкам
  3. Создать клиента для api OpenWeatherMap.
  4. Создать дублирующий сервер на основе api OpenWeatherMap

В документации к vibe.d имеется пример пользования генератором. Однако данный пример, на мой взгляд, слишком прост и не раскрывает механизма работы генератора.

Подготовка

Для языка D существует множество средств программирования, включая как отдельные полноценные ide, так и плагины к уже существующим. Я выбрал для себя плагин DDT для eclipse, так как благодаря кроссплатформенности eclipse позволяет мне не привязываться к одной операционной системе.

Инструменты

  • Скачать и установить dmd.
  • Скачать и установить dub. С этого момента можно программировать на D. Однако нам нужна ide.
  • Скачать и установить eclipse.
  • Установить ddt.

На этом можно считать установку оконченной. Далее будем настраивать проект. К слову, стандартные настройки DDT годятся для написания простых программ, вроде «hello world», хотя для таких целей и обычная консоль идеально подходит, не говоря уже про встроенную поддержку D редактором sublime text.

Для чего нибудь посложнее, в качестве сборщика удобно использовать dub. Для этого отключим стандартные билдеры ddt: Project->Properties->Builders, и добавим туда dub, запускаемым из директории с вашим проектом и аргументом build. Так же советую проверить, чтобы в меню Window->Preferences->DDT->Compilers был компилятор dmd. Если его нет, указать до него путь. Тогда ddt получит доступ к стандартной библиотеке phobos.

Hello World на vibe.d

  • Инициализируем проект:
    dub init helloVibe
  • Отредактируем dub.json. Добавим туда следующее
    "libs-posix": ["dl"],
    "versions":  ["VibeCustomMain"],
    "dependencies": 
    {
    	"vibe-d": "~master"
    }
    		
  • Затем из директории helloVibe
    dub run

    Поздравляю, ваша первая программа на D заработала.

  • Создадим проект в eclipse. И в качестве библиотеки добавим папку source
    %APPDATA%/dub/packages/vibe-d-master/source

    Теперь автодополнение наш помощник.

  • Запустим vibe.d. Для этого нам необходимо начать слушать порт и запустить петлю событий. Делается это функциями
    listenHTTP(HTTPServerSettings,URLRouter)
    runEventLoop()

    соответственно. Еще необходимо создать настройки сервера и настроить маршрутизатор.

  • Настройки создаем так:
    
    auto settings = new HTTPServerSettings;
    
    settings.bindAddresses = ["127.0.0.1"];
    
    settings.port = 80;
    		
  • Не забываем подключить vibe:
    import vibe.d;
  • Маршрутизатор у нас будет включать один адрес, доступный по методу GET
    
    auto router = new URLRouter;
    
    router.get("/", &index);
    		
  • И функция обработчик:
    
    void index(HTTPServerRequest req, HTTPServerResponse res)
    {
    	res.writeBody("Hello World!");
    }
    		

    Обработчик может быть как функцией, так и делегатом, что очень удобно и позволяет использовать в качестве обработчиков методы класса. Такая особенность используется как в REST interface генераторе, так и в Web interface генераторе.

  • Осталось все вызвать в нужном порядке:
    
    import std.stdio;
    
    import vibe.d;
    
    void main()
    {
    	writeln("Edit source/app.d to start your project.");
    
    	void index(HTTPServerRequest req, HTTPServerResponse res)
    	{
    		res.writeBody("Hello World!");
    	}
    
    	auto settings = new HTTPServerSettings;
    
    	settings.bindAddresses = ["127.0.0.1"];
    
    	settings.port = 80;
    
    	auto router = new URLRouter;
    
    	router.get("/", &index);
    
    	listenHTTP(settings, router);
    
    	runEventLoop();
    }
    
  • dub run

    . Поздравляю, ваш первый сервер на vibe.d готов.

Rest interface генератор

Клиент

Vibe.d годится как для написания серверов, так и клиентов.
Чтобы создать rest interface, необходимо объявить interface (что логично). В этой статье будем рассматривать клиент для API OpenWeatherMap. Данный API имеет всего один метод weather c параметром q. Так и напишем


interface OpenWeather
{
	Weather getWeather(string q);
}

Ответ от сервера будет автоматически преобразовываться в структуру Weather. Ее опишем согласно API OpenWeatherMap

Структура, в которую преобразуется ответ сервера


struct Weather
{
	@Label("City identification")
	long id;
	
	@Label("Data receiving time, unix time, GMT")
	ulong dt;
	
	@Label("City name")
	string name;
	
	WCoord coord;
	
	WSys sys;
	
	WMain main;
	
	WWind wind;
	
	WClouds clouds;
	
	//@optional()
	//WConditions[] weather;
	
	@optional()
	WRain rain;
	
	@optional()
	WSnow snow;
}

struct WCoord
{
	@Label("City geo location, lat")
	double lat;
	
	@Label("City geo location, lon")
	double lon;
}

struct WSys
{
	@Label("System parameter, do not use it")
	double message;
	
	@Label("Country (GB, JP etc.)")
	string country;
	
	@Label("Sunrise time, unix, UTC")
	ulong sunrise;
	
	@Label("Sunset time, unix, UTC")
	ulong sunset;
}

struct WMain
{
	@Label("Temperature, Kelvin (subtract 273.15 to convert to Celsius)")
	double temp;
	
	@Label("Humidity, %")
	double humidity;
	
	@Label("Minimum temperature at the moment. This is deviation from current temp that is possible for large cities and megalopolises geographically expanded (use these parameter optionally)")
	double temp_min;
	
	@Label("Maximum temperature at the moment. This is deviation from current temp that is possible for large cities and megalopolises geographically expanded (use these parameter optionally)")
	double temp_max;
	
	@Label("Atmospheric pressure, hPa ")
	double pressure;
	
	@Label("Atmospheric pressure on the sea level, hPa") @optional()
	double sea_level;
	
	@Label("Atmospheric pressure on the ground level, hPa") @optional()
	double grnd_level;	
}

struct WWind
{
	@Label("Wind speed, mps")
	double speed;
	
	@Label("Wind direction, degrees (meteorological)")
	double deg;
	
	@Label("Wind gust, mps") @optional()
	double gust;
}

struct WClouds
{
	@Label("Cloudiness, %")
	double all;
}

struct WConditions
{
	@Label("Weather condition id")
	long id;
	
	@Label("Group of weather parameters (Rain, Snow, Extreme etc.)")
	Weather main;
	
	@Label("Weather condition within the group")
	string description;
	
	@Label("Weather icon id")
	string icon;
}

struct WRain
{
	@Label("Precipitation volume for last 3 hours, mm") @optional()
	double _3h;

}

struct WSnow
{
	@Label("Snow volume for last 3 hours, mm") @optional()
	double _3h;
}
  

Пара слов о косяках vibe.d.
Во первых, плохо работает десериализация массивов из пользовательских типов.
Во вторых, в силу особенностей с подобного языка программирования, переменные не могут начинаться с цифры (так не только в d же). Я бы записал этот косяк на счет API OpenWeatherMap, а не vibe.d, так как, имхо, этот момент был плохо продуман разработчиками API.

D позволяет нам централизованно управлять ответом. Аттрибут Label() (тут «собачка») содержит описание переменной, @optional() говорит генератору, что данное поле может и не содержаться в ответе сервера. Label() является user defined, это позволяет, к примеру, не беспокоиться о соответсвии описания и переменной.

Label

является структурой:


struct Label
{
	string text;
}
	

Все что осталось сделать, это создать клиент интерфейса. Делается это так:


auto client = new RestInterfaceClient!OpenWeather("http://api.openweathermap.org/data/2.5/");

Здесь через "!" передан интерфейс. Это означает, что шаблонный класс RestInterfaceClient использует его в compiletime. В D ваша программа сначала выполняется в compiletime, а затем в runtime. В compiletime происходит конкретизация шаблона, иными словами генерируется специальный класс, объект которого и будет использовться при выполнении программы.
Если метод начинается с get*, то генератор зарегистрирует этот обработчик по методу GET, аналогично и для post*, put*, delete*. Можно и вовсе не указывать метод, тогда генератор сам определит как с ним быть. Подробнее описано в документации к vibe.d.

Перепишем функцию index


void index(HTTPServerRequest req, HTTPServerResponse res)
{
	auto client = new RestInterfaceClient!OpenWeather("http://api.openweathermap.org/data/2.5/");

	auto weather = client.getWeather("Moscow");

	string result = "<html>";

	foreach(each; respond(weather))
	{
		auto writer = res.bodyWriter();

		if (!each.label.empty)
		{
			result ~=format("<b>%s</b>:<br/>"%s" = %s<br/>", each.label, each.name, each.value);
		}
		else
		{
			result ~= format("<i>%s</i><br/>", each.name);
		}
	}

	result ~= "</html>";

	res.writeBody(result);
}

Функция respond

import std.traits;
import std.conv;

struct A
{
	string name;

	string value;

	string label;
}

string getLabel(alias ST, alias mem)()
{

	foreach (attr; __traits(getAttributes, __traits(getMember,ST,mem)))
	{
		if (is(typeof(attr) == Label))
		{
			return attr.text;
		}
	}

	return "";

}

A[] respond(ST)(ST st)
{
	A[] ret = new A[0];

	foreach(mem; __traits(derivedMembers, ST))
	{
		static if(is(typeof(__traits(getMember,ST,mem)) == struct))
		{
			ret ~= A(mem,"","");

			ret ~= respond!(typeof(__traits(getMember,ST,mem)))(__traits(getMember,st, mem));
		}
		else
		{
			ret ~= A(mem, to!string(__traits(getMember,st, mem)), getLabel!(ST,mem) );
		}
	}

	return ret;

}
		

Шаблон getLabel, восстанавливает описание к переменной (я выше писал про соответствие переменной и описания). Шаблон respond собирает имена переменных, их значения и описания в удобный массив из структур A.

После сборки проекта и запуска, по адресу 127.0.0.1/ получим подробную информацию о погоде в Москве.

Сервер

Создать сервер, можно создав класс релизующий интерфейс API, т.е class OpenWeatherMapImpl.


class OpenWeatherImpl:OpenWeather
{
	Weather getWeather(string q)
	{
		auto client = new RestInterfaceClient!OpenWeather("http://api.openweathermap.org/data/2.5/");

		return client.getWeather(q);
	}
}
	

Этот класс дублирует показания официального сервера OpenWeatherMap. Зарегистриуерм интерфейс

router.registerRestInterface(new OpenWeatherImpl);
Все вместе


import std.traits;
import std.conv;

struct Label
{
	string text;
}

struct A
{
	string name;

	string value;

	string label;
}

string getLabel(alias ST, alias mem)()
{

	foreach (attr; __traits(getAttributes, __traits(getMember,ST,mem)))
	{
		if (is(typeof(attr) == Label))
		{
			return attr.text;
		}
	}

	return "";

}

A[] respond(ST)(ST st)
{
	A[] ret = new A[0];

	foreach(mem; __traits(derivedMembers, ST))
	{
		static if(is(typeof(__traits(getMember,ST,mem)) == struct))
		{
			ret ~= A(mem,"","");

			ret ~= respond!(typeof(__traits(getMember,ST,mem)))(__traits(getMember,st, mem));
		}
		else
		{
			ret ~= A(mem, to!string(__traits(getMember,st, mem)), getLabel!(ST,mem) );
		}
	}

	return ret;

}

struct Weather
{
	@Label("City identification")
	long id;
	
	@Label("Data receiving time, unix time, GMT")
	ulong dt;
	
	@Label("City name")
	string name;
	
	WCoord coord;
	
	WSys sys;
	
	WMain main;
	
	WWind wind;
	
	WClouds clouds;
	
	//@optional()
	//WConditions[] weather;
	
	@optional()
	WRain rain;
	
	@optional()
	WSnow snow;
}

struct WCoord
{
	@Label("City geo location, lat")
	double lat;
	
	@Label("City geo location, lon")
	double lon;
}

struct WSys
{
	@Label("System parameter, do not use it")
	double message;
	
	@Label("Country (GB, JP etc.)")
	string country;
	
	@Label("Sunrise time, unix, UTC")
	ulong sunrise;
	
	@Label("Sunset time, unix, UTC")
	ulong sunset;
}

struct WMain
{
	@Label("Temperature, Kelvin (subtract 273.15 to convert to Celsius)")
	double temp;
	
	@Label("Humidity, %")
	double humidity;
	
	@Label("Minimum temperature at the moment. This is deviation from current temp that is possible for large cities and megalopolises geographically expanded (use these parameter optionally)")
	double temp_min;
	
	@Label("Maximum temperature at the moment. This is deviation from current temp that is possible for large cities and megalopolises geographically expanded (use these parameter optionally)")
	double temp_max;
	
	@Label("Atmospheric pressure, hPa ")
	double pressure;
	
	@Label("Atmospheric pressure on the sea level, hPa") @optional()
	double sea_level;
	
	@Label("Atmospheric pressure on the ground level, hPa") @optional()
	double grnd_level;	
}

struct WWind
{
	@Label("Wind speed, mps")
	double speed;
	
	@Label("Wind direction, degrees (meteorological)")
	double deg;
	
	@Label("Wind gust, mps") @optional()
	double gust;
}

struct WClouds
{
	@Label("Cloudiness, %")
	double all;
}

struct WConditions
{
	@Label("Weather condition id")
	long id;
	
	@Label("Group of weather parameters (Rain, Snow, Extreme etc.)")
	Weather main;
	
	@Label("Weather condition within the group")
	string description;
	
	@Label("Weather icon id")
	string icon;
}

struct WRain
{
	@Label("Precipitation volume for last 3 hours, mm") @optional()
	double _3h;

}

struct WSnow
{
	@Label("Snow volume for last 3 hours, mm") @optional()
	double _3h;
}

interface OpenWeather
{
	Weather getWeather(string q);
}

class OpenWeatherImpl:OpenWeather
{
	Weather getWeather(string q)
	{
		auto client = new RestInterfaceClient!OpenWeather("http://api.openweathermap.org/data/2.5/");

		return client.getWeather(q);
	}
}

import std.stdio;

import vibe.d;

import std.string;

void main()
{
	writeln("Edit source/app.d to start your project.");

	void index(HTTPServerRequest req, HTTPServerResponse res)
	{
		auto client = new RestInterfaceClient!OpenWeather("http://api.openweathermap.org/data/2.5/");

		auto weather = client.getWeather("Moscow");

		string result = "<html>";

		foreach(each; respond(weather))
		{
			auto writer = res.bodyWriter();

			if (!each.label.empty)
			{
				result ~=format("<b>%s</b>:<br/>"%s" = %s<br/>", each.label, each.name, each.value);
			}
			else
			{
				result ~= format("<i>%s</i><br/>", each.name);
			}
		}

		result ~= "</html>";

		res.writeBody(result);
	}

	auto settings = new HTTPServerSettings;

	settings.bindAddresses = ["127.0.0.1"];

	settings.port = 80;

	auto router = new URLRouter;

	router.get("/", &index);
	
	router.registerRestInterface(new OpenWeatherImpl);

	listenHTTP(settings, router);

	runEventLoop();
}
	

По адресу 127.0.0.1/weather?q=«Moscow» получим json ответ о пагоде в Москве.

Примеры из статьи в удобном формате github.com/ntstv/viberest

Автор: ntstv

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js