уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Это вторая часть большой статьи про использование API онлайн-сервисов в Delphi. В первой части мы определились с тем, что необходимо знать прежде, чем начинать свои разработки.

В этой части мы рассмотрим весь процесс написания своего первого модуля Delphi для работы с API онлайн-сервиса. 

Так как эта часть посвящено работе в Delphi, то здесь будет достаточно много кода.

Чтобы избежать лишних вопросов, сразу скажу, что вся дальнейшая работа будет выполняться в Delphi XE7, которая значительно отличается от Delphi 7. 

Авторизация и получение доступа к API

Один из ключевых моментов реализации любого API — авторизация пользователя. Как уже было сказано ранее, в настоящее время наиболее активно используется авторизация пользователей по протоколу OAuth, а точнее — OAuth 2.0. Чтобы авторизовать пользователя по OAuth вы можете написать свой собственный класс или же, если Вы используете Delphi XE5-XE7, то можете воспользоваться компонентами REST Client Library. Чтобы не повторяться о том, как это сделать, я просто приведу здесь ссылки на статьи где рассматривалась авторизация по OAuth в разных онлайн-сервисах:

  1. Серия статей про OAuth в Google:
    1. Google API в Delphi. OAuth для Delphi-приложений,
    2. Google API в Delphi. Обновление модуля для OAuth,
    3. Решение проблем с Google OAuth 2.0. для Win-приложений,
    4. Тестирование запросов к API Google средствами Delphi. Компонент OAuthClient для Delphi XE — XE3.
  2. Использование REST Client Library для OAuth:
    1. Delphi XE5: REST Client Library,
    2. Delphi: авторизация по OAuth 2.0 в Dropbox своими силами,
    3. REST Client Library: использование API ВКонтакте

Однако встречаются и такие API в которых авторизация пользователя проводится по собственным протоколам и правилам. В этом случае вам необходимо самостоятельно реализовать процедуру авторизации (для чего необходимо знать всё то, о чем сказано в первой части статьи). Рассмотрим пример работы с подобными API.

Пример с etxt.ru

Начинаем читать документацию. Что нам говорит сервис:

В каждом запросе должен присутствовать набор обязательных параметров. Также для каждой функции в ее документации определены дополнительные параметры, нужные только для этой функции. Текстовые значения параметров должны быть преданы в кодировке UTF-8. Одинаковые для всех функций параметры:
  1. method (string) — название вызываемого метода, например, users.getList; обязательный параметр
  2. sign (string) — подпись запроса; обязательный параметр
  3. token (string) — API-ключ текущего пользователя

Порядок следования параметров в запросе значения не имеет, порядок параметров важен только при расчете подписи.

API-ключ token уникален для каждого пользователя и его можно узнать в разделе «Мой профиль/Настройка интерфейса».

Подпись sign расcчитывается по алгоритмe, приведенному ниже. Подписываются только параметры, переданные по GET.

Важные моменты в этой части документации выделены жирным:

  1. Все текстовые параметры запроса передаются в кодировке UTF-8
  2. Для каждого запроса нам необходимо рассчитывать по специальному алгоритму подпись.
  3. Порядок следования параметров в самом запросе не важен
  4. При расчёте подписи все параметры должны следовать в строго определенном порядке.

Что это всё значит? Ну, с кодировкой, допустим, всё понятно. В остальном же получается, что доступ к API предоставляется нам только, если мы передадим на сервер два верных параметра — это token (он не меняется и получается при регистрации пользователя) и sign — подпись, которая меняется при каждом запросе. Следовательно, нам необходимо в своей программе предусмотреть специальный метод, который будет рассчитывать нам эту подпись. Попробуем написать такой метод.

Снова смотрим документацию. Вот, что говорит нам сервис про алгоритм подписи запроса:

Алгоритм использует отдельный ключ api_pass, который мы настоятельно рекомендуем вам хранить только на ваших серверах и использовать только при запросах с них к серверу Биржи. Данный ключ задается в разделе «Мой профиль/Настройки интерфейса».

sign = md5(params.md5(api_pass.'api-pass'))

Значение params — это конкатенация пар «имя=значение» отсортированных в алфавитом порядке по «имя», где «имя» — это название параметра, передаваемого в функцию API, «значение» — значение параметра. Разделитель в конкатенации не используется. Параметр sign при расчете подписи не учитывается, все остальные параметры запроса должны учитываться при расчете.

Теперь попробуем составить алгоритм расчёта такой подписи. Итак, что нам нужно:

  1. Найти в ключ api_pass (он как и token нам выдается сервисом один раз и на всю жизнь)
  2. Необходимо отсортировать все параметры запроса в алфавитном порядке
  3. Необходимо произвести конкатенацию, т.е. «склеивание» параметров
  4. Рассчитать MD5 для строки api_pass.’api-pass’
  5. Полученную в п.3 строку «склеить» с результатов п.4
  6. Рассчитать для полученной в п.5 строки MD5- это и будет наша подпись.

