Это восемнадцатая статья в серии статей, в которой описаны все этапы создания веб-приложения. Если вы честолюбивый разработчик, если возиться с детскими учебниками для начинающих вас расстраивает, если вы хотели бы создать достаточно серьезное приложение, которое делает потрясающие вещи, эти статьи для вас.

Слово мудрому: эти руководства не описывают отточенный, первозданный рабочий процесс. Вы не найдете «следуй направлениям A, B, C», ведущим вас по идеально прямой линии. Неа. Эти уроки изображают беспорядок, хаос и песок. Они меандрируют. Они показывают, что такое настоящая веб-разработка. Стандартная черта профессионалов — сделать так, чтобы ваша сделка выглядела легкой. Не здесь, детка. Если я правильно написал эти статьи, то начинающие и средние разработчики прочтут их и подумают: «О, слава богу, оказывается, эти самопровозглашенные старшие разработчики борются так же, как и я! Мой синдром самозванца — всего лишь синдром!»

В прошлый раз мы решили, что чертовски хороший способ сериализации нашей модели Rails/базы данных — это гем jsonapi-resources. Сегодня продолжим наше путешествие.

Почтальон

Давайте познакомим вас с другой вещью. "Почтальон".

Postman — это инструмент тестирования. Он запускает HTTP-запросы по любому URL-адресу, который вы хотите назвать. Вы можете установить заголовки запроса и получить заголовки ответа. Почтальон классный. Почтальон милый. Проверь это:

Вы запускаете HTTP-запросы по выбранному вами URL-адресу. Вы можете изменить все до последней мелочи: параметры запроса, параметры авторизации, значения заголовков, все, что вам нравится. Затем Postman получает ответ и по очереди отображает все до последней мелочи.

Здесь вы можете видеть, что я только что выстрелил из GET http://localhost:3000/api/v1/venues?filter[acts]=1,2. Вы также можете видеть на левой панели этого снимка экрана, что я отправил миллион других HTTP-запросов в /api/v1/venues, возился со значением filter[acts], пытаясь понять, как jsonapi-resources анализирует параметры запроса. Postman позволяет мне легко отправлять все виды значений: filter[acts]=1,2, filter[acts]=1+2, 1;2, 1–2, foo,bar и так далее.

Как видите, цель состоит в том, чтобы отправить массив положительных целых чисел, и jsonapi-resources интерпретирует 1,2 как массив, то есть ['1', '2'], но почти все остальное он интерпретирует как просто необработанную строку.

Я хотел узнать, почему, ооооо…

… Поэтому я пришел к выводу, что потратил слишком много часов на поиск точной строки внутреннего кода jsonapi-resources, ответственной за это. Но я нашел его наконец! Оказывается, он просто вызывает CSV.parse_line на входе, который, как вы уже догадались, преобразует список, разделенный запятыми, в массив. Потрясающий.

Хорошо, это позаботится о некоторых нашей проверке. Но в итоге: когда мы отправляем значения в filter[acts], которые не являются положительными целыми числами, как мы хотим, чтобы наш API реагировал? С теми же конкретными сообщениями об ошибках, что и раньше? Или просто игнорировать и отбрасывать неположительные целые числа?

Знаешь что? Учитывая, что единственным клиентом этой конечной точки API на данный момент будет наше клиентское приложение пользовательского интерфейса… Честно говоря, у меня просто возникло искушение выбросить все эти проверки JsonSchema, которые мы написали для нашего SearchController. Это перебор. Просто используйте to_i для преобразования всех строковых цифр в фактические целые числа: первые запрашиваются; все остальное отбрасывается. Сильно упрощает дело. Очень просто.

Что же тогда мы можем протестировать? Я думаю примерно так:

# spec/requests/api/v1/venues_spec.rb
describe "GET index" do
  before {
    get api_v1_venues_path, params: params
  }
  describe "sending no act IDs at all" do
    it "returns the most recent 20"
    it "conforms to the json-api standard" 
  end
  describe "sending IDs 2,4,5" do
    it "returns acts 1,2,10,11"
  end
end

Давайте взломать.

Неправильные документы хуже, чем отсутствие документов

