Как стать автором
Обновить

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

Время на прочтение 9 мин
Количество просмотров 4.2K
В этой статье я поэтапно покажу как пользоваться 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
Теги:
Хабы:
+6
Комментарии 2
Комментарии Комментарии 2

Публикации

Истории

Ближайшие события

PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн
Weekend Offer в AliExpress
Дата 20 – 21 апреля
Время 10:00 – 20:00
Место
Онлайн