Реализуем этот алгоритм в Delphi.

Для работы я буду использовать только те компоненты и классы, которые есть в поставке Delphi XE7 — это библиотека Indy и библиотека для работы с JSON, которая в XE7 находится в модуле System.JSON.

Так как в дальнейшем предстоит использовать этот API, то я создал отдельный класс, который постепенно будет «обрастать» новыми методами и свойствами для работы с API. Класс этот вынесен в отдельный модуль и на данном этапе выглядит так:

type
  TEtxtAPI = class
  private
    FToken: string;
    FApiPass: string;
  public
    procedure SignRequest(AParams: TStringList);
    property Token: string read FToken write FToken;
    property ApiPass: string read FApiPass write FApiPass;
end;

Свойства Token и ApiPass — это ключ доступа и пароль к API, которые, как мы уже выяснили никогда не меняются. Теперь рассмотрим метод SignRequest, который будет вычислять подпись и добавлять её к параметрам запроса:

procedure TEtxtAPI.SignRequest(AParams: TStringList);
var Params: string;
    I: Integer;
    md5indy: TIdHashMessageDigest;
    hashPass: string;
begin
  if not Assigned(AParams) then Exit;
  //удаляем подпись, если она есть в параметрах
  if AParams.IndexOfName('sign')>-1 then
    AParams.Delete(AParams.IndexOfName('sign'));
  //проверяем наличие ключа token и добавляем его в запрос
  if AParams.Values['token']=EmptyStr then
    AParams.Values['token']:=FToken;
  //сортируем все параметры
  AParams.Sort;
  //конкатенация пар
  for I := 0 to AParams.Count-1 do
    Params:=Params+AParams[i];
  md5indy:=TIdHashMessageDigest5.Create;
  try
    //рассчитываем md5 пароля
    hashPass:=md5indy.HashStringAsHex(FApiPass+'api-pass');
    //расчитываем md5 всей подписи и записываем её в параметры
    AParams.Values['sign']:=LowerCase(md5indy.HashStringAsHex(params+LowerCase(hashPass)));
  finally
    md5indy.Free;
  end;
end;

Так как параметр sign не участвует в расчёте подписи, то вначале мы проверили есть ли такой параметр в списке и, если sign присутствует, то удалили его. Далее мы проверили наличие параметра token и, при необходимости, добавили его в список. После этого мы отсортировали весь список параметров в алфавитном порядке и составили строку params. Для расчёта MD5 мы воспользовались возможностями класса  TIdHashMessageDigest, который находится в модуле IdHashMessageDigest. Расчёт производился в два шага:

  1. Рассчитали хэш пароля API.
  2. Полученный хэш добавили к строке params и рассчитали новый хэш для полученной строки

После этого добавили подпись в параметр sign запроса. Теперь список AParams содержит все необходимые параметры для выполнения запроса к API. Как проверить, что рассчитанная подпись верная? Очень просто — попробовать выполнить какой-нибудь простой запрос к API.

Выполнение запросов к API

Выполнив авторизацию и получив доступ к API мы можем выполнять различные запросы к API. Различные API требуют могут предъявлять разные требования к выполнению запросов. И прежде, чем начинать писать код в Delphi, опять же, следует внимательно прочитать документацию к API и определиться с тем как могут выглядеть различные запросы к одному и тому же API.

В URL запроса всегда присутствует общая для всех запросов часть. Так, например, если используется API, использующий REST-принципы (любой API Яндекса, Google, ВКонтакте и т.д.), то запросы к такому серверу могут иметь следующий вид:

  • http://example.com/api/book/1
  • http://example.com/api/lists/
  • http://example.com/api/authors/123
  • и т.д.

Видите? В каждом из запросов есть http://example.com/api/. В различной документации к API этот URL может называться по-разному: точка доступа, Base URL или просто URL запроса. Base URL всегда следует выносить в раздел констант. Объясню почему это стоит делать. Причин две:

  1. Для того, чтобы не использовать в дальнейшем в своем коде одну и ту же строку по 100 раз и избегать случайных опечаток, которые потом довольно сложно обнаружить в большом массиве кода
  2. Редко, но тем не менее встречается ситуация, когда сервер изменяет Base URL полностью или частично. Если произойдет смена Base URL, например, в новой версии API, то вам будет достаточно изменить всего одну константу в коде.

Определившись с Base URL можно начать реализовывать выполнение запросов к API в Delphi. Рассмотрим это, опять же, на примере API etxt.ru.

Пример выполнения запросов к etxt.ru

Определяем Base URL. В случае с etxt.ru этот URL указан в документации и выглядит так:

https://www.etxt.ru/api/json/

Этот URL не изменяется — изменяются только параметры запроса. Так, например, запрос к списку категорий может выглядеть так:
https://www.etxt.ru/api/json/?token=12345&method=categories.listCategories&sign=1234fde4567ef
при запросе списка папок запрос будет таким:
https://www.etxt.ru/api/json/?token=12345&method=folders.listFolders&sign=1dnt34dde4567ee