О, да. Jsonapi-resources испытывает мое терпение.

Вот почему. Я надеялся, что мы сможем просто взять себя в руки и провести несколько прекрасных полезных тестов. Что-то, что вы могли бы принести домой своей старой маме, не опасаясь смущения. Я надеялся написать что-то вроде этого:

it "conforms to the json-api standard" do
  expect(response_json['data'][...]).to eq "[json string]"
end

Как и раньше, мы будем сравнивать json-ответ API с теми jsonapi-ресурсами, которые должны сериализовать наши модели. "[json string]" выше должен быть вызовом его сериализатора.

Чтобы сериализовать модель ActiveRecord, как она утверждает, вы делаете следующее:

post = Post.find(1)
serialized_post = JSONAPI::ResourceSeralizer.new(PostResource)
  .serialize_to_hash(PostResource.new(post, nil))

Э, хорошо. Немного многословно, но какого черта. Давайте повторим это. Мы прыгаем в консоль Rails.

irb(main):001:0> venue = Venue.find(1)
Venue Load (0.8ms) SELECT "venues".* FROM "venues" WHERE "venues"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> #<Venue id: 1, name: "Payne Arena", ticketmaster_id: "KovZpZAJJtIA">
irb(main):002:0> serialized_venue = JSONAPI::ResourceSerializer.new(Api::V1::VenueResource).serialize_to_hash(Api::V1::VenueResource.new(venue, nil))
Traceback (most recent call last):
        1: from (irb):2
NoMethodError (undefined method `serialize_to_hash' for #<JSONAPI::ResourceSerializer:0x00007fb155bc2448>
Did you mean?  serialize_to_relationship_hash)

undefined method `serialize_to_hash'? Что?

Нет, серьезно. Длительное ручное тестирование действительно показало, что #serialize_to_hash не существует в ресурсах jsonapi.

О, ура, еще одна ошибка.

**гуглить**

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

Как нам решить это?

На ум приходят два варианта: (1) заменить точную версию jsonapi-resources, которую мы используем, на другую, которая действительно ее содержит; или (2) просто полностью отказаться от ресурсов jsonapi.

Я не могу отрицать, что (2) имеет определенную привлекательность… но давайте пока не будем отвергать эту чертову библиотеку. Попробуем (1).

Как вручную установить версию гема

Во-первых, получите полный список всех доступных версий. Каждый общедоступный гем Ruby имеет собственную страницу rubygems.org. Это — страница версий jsonapi-resources, на которой перечислены все 82 версии, макро, микро и багфиксро.

Какой из них мы должны использовать? Я начал возиться со своей подписью на этой странице отчетов об ошибках Github и других, и оказалось, что версия Gem с работающей #serialize_to_hash — это 0.9.11.

В Gemfile мы прыгаем. Мы меняемся

# Gemfile
...
gem 'jsonapi-resources'
...

To

# Gemfile
...
gem 'jsonapi-resources', '0.9.11'
...

Затем обновите с помощью bundle update jsonapi-resources, и все готово!

Управление версиями Gem через Bundler — это совсем другое дело. Прочтите это для получения дополнительной информации.

Давайте повторим сериализацию.

irb(main):001:0> venue = Venue.find(1)
  Venue Load (1.2ms)  SELECT  "venues".* FROM "venues" WHERE "venues"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
=> #<Venue id: 1, name: "Payne Arena", ticketmaster_id: "KovZpZAJJtIA">
irb(main):002:0> JSONAPI::ResourceSerializer.new(Api::V1::VenueResource).serialize_to_hash(Api::V1::VenueResource.new(venue, nil))
=> {:data=>{"id"=>"1", "type"=>"venues", "links"=>{"self"=>"/api/v1/venues/1"}, "attributes"=>{"name"=>"Payne Arena"}, "relationships"=>{"gigs"=>{"links"=>{"self"=>"/api/v1/venues/1/relationships/gigs", "related"=>"/api/v1/venues/1/gigs"}}}}}

Успех! Действительная JSON-API-сериализация. Это будет хорошо! Давайте продолжим наше тестирование.

Между прочим, я должен отметить, что не исключено, что снижение нашей версии gem может вызвать проблемы в другом месте. Мы очень возимся с этим драгоценным камнем, и не исключено, что этот акт вращения тарелок, который мы здесь устраиваем, может принести больше проблем, чем пользы. Но время покажет. Вперед.

Франкен-кодирование

Тангентное время! Не волнуйтесь, этот намного короче.

Помните Франкен-кодирование? Впервые мы столкнулись с этим в статье 12. Это относится к кодерам, собирающим вместе огромное количество совершенно не связанных между собой библиотек. Они сражаются друг с другом. Они мешают друг другу. То, что я собираюсь вам показать, — это всего лишь очень незначительный пример, но я думаю, что его стоит упомянуть.

Помните тот тест, который мы написали для проверки значения заголовка HTTP-ответа Content-Type?

# spec/requests/search_spec.rb
...
describe "Querying API with zero params of any kind" do
  let(:params) { {} }
  it "returns the jsonapi Content-Type" do
    expect(response.headers['Content-Type'])
      .to eq 'application/vnd.api+json; charset=utf-8'
  end
end

Я бы не запускал этот тест с тех пор, как установил что-либо, связанное с ресурсами jsonapi. Ну **кхм**:

$ rspec spec/requests/search_spec.rb:38
Run options: include {:locations=>{"./spec/requests/search_spec.rb"=>[38]}}
F
Failures:
1) Search Querying API with zero params of any kind returns the jsonapi Content-Type
     Failure/Error: expect(response.headers['Content-Type']).to eq 'application/vnd.api+json; charset=utf-8'
       expected: "application/vnd.api+json; charset=utf-8"
            got: "application/vnd.api+json"
