Skip to content

[ru] GitHub клиент

Aleksey Zhidkov edited this page Feb 5, 2016 · 1 revision

GitHub клиент с помощью Restler

На практике Restler-у не так уж и важно, какая технология на самом деле используется на стороне сервера, главное чтобы эндпоинты можно было описать в терминах (на данный момент только) Spring MVC Controller, а структуры - в терминах Java/Kotlin/Scala/Groovy классов.

В этом туториале мы разработаем небольшое приложение на Kotlin, которое на вход получает имя пользователя GitHub и выдаёт список языков, используемый в нефоркнутых репозиториях.

Полная версия исходного кода досупна здесь

UserRepo DTO

Давайте рассмотрим класс, который мы будем использовать для разбора ответа от GitHub:

1.	class UserRepo @JsonCreator constructor(
2.	        val name: String,
3.	        val fork: Boolean,
4.	        val language: String?)

В объекты этого класса мы будем разбирать ответы от https://api.github.com/users/{userName}/repos. Большую часть полей ответа мы игнорируем и выбираем только 3 из них:

  • name - имя репозитория, потребуется для отладочного вывода
  • fork - флаг является ли репозиторий форком. Нас интересуют только не форкнутые репозитории
  • language - будем использовать для ранней фильтрации репозиториев, которые не содержат кода на известных языках

GitHub

Теперь давайте рассмотрим класс GitHub который будет выступать в роли дескриптора сервиса.

1.	@RestController
2.	open class GitHub {
3.	
4.	    @RequestMapping("/users/{userName}/repos")
5.	    open fun userRepos(@PathVariable userName: String): List<UserRepo> = emptyList()
6.	
7.	    @RequestMapping("/repos/{userName}/{repo}/languages")
8.	    open fun repoLanguages(@PathVariable userName: String, @PathVariable repo: String): DeferredResult<Map<String, Int>> =
9.	            DeferredResult<Map<String, Int>>()
10.	}

Данный класс содержит два метода, которые описывают эндпоинты для получения списка репозиториев пользователя и списка языков репозитория.

Аннотация @RequestMapping используется для того чтобы задать шаблон эндпоинта, а переменные указываются в фигурных скобках. Для каждой переменной шаблона, необходимо определить параметр метода с таким же именем, который обязательно должен быть проаннотирован @PathVariable. Вы так же можете ввести дополнительные параметры проаннотированные @RequestParam для передачи параметров запроса, но в нашем примере это не требуется.

Обратите внимание на тип результата метода repoLanguages - DeferredResult. Этот тип имеет особую поддержку в Restler и методы возвращающие этот тип обрабатываются в отдельном пуле потоков. Эта фича Restler позволит нам выполнить запросы языков репозиториев параллельно без написания ни единой строчки дополнительного кода.

В случае разработки на Java описать сервис лучше воспользовавшись интерфейсом, а не классом, но Kotlin пока что не поддерживает запись имён параметров интерфейсов в байткод. Это ограничение можно обойти дублируя имена параметров в аннотациях @PathVariable, но т.к. имена будут меняться с большой вероятностью, избавление от дублирования в этом месте стоит небольшого шума в виде фейковых тел методов.

Основной код

Теперь давайте рассмотрим собственно код решающий нашу задачу.

1.	fun main(args: Array<String>) {
2.        val springMvcSupport = SpringMvcSupport().
3.            addJacksonModule(ParanamerModule())
4.        val github = Restler("https://api.github.com/", springMvcSupport).
5.            build().
6.            produceClient(GitHub::class.java)   
7.	    val userName = if (args.size() > 0) args[0] else "aleksey-zhidkov"
8.	    val userRepos = github.userRepos(userName)
9.	
10.	    val userLngs = userRepos.
11.	            filter { it.language != null && !it.fork }.
12.	            map { Pair(it.name, github.repoLanguages(userName, it.name)) }.
13.	            flatMap {
14.	                val (name, res) = it
15.	                while (!res.hasResult()) Thread.sleep(100)
16.	                println("$name: ${res.getResult()}")
17.	                (res.getResult() as Map<String, Int>).keySet() }.
18.	            toSet()
19.	    println("User languages: $userLngs")
20.	}

Рассмотрим этот код по частям. В строках 2-5 создаётся билдер сервиса, добавляется Jackson модуль Paranamer (необходим для того чтобы не дублировать имена параметров конструктора в аннотациях @JsonParameter), строится сервис и создаётся клиент на базе класса GitHub.

В строках 7-8 проверяется было ли передано имя пользователя и если нет то в качестве дефолтного имени пользователя используется "aleksey-zhidkov". Затем для полученного имени пользователя получается список его репозиториев на GitHub.

В строках 10-18 содержится основное мясо нашей программы, давайте рассмотрим каждую строчку в отдельности:

  1. val userLngs = userRepos. - объявляется переменная, в которой будет содержаться список языков программирования используемых пользователем.
  2. filter { it.language != null && !it.fork }. - отфильтровываем репозитории для которых не определён язык и которые являются форками
  3. map { Pair(it.name, github.repoLanguages(userName, it.name)) }. - каждый репозиторий превращаем в пару <Имя, Список Языков>. Благодаря DeferredResult вызов repoLanguages становится асинхронным.
  4. flatMap { - превращаем список списков языков программирования в плоский список
  5. val (name, res) = it - для удобства разбиваем пару на локальные переменный
  6. while (!res.hasResult()) Thread.sleep(100) - грязно, но просто ждём выполнения асинхронного вызова
  7. println("$name: ${res.getResult()}") - для отладки выводим языки каждого отдельного репозитория
  8. (res.getResult() as Map<String, Int>).keySet() }. - GitHub по запросу языков возвращает мапу язык -> количество байтов и мы из этой мапы берём только ключи.
  9. toSet() - простой способ избавиться от дубликатов.
  10. println("User languages: $userLngs") - выводим на экран результат работы

Заключение

В этом туториале мы рассмотрели как Restler позволяет написать программу всего в 34 строки кода которая, может асинхронно получать данные от API GitHub без единой строчки бойлерплейт связанного с выполнением HTTP-запросов или параллельным программированием.