То есть, наша константа в Delphi может выглядеть так:

const
  cBaseURL = 'https://www.etxt.ru/api/json/?%s';

Так как сервер требует доступа по https, то для дальнейшей работы нам потребуются два компонента Indy: TidHTTP и TIdSSLIOHandlerSocketOpenSSL, которые находятся, соответственно, в модулях idHTTP и idSSLOpenSSL. Так же нам потребуются две динамические библиотеки: libeay32.dll и ssleay32.dll, которые вы можете скачать со страницы с исходниками.

Добавим TidHTTP и  TIdSSLIOHandlerSocketOpenSSL в наш класс для работы с API:

type
  TEtxtAPI = class
  private
    FHTTP: TidHTTP;
    FSSLIOHandler: TIdSSLIOHandlerSocketOpenSSL;
    FToken: string;
    FApiPass: string;
  public
    constructor Create;
    destructor Destroy;override;
    procedure SignRequest(AParams: TStringList);
    property Token: string read FToken write FToken;
    property ApiPass: string read FApiPass write FApiPass;
end;
 
constructor TEtxtAPI.Create;
begin
  inherited;
  FSSLIOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
  FHTTP := TIdHTTP.Create(nil);
  FHTTP.IOHandler := FSSLIOHandler;
  FHTTP.HandleRedirects := True;
end;
 
destructor TEtxtAPI.Destroy;
begin
  FHTTP.Free;
  FSSLIOHandler.Free;
  inherited;
end;

Динамические библиотеки необходимо положить в папку с exe-файлом приложения. Теперь напишем новый метод нашего класса, который будет выполнять GET-запрос на сервер и возвращать ответ. С учётом того, что у нас уже написана процедура подписи запроса, наш новый метод может выглядеть так:

function TEtxtAPI.GET(const AParams: TStringList): string;
begin
  SignRequest(AParams);
  AParams.Delimiter:='&';
  FHTTP.Disconnect;
  Result:=FHTTP.Get(Format(cBaseURL,[AParams.DelimitedText]));
end;

Метод получает на входе список параметров, затем подписывает запрос, отправляет его на сервер и записывает полученный ответ в Result. Проверим работу нашего метода.
Для этого создадим новое приложение VCL, подключим в uses модуль с нашим классом, а на главную форму бросим всего два компонента — TButton и TMemo:

etxt_api

В обработчике OnClick кнопки напишем следующий код:

procedure TForm3.Button1Click(Sender: TObject);
var Params: TStringList;
    Etxt:TEtxtAPI;
begin
  //создаем список для записи параметров
  Params:=TStringList.Create;
  try
    //записываем обязательный параметр - method
    Params.Values['method']:='categories.listCategories';
    //создаем объект для работы с API
    Etxt:=TEtxtAPI.Create;
    try
      Etxt.Token:='СЮДА_ЗАПИСЫВАЕМ_СВОЙ_TOKEN';
      Etxt.ApiPass:='СЮДА_ЗАПИСЫВАЕМ_СВОЙ_ПАРОЛЬ';
      //выполняем запрос и записываем результат в Memo
      Memo1.Lines.Text:=Etxt.GET(Params);
    finally
      //освобождаем память
      Etxt.Free;
    end;
  finally
    //освобождаем память
    Params.Free;
  end;
end;

Если наша подпись была рассчитана верно, то в результате мы должны получить в Memo JSON-объект с данными по категориям. Запускаем приложение, кликаем по кнопке и видим следующий результат:
etxt_api2
Результат получен, следовательно, можно приступать к следующему шагу работы над API — разбору результатов запроса.

Парсинг результатов запроса

На предыдущем шаге работы с API мы получили от сервера «сырые» для будущего приложения  данные. То есть на данный момент ни наше приложение ни наш класс для работы с API «не знают» что делать с данными — это простая строка, которую необходимо правильно разобрать и представить пользователю приложения.

В своей работе с самыми различными API я придерживаюсь следующих двух положений:

  1. Один класс используется непосредственно для обмена данными с сервером: в этом классе реализованы методы выполнения GET-, POST-, DELETE- и других запросов к API по HTTP(S). Результатом выполнения таких методов всегда является строка.
  2. Второй класс используется для работы с объектами и методами API — здесь уже реализуются конкретные методы API (из документации), производится парсинг данных и т.д. Этот класс использует методы и свойства первого и может быть как дочерним, так и отдельным классом.

Мне такая схема работы представляется наиболее удобной в плане отладки. Вы же в своих приложениях вольны делать как угодно.

Для разбора ответов сервера, как я упоминал в первой части статьи, Вы должны понимать, хотя бы, что такое XML и JSON и как их можно разобрать в Delphi. Данные, полученные от сервера, внутри своей программы вы можете хранить и представлять как вам угодно — хранить в виде простой строки, создавать свои собственные классы, записи (record) и т.д. Внутри вашего приложения — вы хозяин и только Вы решаете как хранить и использовать полученные данные.