(compared using ==)
     # ./spec/requests/search_spec.rb:39:in `block (3 levels) in <top (required)>'
Finished in 0.7712 seconds (files took 3.95 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/requests/search_spec.rb:38 # Search Querying API with zero params of any kind returns the jsonapi Content-Type

Что случилось? Оказывается, jsonapi-resources путается с нашими заголовками HTTP-ответов. Но это нормально! Это должно быть. Помните, это полноценное дополнение к JSON-API-ify Rails? Весь его смысл в том, что вы добавляете его в свой Gemfile, и он будет обрабатывать все оттуда. Естественно, он будет автоматически делать некоторые вещи, которые мы уже настроили вручную, верно?

Но, как мы видим, это не совсем идентично. Этот бит charset=utf-8 исчез. Но да, это не конец света, честное слово. Важным битом был бит application/vnd.api+json. Этот файл инициализатора register_json_mime_types.rb больше не нужен; мы можем удалить его.

Давайте зафиксируем все это: установим нашу конкретную версию jsonapi-resources; потеря нашего файла register_json_mime_types.rb; изменение этого теста; добавление файлов ресурсов; прочая фигня. Commit и diff, мило.

Написание обобщенных тестов JSON-API

Ладно, где мы были? Мы писали тесты, управляющие общим поведением нашего API, вот что.

Итак, в RSpec есть кое-что. Общие примеры. RSpec — это RSpec, он просто должен реализовать свой собственный способ работы. Shared Examples — это способ стандартного вызова функций в духе RSpec:

shared_examples_for 'our little demo example' do |param1, param2|
  it 'does Thing One' do
    expect(param1).to eq 'bar'
  end
  it 'does Thing Two' do
    expect(param2).to eq 'baz'
  end
end
RSpec.describe 'one' do
  include_examples 'our little demo example', 'param1a', 'param2a'
end
RSpec.describe 'two' do
  include_examples 'our little demo example', 'param1b', 'param2b'
end

Это запускает четыре теста.

Я упоминаю об этом, потому что предпочел бы написать набор общих тестов, которые будут запускаться для каждого отдельного теста ответа API. Например, наш тест заголовка Content-Type. Важно, чтобы этот заголовок был установлен для каждого отдельного запроса и ответа. Имеет смысл проверить это, не так ли?

И еще несколько вещей: каждый ответ API будет возвращать какой-то объект jsonapi-schema. Это мы тоже можем проверить. И включайте эти общие примеры позже в каждый другой тест. Быстрее.

Больше шуток позже…

Я реорганизовал наш тестовый блок таким образом:

# spec/requests/search_spec.rb
...
shared_examples_for 'general jsonapi behaviour for' do |klass|
  
  it 'returns the jsonapi Content-Type' do
    expect(response.headers['Content-Type']).to eq 'application/vnd.api+json'
  end
  it 'returns an array of jsonapi objects' do
    expect(response_json['data']).to match_jsonapi_array_schema
  end
  it 'returns all with type=="klass name"' do
    expect(response_json['data']).to match_jsonapi_array_types_for klass
  end
end
...
describe 'Querying API with zero params of any kind' do
  let(:params) { {} }
  include_examples 'general jsonapi behaviour for', Venue
end

Здесь у вас могут возникнуть две вещи. Во-первых, что такое klass? Во-вторых, что это за match_*_*... совпадения?

klass первый. Легкий. Переменная, с которой мы здесь работаем, — это реальный класс Ruby. Класс ActiveRecord. Но в Ruby, как и в любом языке, есть зарезервированные ключевые слова, и class — одно из них. Таким образом, klass просто означает, что эта переменная является классом, но «класс непослушный, поэтому вместо этого мы используем класс.

Теперь эти совпадения. Я создал еще один вспомогательный файл спецификации:

# spec/helpers/jsonapi_helpers.rb
jsonapi_object_schema = {
  type: 'object',
  required: ['id', 'type', 'attributes', 'relationships'],
  properties: {
    id: { type: 'string' },
    type: { type: 'string' },
    attributes: { 
      type: 'object',
    },
    relationships: {
      type: 'object',
    },
    links: {
      type: 'object',
    },
    related: {
      type: 'object',
    },
  }
}
jsonapi_array_schema = {
  type: 'array',
  items: jsonapi_object_schema
}
RSpec::Matchers.define :match_jsonapi_array_types_for do |klass|
  match do |candidate_array|
    candidate_array.all? {|item| item['type'] == klass.model_name.plural }
  end
end
RSpec::Matchers.define :match_jsonapi_array_schema do
  match do |candidate_jsonapi_array|
    JSON::Validator.fully_validate(jsonapi_array_schema, candidate_jsonapi_array).length == 0
  end
end

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

Единственное понятие, с которым мы еще не сталкивались, это RSpec::Matchers.define. Это пользовательский сопоставитель RSpec. Все, что он делает, — запускает валидатор JSON и проверяет соответствие тестового массива стандарту JSON API. Или, по крайней мере, ту малюсенькую часть, которую определяет jsonapi_object_schema. Он охватывает лишь несколько основ, и я, вероятно, добавлю их по мере необходимости.

Хорошо! Давайте проверим эти тесты.

$ rspec spec/requests/search_spec.rb:48
Run options: include {:locations=>{"./spec/requests/search_spec.rb"=>[48]}}
...
Finished in 1.14 seconds (files took 3.02 seconds to load)
3 examples, 0 failures

Красивый. Commit и diff.

Хорошо, что дальше? Завершение рефакторинга наших тестов Act ID, вот что. Вперед!

Лаконичный тест

Вы когда-нибудь слышали о чем-то под названием Лаконичная фраза? Короче говоря, это искусство говорить много в нескольких словах. Прочтите эту ссылку, чтобы узнать больше.

Давайте примем эту философию. Вы помните, что наш первый тест на основе Act-ID возвращал все места проведения:

# spec/requests/search_spec.rb
...
context 'Not supplying any Act IDs' do
let(:params) {}
  
  describe 'Returning every single Venue' do
    
    subject { response_json }
    
    it { is_expected.to include(
      *[venue1, venue2, venue3, venue4, venue5].map { |venue|
        venue.as_json(include: :gigs)
      }
    })
  end
end

Я стал недоволен таким тестом. Это небрежно! Вы помните, что еще в Статье 15, когда мы начали с добавления временных меток к нашим гигам (не забыли об этом, кстати, немалая часть его тестового кода находится на моей машине прямо сейчас, когда я пишу это, просто еще не зафиксировал это), мы остановились на полпути, когда я обнаружил, что мой API возвращает 13 площадок вместо 5.

Этот тест прямо здесь не поднимает это. Это небрежно. Вместо этого я бы предпочел что-то вроде этого:

# spec/requests/search_spec.rb
...
let!(:venue1) { create :venue, updated_at: Time.now - 9.days }
let!(:venue2) { create :venue, updated_at: Time.now - 3.days }
let!(:venue3) { create :venue, updated_at: Time.now - 7.days }
let!(:venue4) { create :venue, updated_at: Time.now - 2.days }
let!(:venue5) { create :venue, updated_at: Time.now - 8.days }
...
context 'Not supplying any act IDs' do
  let(:params) {
    { filter: { acts: '' } }
  }
  it 'returns every venue, ordered by updated_at desc' do
    expect(response_json['data']).to eq(
      [venue4, venue2, venue3, venue5, venue1].map { |v|
        JSONAPI::ResourceSerializer.new(Api::V1::VenueResource)
          .serialize_to_hash(Api::V1::VenueResource.new(v, nil))
      }
    )
  end
end

Это все еще потребует тонны полировки: #serialize_to_hash принимает все виды опций и параметров и неудобных битов, чтобы помочь вам настроить атрибуты, отношения, ссылки и т. д.

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

Все в одном тесте. Лаконично! Давайте взломать.

t.timestamps

Я написал и запустил этот новый тест. Это произошло:

$ bundle exec rspec spec/requests/search_spec.rb:64
Run options: include {:locations=>{"./spec/requests/search_spec.rb"=>[64]}}
F
Failures:
1) Search Filtering by Act Not supplying any act IDs returns every single venue
  Failure/Error: let!(:venue1) { create :venue, updated_at: Time.now - 9.days }