Опять же (и я не устану это повторять), прежде чем писать код необходимо прочитать документацию по API. На этот раз надо изучить то:

  1. какие свойства содержат возвращаемые объекты и типы данных этих свойств
  2. какие общие свойства есть у всех объектов API (и есть ли такие общие свойства, в принципе).

Если упустить этот момент, то в итоге вы можете сильно «раздуть» свой код повторяющимися свойствами родственных объектов. Например, в API Box.com можно встретить объекты Folder (папка) и MiniFolder (краткая информация о той же папке). В этом случае лучше всего в Delphi сделать класс TFolder дочерним от TMinifolder — упростит, в дальнейшем, отладку приложения, сократит код и, плюс, поможет избежать ошибок при парсинге.

Чем просматривать ответы сервера? Если данные приходят в JSON, то могу вам порекомендовать использовать онлайн-сервис  http://jsonviewer.stack.hu/. Вот как выглядит в этом сервисе объект, полученный в предыдущем примере:

etxt_api3Как видно на рисунке, все поля объектов представляют из себя обычные строки. Разобрать такой объект будет достаточно просто.

Для примера, рассмотрим как можно разбирать, хранить и представлять данные от сервера etxt.ru.

Разбор данных etxt.ru

Итак, класс для работы с сервером по HTTP у нас есть (впоследствии мы можем добавить в него, например, метод выполнения POST-запроса к серверу или любой другой по необходимости) — его содержимого нам пока хватит для реализации разных методов API.

В предыдущем примере мы получили большой JSON-объект с тематическими категориями. Посмотрим из чего состоит объект категории. Читаем документацию:

categories.listCategories

Возвращает список тематических категорий заказов/статей, отсортированный по названию категории.

Результат
Поле Описание
id_category Идентификатор категории
id_parent Идентификатор родительской категории
name Название категории
keyword Ключевое слово категории

На языке Delphi это может быть, например, такой класс:

TCategory = class
private
 FId: string;
 FParentID: string;
 FName: string;
 FKeyword: string;
public
 procedure Parse(AValue: TJsonValue);
 property Id: string read Fid;
 property ParentID: string read FParentID;
 property Name: string read FName;
 property Keyword: string read FKeyword;
end;

Все поля класса доступны только для чтения, т.к. в API нет методов создания/редактирования категорий, а следовательно и мы изменять значения полей не будем.
Метод Parse производит разбор JSON-объекта категории и записывает полученные значения в поля класса, т.е. в Parse должен передаваться объект, следующего содержания:
{"id_category" : "1911", "id_parent" : "0", "name" : "-- \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f \u043d\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0430", "keyword" : "not", "type" : "object" }
Метод Parse выглядит следующим образом:

function PairIndex(const AName: string; APairNames: array of string):integer;
var
  I: Integer;
begin
  for I := Low(APairNames) to High(APairNames) do
    if SameText(AName, APairNames[i]) then
      Exit(I);
  Result:=-1;
end;
 
procedure TCategory.Parse(AValue: TJsonObject);
const
  cPairs: array [0..3]of string = ('id_category','id_parent','name','keyword');
var I: Integer;
    APairName: string;
begin
  if not Assigned(AValue) then Exit;
  for I := 0 to AValue.Count-1 do
    begin
      APairName:=AValue.Pairs[i].JsonString.Value;
      case PairIndex(APairName, cPairs) of
        0:FId:=AValue.Values[APairName].Value;//id_category
        1:FParentID:=AValue.Values[APairName].Value;//id_parent
        2:FName:=AValue.Values[APairName].Value;//name
        3:FKeyword:=AValue.Values[APairName].Value;//keyword
      end;
    end;
end;
Внимание: этот код будет работать безошибочно только в Delphi XE5-XE7. Для более ранних версий Delphi вам вместо свойств Values и Pairs у TJsonObject надо использовать перечислители. О том как их использовать можно посмотреть вот в этой статье про API ВКонтакте.
В этом методе мы разобрали один объект одной категории, а сервер в ответе присылает нам набор объектов разных категорий. Разберемся как и где парсить весь ответ сервера.

Как я сказал выше — для реализации методов API я, обычно, пишу отдельный класс. Самое время начать его писать для etxt.ru. Новый класс можно представить следующим образом:

TEtxtClient = class(TEtxtAPI)
private
  FCategories: TObjectList;
  FLastError: string;
  function GetCatCount: integer;
  function GetCategory(AIndex:integer):TCategory;
  function GetJson(const AResponseString: string):TJSONObject;
public
  constructor Create;
  destructor Destroy;override;
  procedure ListCategories;
  property LastError: string read FLastError;
  property CatCount: integer read GetCatCount;
  property Category[AIndex: integer]:TCategory read GetCategory;