NoMethodError:
    undefined method `updated_at=' for #<Venue:0x00007fbcc1248cd8>
      Did you mean?  update
    # ./spec/requests/search_spec.rb:28:in `block (2 levels) in <top (required)>'
Finished in 0.6644 seconds (files took 3.84 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/requests/search_spec.rb:64 # Search Filtering by Act Not supplying any act IDs returns every single venue

Ад?

Я пытаюсь установить атрибут #updated_at каждой площадки, но это не позволяет мне! Я сделал что-то не так? ActiveRecord всегда был настроен таким образом, что если вы создаете столбец таблицы базы данных, вы можете автоматически читать и записывать атрибуты этой табличной модели, без дополнительной настройки. Изменил ли Rails это в своих последних версиях ActiveRecord?

Несколько минут безделья открыли ответ. Я просто забыл добавить временные метки по умолчанию в свои модели.

Помните временные метки? Я приступил к общему определению меток времени еще в статье 15, когда опрометчиво предположил, что мы сразу же начнем с добавления меток времени начала и окончания в наш поисковый API. Увы.

Если вы еще не использовали миграции ActiveRecord для создания столбцов ваших таблиц (что мы впервые сделали еще в Статье 8), идея заключается в том, что миграции содержат свой собственный встроенный набор функций для построения столбцов, установки их значений. имена, набор типов данных, значения по умолчанию, null/non-null, все виды вещей.

Одной из таких вещей являются временные метки. Есть два столбца, которые вы можете добавить в любую таблицу базы данных на основе ActiveRecord: created_at и updated_at. Они оба на свидании. Первый заполняется любым моментом времени, когда эта строка таблицы была впервые создана, и никогда не изменится в будущем (ничто не мешает вам изменить ее вручную, заметьте, но это безумие и спагетти). Второй обновляется прямо сейчас каждый раз, когда вы обновляете любой из других атрибутов: он записывает, когда эта модель была обновлена ​​в последний раз.

Но вы должны добавить их самостоятельно. Вот снова мой файл миграции:

# db/migrate/20190910020025_create_acts_gigs_venues_tables.rb
class CreateActsGigsVenuesTables < ActiveRecord::Migration[5.2]
  def change
    create_table :acts do |t|
      t.string :name
    end
    ...
  end
end

Должно было быть так:

# db/migrate/20190910020025_create_acts_gigs_venues_tables.rb
class CreateActsGigsVenuesTables < ActiveRecord::Migration[5.2]
  def change
    create_table :acts do |t|
      t.string :name
      t.timestamps
    end
    ...
  end
end

t.timestamps обрабатывает создание created_at/updated_at. Или сделал бы, если бы я обладал поразительным и ужасающим мастерством кодирования, о котором я заявлял в этих последних 18 статьях.

Время миграции! Пришло время создать новую миграцию, чтобы наконец добавить их.

Совершенно новая миграция, говорите? Можем ли мы просто изменить существующий и запустить его повторно? Да, мы могли бы: нас ничего не останавливает… но это почти всегда плохая идея.

Почему? Представьте, что вы являетесь частью команды программистов. Вы все работаете над одной и той же кодовой базой, возитесь с фичами, возитесь с ошибками, возитесь без какой-либо конкретной цели или задачи. Вы будете периодически писать ветку Git, отправлять ее на Github, отправлять что-то, называемое Pull Request (еще одна вещь, прочитайте эту ссылку для получения дополнительной информации), объединять свою ветку в master … затем извлекать ее последнее содержимое обратно в вашу локальную копию master. И так будет со всеми остальными. Вы будете вносить изменения в базу данных. Вы будете писать миграции. Новый файл миграции? Милая, беги. Это все. Все это понимают.

Но если вы измените существующую миграцию? Вы должны сообщить об этом остальным. Вручную. И не только твои товарищи по команде. Любой, кто когда-либо копировал вашу кодовую базу во всей вселенной, вплоть до вашей модификации миграции, должен быть выследен и обучен. Фу.

Но содержать точно такие же изменения в новом файле миграции? Очень просто. Напишем один.

Это будет хорошо:

# db/migrate/20200504095206_add_timestamps.rb
class AddTimestamps < ActiveRecord::Migration[5.2]
  def up
    %w(gigs acts venues).each do |model|
      %w(created_at updated_at).each do |column|
        add_column model, column, :datetime,
          null: false, 
          default: Time.now
      end
    end
  end
  def down
    %w(gigs acts venues).each do |model|
      %w(created_at updated_at).each do |column|
        remove_column model, column
      end
    end
  end
end

Две вещи могут выскочить на вас. Во-первых, что это за методы up/down? Разве миграции не используют change? Во-вторых, почему существует default: Time.now?

Краткие ответы: во-первых, каждая миграция должна быть обратимой. Большинство отдельных действий по миграции ActiveRecord имеют очевидное обратное действие, поэтому вы можете просто написать метод change, и ActiveRecord определит это автоматически. Но если вы делаете что-то действительно сложное, вместо этого вы можете использовать методы up/down для более точного ручного управления. Это то, что я делаю здесь.

Во-вторых, эти столбцы меток времени никогда не могут быть пустыми. По задумке временные метки модели ActiveRecord имеют ограничение NOT NULL (само ограничение относится к базе данных/SQL, заметьте, а не к Rails, мы просто настраиваем нашу базу данных изнутри Rails, поэтому мы пишем ограничение SQL NOT NULL как Rails). -y null: false ограничение).

Но! Во всех трех таблицах уже существуют сотни строк. Создайте эти метки времени, и мы создадим еще сотни пустых ячеек. Вот что означает «null»: это не значение, это отсутствие значения. NOT NULL просто прекрасен, когда мы создаем новую таблицу, потому что ActiveRecord автоматически заполнит временные метки новых строк. Но если мы добавим их в существующие таблицы с кучей строк, то нам придется заполнить и эти существующие строки. В противном случае ограничение нарушено. Поэтому мы обходим это, предоставляя значение по умолчанию. Честно говоря, не имеет большого значения, какое это значение… так что давайте просто возьмем Time.now.

Любые другие неудобные биты, требующие перенастройки для всего приложения, для этих временных меток? Конечно. Атрибуты ресурса: я не вижу причин, почему бы не вернуть created_at и updated_at с ответом нашего API.

Ах да, и атрибуты FactoryBot. Те тоже. Давайте зафиксируем и diff и продолжим писать наши спецификации Act ID.

И это, я думаю, фантастическое усилие на сегодняшний день. В следующий раз мы возобновим рефакторинг теста Act ID.