end;

При этом в родительском классе TEtxtAPI методы отправки/получения данных по HTTP я перенес в секцию protected:

type
  TEtxtAPI = class
  private
    [...]
  protected
    function GET(const AParams: TStringList):string;
  public
    [...]
end;

Посмотрим из чего состоит класс TEtxtClient.

FCategories: TObjectList;

Список в котором будут храниться объекты категорий. Свойства:

  property CatCount: integer read GetCatCount;
  property Category[AIndex: integer]:TCategory read GetCategory;

Возвращают, соответственно, количество категорий в списке и объект категории с индексом AIndex в списке.
Свойство

property LastError: string read FLastError;

Хранит информацию по последней ошибке доступа к API. Об ошибках и исключениях при работе с API мы поговорим в третьей части.
Метод:

procedure ListCategories;

Выполняет одноименный метод API (см. документацию сервиса), производит разбор полученного JSON-объекта и заполняет список FCategories. Посмотрим на него подробнее:

procedure TEtxtClient.ListCategories;
var AJson: TJSONObject;
    AValue: TJsonValue;
    AParams: TStringList;
    AResponse: string;
    I: Integer;
begin
  //очистили список от результатов предыдущего запроса
  FCategories.Clear;
  AParams:=TStringList.Create;
  try
    //записали параметр method - остальные обязательные параметры запишутся при
    //расчёте подписи к запросу в методе TEtxtAPI.SignRequest
    AParams.Values['method']:='categories.listCategories';
    //отправили запрос на сервер и получили "сырую" строку с JSON
    AResponse:=GET(AParams);
    //попробовали получить JSON-объект из строки
    AJson:=GetJson(AResponse);
    //не удалось получить объект, значит сервер вернул ошибку
    if not Assigned(AJson) then
      raise Exception.Create(FLastError);
    try
    //проходим по каждой паре в полученном JSON-объекте
    for I := 0 to AJson.Count-1 do
      begin
        //получили значение пары
        AValue:=AJson.Values[AJson.Pairs[i].JsonString.Value];
        //значение пары является объектом
        if AValue is TJSONObject then
          begin
            //добавляем в список новый объект категории
            FCategories.Add(TCategory.Create);
            //разбираем объект категории
            FCategories.Last.Parse(AValue as TJSONObject);
          end;
      end;
      finally
        AJson.Free; 
      end;
  finally
    AParams.Free;
  end;
end;

Чтобы было понятнее, что мы делали с JSON-объектом в этой процедуре, я представлю процесс разбора в виде картинки:
etxt_api4
Теперь мы можем переписать код нашей программы (обработчик OnClick кнопки) и выводить в Memo уже не «сырой» ответ сервера, а, например, имена категорий:

procedure TForm3.Button1Click(Sender: TObject);
var  Etxt:TEtxtClient;
     I: Integer;
begin
  Etxt:=TEtxtClient.Create;
  try
    Etxt.Token:='СЮДА_ЗАПИСЫВАЕМ_СВОЙ_TOKEN';
    Etxt.ApiPass:='СЮДА_ЗАПИСЫВАЕМ_СВОЙ_ПАРОЛЬ';
    //запросили с сервера список категорий
    Etxt.ListCategories;
    //прошли по списку и вывели имена категорий в Memo
    for I := 0 to Etxt.CatCount-1 do
      Memo1.Lines.Add(Etxt.Category[i].Name)
  finally
    Etxt.Free;
  end;
end;

Сравните этот код с представленным ранее в части «Пример выполнения запросов к etxt.ru» — код стал короче и понятнее.

Выполнение взаимосвязанных методов

Чтобы закрепить материал, реализуем ещё пару связанных методов — когда от результата выполнения одного метода (или от значения какого-либо параметра из данных предыдущего запроса) зависит результат выполнения другого.

Например, попробуем получить список исполнителей и подробную информацию по выбранному пользователю. Если мы не можем получить список, то следовательно и получить подробную информацию по отдельному пользователю мы не получим. Если мы не получим из списка верные значения свойств объекта пользователя, то и результатом выполнения связанного метода будет ошибка.

Снова смотрим документацию. Получить список пользователей (исполнителей) можно, выполнив метод users.getList. Этот метод, в отличие от предыдущего, требует указания нескольких параметров, а именно:

  • count (integer) Число пользователей для выборке, не более 100 за запрос (по умолчанию)
  • from (integer) Смещение от последней записи в выборке, по умолчанию 0
  • rate_from (integer) Фильтрация по рейтингу, начиная с данного значения
  • rate_out (integer) Фильтрация по рейтингу, заканчивая данным значением
  • online (integer) Флаг онлайн статуса на бирже, 1 — онлайн, 0 — офлайн, по умолчанию все вместе

Соответственно, и новый метод в классе TEtxtClient должен будет на входе принимать эти параметры. Например, можно создать такой метод:

procedure ListUsers(ACount, AFrom, ARateFrom, ARateOut, AOnline: integer);

А можем, для повышения читабельности нашего кода и избежания неправильного указания последнего параметра сделать так:

procedure ListUsers(ACount, AFrom, ARateFrom, ARateOut:integer; AOnline: boolean);

Как вам будет угодно. Как по мне, так более удобно использовать второе описание, т.е. когда статус пользователя передается в метод как значение boolean.
Теперь посмотрим, какие объекты должен вернуть нам сервер. А сервер нам вернет опять же объект в котором каждая пара будет представлять из себя объект одного пользователя. JSON-объект будет выглядеть таким образом:
etxt_api5

 

Следовательно, т.к. мы можем в дальнейшем попросить у сервера подробную информацию на любого пользователя, то стоит сохранить полученный список в программе. Снова создаем новый класс, который будет хранить информацию о пользователе:

TUser = class
private
  Fid: integer;
  FLogin: string;
  FFio:string;
  FDescription: string;
  FCountry: string;
  FCity: string;
  FOnline: boolean;
  FRegdate: TDateTime;
  FRate: integer;
public
  procedure Parse(AValue: TJSONObject);virtual;
  property Id: integer read FId;
  property Login: string read FLogin;
  property Fio:string read FFio;
  property Description: string read FDescription;
  property Country: string read FCountry;
  property City: string read FCity;
  property Online: boolean read FOnline;
  property Regdate: TDateTime read FRegdate;
  property Rate: integer read FRate;
end;

Метод Parse выглядит практически также, как и в классе TCategory:

procedure TUser.Parse(AValue: TJSONObject);
const
  cPairs: array [0..8]of string = ('id_user','login','fio','description','country','city','online','regdate','rate');
var I: Integer;
    APairName: string;
begin
  if not Assigned(AValue) then Exit;
  for I := 0 to AValue.Count-1 do
    begin
      APairName:=AValue.Pairs[i].JsonString.Value;
      case PairIndex(APairName, cPairs) of
        0:Fid:=StrToInt(AValue.Values[APairName].Value);//'id_user'
        1:FLogin:=AValue.Values[APairName].Value;//'login'
        2:FFio:=AValue.Values[APairName].Value;//'fio'
        3:FDescription:=AValue.Values[APairName].Value;//'description'
        4:FCountry:=AValue.Values[APairName].Value;//'country'
        5:FCity:=AValue.Values[APairName].Value;//'city'
        6:FOnline:=AValue.Values[APairName].Value='1';//'online'
        7:FRegdate:=UnixToDateTime(StrToInt(AValue.Values[APairName].Value));//'regdate''rate'
      end;
    end;
end;

Теперь допишем класс TEtxtClient и реализуем в нем метод API users.getList:

TEtxtClient = class(TEtxtAPI)
private
  [...]
  FUsers: TObjectList;
  [...]
  function GetUserCount: integer;
  function GetUser(AIndex: integer):TUser;
public
  [...]
  procedure ListUsers(ACount, AFrom, ARateFrom, ARateOut: integer; AOnline: boolean);
  [...]
  property UserCount: integer read GetUserCount;
  property User[AIndex: integer] read GetUser;
end;

Метод ListUsers можно представить так:

procedure TEtxtClient.ListUsers(ACount, AFrom, ARateFrom, ARateOut: integer; AOnline: boolean);
var AJson: TJSONObject;
    AValue: TJsonValue;
    AParams: TStringList;
    AResponse: string;
    I: Integer;
begin
  AParams:=TStringList.Create;
  try
    AParams.Values['method']:='users.getList';
    AParams.Values['count']:=IntToStr(ACount);
    AParams.Values['from']:=IntToStr(AFrom);
    AParams.Values['rate_from']:=IntToStr(ARateFrom);
    AParams.Values['rate_out']:=IntToStr(ARateOut);
    if AOnline  then
      AParams.Values['online']:='1'
    else
      AParams.Values['online']:='0';
 
    AResponse:=GET(AParams);
    if AResponse.IsEmpty then
      Exit;
    AJson:=GetJson(AResponse);
 
    if not Assigned(AJson) then
      raise Exception.Create(FLastError);
    try
    for I := 0 to AJson.Count-1 do
      begin
        AValue:=AJson.Values[AJson.Pairs[i].JsonString.Value];
        if AValue is TJSONObject then
          begin
            FUsers.Add(TUser.Create);
            FUsers.Last.Parse(AValue as TJSONObject);
          end;
      end;
      finally 
        AJson.Free 
      end;
  finally
    AParams.Free;
  end;
end;

В этом методе чистить список FUsers не надо, т.к. сервер может вернуть вам максимум 100 пользователей за 1 запрос, а на деле их больше 500 000 и, если вы вдруг решите, что вам нужен список всех пользователей, то придётся выполнять этот метод несколько раз (до тех пор пока сервер не вернет вам всех) и в этом случае очистка списка внутри приведет к тому, что в конце концов вы сохраните только 100 последних пользователей. Для очистки списка лучше предусмотреть отдельный метод класса:

procedure TEtxtClient.ClearUserList;
begin
  FUsers.Clear;
end;

Перейдем к следующему методу API — получению подробной информации по конкретному пользователю. Подробную информацию по конкретному пользователю возвращает нам метод users.getUser. Снова смотрим в документацию. Методу требуются следующие параметры:

  • id (integer) — Идентификатор пользователя, полуобязательный параметр, имеет приоритет над параметром login
  • login (string) Логин пользователя, полуобязательный параметр, может указываться при отсутствии параметра id

При успешном выполнении запроса сервер вернет нам информацию по пользователю, включающую в себя всю информацию из класса TUser, который мы уже написали и плюс к этому следующие свойства:

  • photo — путь до аватара пользователя
  • group — название группы пользователя
  • works — виды указанных пользователем работ, только для исполнителя
  • categories — категории, указанные пользователем, только для исполнителя
  • portfolio — число работ в портфолио, только для исполнителя

Объект подробной информации о пользователе может выглядеть вот так:

etxt_api6

Как видно из рисунка, представленный объект уже имеет вложенные объекты, такие как works и categories. Более того, в объекте categories содержаться не объекты категорий или их названия, а только их id, то есть для вывода названий категорий пользователя нам предварительно надо будет загрузить их список, выполнив уже имеющийся метод ListCategories, а потом проводить по полученному списку поиск категории с необходимым id (как это делать рассказано в третьей части).

Теперь обратим внимание на вложенный объект works. Об этом объекте в документации нет ни слова (такое тоже часто встречается). Чтобы понять , что означает поле work можно посмотреть, например, несколько профилей разных пользователей и определить, что это поле содержит следующие значения:

  1. 1 — Копирайтинг
  2. 2 — Рерайтинг
  3. 3 — Перевод
  4. 4 — SEO-копирайтинг

Других вариантов нет, но при обновлении сервиса и, соответственно, API — могут появиться (и это стоит помнить).
Теперь подумаем, как представить новый класс в Delphi? Представить новый класс можно, например, так:

TUserInfo = class(TUser)
private
  FPhoto: string;
  FGroup: string;
  FWorks: TStrings;
  FCategories: TStrings;
  FPortfolio: integer;
  function GetWorksCount: integer;
  function GetCatCount: integer;
  function GetCategoryID(AIndex: integer): string;
  function GetWork(AIndex:integer):string;
public
  constructor Create;
  destructor Destroy;override;
  procedure Parse(AValue: TJSONObject);override;
  property Photo: string read FPhoto;
  property Group: string read FGroup;
  property Work[AIndex:integer]: string read GetWork;
  property CategoryID[AIndex:integer]: string read GetCategoryID;
  property Portfolio: integer read FPortfolio;
  property WorkCount:integer read GetWorksCount;
  property CatCount: integer read GetCatCount;
end;

Как видите, так как в подробной информации повторяется вся информация из TUser, то новый класс сделан дочерним для TUser, а метод Parse теперь стал override и выглядит следующим образом:

procedure TUserInfo.Parse(AValue: TJSONObject);
 
procedure ParseCategories(ACategoryObject: TJSONObject);
var i:integer;
    AName: string;
begin
  for I := 0 to ACategoryObject.Count-1 do
    begin
      AName:=ACategoryObject.Pairs[i].JsonString.Value;
      if SameText(AName,'type') then
        continue;
      FCategories.Add(ACategoryObject.Values[AName].Value);
    end;
end;
 
procedure ParseWorks(AWorksObject: TJSONObject);
const
  cWorks: array [1..4] of string = ('Копирайтинг','Рерайтинг','Перевод','SEO-копирайтинг');
var i:integer;
    AName: string;
    AWork: TJSONObject;
    AWorlID: integer;
begin
  for I := 0 to AWorksObject.Count-1 do
    begin
      AName:=AWorksObject.Pairs[i].JsonString.Value;
      if SameText(AName,'type') then
        continue;
      AWork:=AWorksObject.Values[AName] as TJSONObject;
      AWorlID:=StrToInt(AWork.Values['work'].Value);
      if (AWorlID>0)and(AWorlID<5) then
        FWorks.Add(cWorks[AWorlID])
      else
        FWorks.Add('Неизвестная работа')
    end;
end;
 
const
  cPairs: array [0..4]of string = ('photo','group','works','categories','portfolio');
var I: Integer;
    APairName: string;
begin
  //выполняем родительский метод Parse
  //заполняем поля id, fio и т.д.
  inherited Parse(AValue);
  //парсим оставшиеся поля
  if not Assigned(AValue) then Exit;
  for I := 0 to AValue.Count-1 do
    begin
      APairName:=AValue.Pairs[i].JsonString.Value;
      case PairIndex(APairName, cPairs) of
        0:FPhoto:=AValue.Values[APairName].Value;//'photo'
        1:FGroup:=AValue.Values[APairName].Value;//'group'
        2:ParseWorks(AValue.Values[APairName] as TJSONObject);//'works'
        3:ParseCategories(AValue.Values[APairName] as TJSONObject);//'categories'
        4:FPortfolio:=StrToInt(AValue.Values[APairName].Value);//'portfolio'
      end;
    end;
end;

Этот метод выглядит по-сложнее, чем ранее рассмотренные и, плюс ко всему, имеет вложенные методы. На самом деле всё тут не так уж и сложно. Вложенный метод ParseCategories разбирает объект с категориями пользователя, т.е. на входе он получает вот этот JSON-объект:
etxt_api7
При этом, проверяется условие

if SameText(AName,'type') then
  continue;

То есть, если имя очередной пары type, то она пропускается, т.к. не содержит информации о id категории.
Аналогичным образом работает и метод ParseWorks. Только здесь используются вот эти объекты:
etxt_api8
При этом в цикле мы получаем очередной объект работы:

AWork:=AWorksObject.Values[AName] as TJSONObject;

и вытаскиваем из этого объекта, только значение пары work:

AWorlID:=StrToInt(AWork.Values['work'].Value);

Так как мы не знаем будет ли в сервис добавляться новые виды работ для исполнителей, то дополнительно проверяем значение AWorkId и, если это значение укладывается в интервал от 1 до 4 (известные значения), то записываем название работы, если нет — записываем в список строку «Неизвестная работа»:

if (AWorlID>0)and(AWorlID<5) then
  FWorks.Add(cWorks[AWorlID])
else
  FWorks.Add('Неизвестная работа')

С парсингом объекта разобрались. Теперь добавляем очередной метод в класс TEtxtClient.

TEtxtClient = class(TEtxtAPI)
private
  [...]
  FUserInfo: TUserInfo;
  [...]
  function GetUserInfo2(AUser: TUser): TUserInfo;overload;
  function GetUserInfo(ALogin: string):TUserInfo;overload;
public
  [...]
  property UserInfo2[AUser:TUser]:TUserInfo read GetUserInfo2;
  property UserInfo[ALogin: string]:TUserInfo read GetUserInfo;
end;

Здесь для примера я создал два метода получения подробной информации о пользователе и для работы использую свойства:

property UserInfo2[AUser:TUser]:TUserInfo read GetUserInfo2;
property UserInfo[ALogin: string]:TUserInfo read GetUserInfo;

Посмотрим как работают методы:

function TEtxtClient.GetUserInfo(ALogin: string): TUserInfo;
var AJson: TJSONObject;
    AParams: TStringList;
    AResponse: string;
begin
  AParams:=TStringList.Create;
  try
    AParams.Values['method']:='users.getUser';
    AParams.Values['login']:=ALogin;
    //выполнили запрос 
    AResponse:=GET(AParams);
    //проверили ответ
    if AResponse.IsEmpty then
      Exit;
    //получили JSON-объект
    AJson:=GetJson(AResponse);
    //проверили на ошибку
    if not Assigned(AJson) then
      raise Exception.Create(FLastError);
    try
    //объект содержит поля
    if AJson.Count>0 then
      begin
        if not Assigned(FUserInfo) then
          FUserInfo:=TUserInfo.Create;
        //парсим ответ. В возвращаемом объекте есть информация только по одному пользователю - цикл не требуется
        FUserInfo.Parse(AJson.Values[AJson.Pairs[0].JsonString.Value] as TJSONObject);
        Result:=FUserInfo;
      end
    else
      Result:=nil;
    finally
      AJson.Free;
    end; 
  finally
    AParams.Free;
  end;
end;
 
function TEtxtClient.GetUserInfo2(AUser: TUser): TUserInfo;
begin
  Result:=GetUserInfo(AUser.Login);
end;

Как видите, второй метод самый простой и состоит всего из одной строки в которой мы получаем результат GetUserInfo.

Теперь у нас есть все необходимые методы, чтобы написать полноценное приложение. Об этом мы поговорим в третьей части. В третьей же части будут выложены и все исходники Delphi.

Книжная полка

Описание Подробно рассматривается библиотека FM, позволяющая создавать полнофункциональное программное обеспечение для операционных систем Windows и OS X, а также для смартфонов и планшетных компьютеров, работающих под управлением Android и iOS
купить книгу delphi на ЛитРес
Описание: Рассмотрены практические вопросы по разработке клиент-серверных приложений в среде Delphi 7 и Delphi 2005 с использованием СУБД MS SQL Server 2000, InterBase и Firebird. Приведена информация о теории построения реляционных баз данных и языке SQL. Освещены вопросы эксплуатации и администрирования СУБД.
купить книгу delphi на ЛитРес
Описание: Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP и др.
купить книгу delphi на ЛитРес
5 1 голос
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
0 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии