Sergey Dolganov
Evil Martians developer, open-source enthusiast, traveller and drummer.
Dolganov Sergey @ Evil Martians
Приложение, полностью построенное на API зависимостях
2. Контрактный подход
3. Каноничное решение для API
4. А мы как его применили?
5. А если сравнить?
6. Ссылки / рекомендации
1. Проблема? Задача!
Input
Output
function (input) { ... } # =>
State
Pre-conditions
Post-conditions
Invariants
Contract
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.dhl.com"
xmlns:dct="http://www.dhl.com/DCTRequestdatatypes"
xmlns:dhl="..." targetNamespace="http://www.dhl.com"
elementFormDefault="unqualified">
<xsd:import namespace="http://www.dhl.com/datatypes" schemaLocation="datatypes.xsd" />
<xsd:import namespace="http://www.dhl.com/DCTRequestdatatypes" schemaLocation="... />
<xsd:element name="DCTRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="GetQuote">
<xsd:annotation>
<xsd:documentation>Root element of Quote request</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Request" type="dhl:Request" />
<xsd:element name="From" type="dct:DCTFrom" minOccurs="1" />
<xsd:element name="BkgDetails" minOccurs="1" type="dct:BkgDetailsType" />
<xsd:element name="To" minOccurs="1" type="dct:DCTTo" />
<xsd:element name="Dutiable" minOccurs="0" type="dct:DCTDutiable" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<!-- .... -->
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<xsd:import namespace="http://www.dhl.com/datatypes"
schemaLocation="datatypes.xsd" />
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.dhl.com"
xmlns:dct="http://www.dhl.com/DCTRequestdatatypes"
xmlns:dhl="..." targetNamespace="http://www.dhl.com"
elementFormDefault="unqualified">
<xsd:import namespace="http://www.dhl.com/datatypes" schemaLocation="datatypes.xsd" />
<xsd:import namespace="http://www.dhl.com/DCTRequestdatatypes" schemaLocation="... />
<xsd:element name="DCTRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="GetQuote">
<xsd:annotation>
<xsd:documentation>Root element of Quote request</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Request" type="dhl:Request" />
<xsd:element name="From" type="dct:DCTFrom" minOccurs="1" />
<xsd:element name="BkgDetails" minOccurs="1" type="dct:BkgDetailsType" />
<xsd:element name="To" minOccurs="1" type="dct:DCTTo" />
<xsd:element name="Dutiable" minOccurs="0" type="dct:DCTDutiable" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<!-- .... -->
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<xsd:element name="From" type="dct:DCTFrom" minOccurs="1" />
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://www.dhl.com" xmlns:dct="http://www.dhl.com/DCTResponsedatatypes"
xmlns:dhl="http://www.dhl.com/datatypes" targetNamespace="http://www.dhl.com"
elementFormDefault="unqualified">
<xsd:import namespace="http://www.dhl.com/datatypes" schemaLocation="config/xmlshipping/xsd/datatypes.xsd" />
<xsd:import namespace="http://www.dhl.com/DCTResponsedatatypes" schemaLocation="config/xmlshipping/xsd/DCTResponsedatatypes.xsd" />
<xsd:element name="DCTResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="GetQuoteResponse">
<xsd:annotation>
<xsd:documentation>Root element of shipment validation request</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Response">
<xsd:complexType>
<xsd:annotation>
<xsd:documentation>Generic response header</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="ServiceHeader" type="ServiceHeader" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="BkgDetails" minOccurs="0" type="dct:BkgDetailsType" maxOccurs="unbounded" />
<xsd:element name="Srvs" minOccurs="0" maxOccurs="1">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Srv" type="dct:SrvType" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="Note" minOccurs="0" type="dct:NoteType" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetCapabilityResponse">
<xsd:annotation>
<xsd:documentation>Root element of shipment validation request</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Response">
<xsd:complexType>
<xsd:annotation>
<xsd:documentation>Generic response header</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="ServiceHeader" type="ServiceHeader" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="BkgDetails" minOccurs="0" type="dct:BkgDetailsType" maxOccurs="unbounded" />
<xsd:element name="Srvs" minOccurs="0" maxOccurs="1">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Srv" type="dct:SrvType" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="Note" minOccurs="0" type="dct:NoteType" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="ServiceHeader">
<xsd:annotation>
<xsd:documentation>Standard routing header</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="MessageTime" type="xsd:dateTime" minOccurs="0">
<xsd:annotation>
<xsd:documentation>Time this message is sent</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="MessageReference" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>A string, peferably number, to uniquely identify individual messages. Minimum length must be
28 and maximum length is 32</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="SiteID" type="xsd:string" />
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
<xsd:element name="BkgDetails"
minOccurs="0"
type="dct:BkgDetailsType"
maxOccurs="unbounded" />
def validate(document, schema_path, root_element)
schema = Nokogiri::XML::Schema(File.read(schema_path))
schema.validate(document.xpath("//#{root_element}").to_s)
end
def get_tariff(data)
xml_request = convert_to_xml(data)
errors = validate(xml_request,
"./get_tariff_schemas/request.xsd", "container")
raise InvalidRequest, errors unless errors.empty?
xml_response = get("GetQuote", xml_request)
errors = validate(xml_response,
"./get_tariff_schemas/response.xsd", "container")
raise InvalidResponse, errors unless errors.empty?
xml_response
end
module RussianPost::Domestic::Calculator
attr_reader :errors
def initialize(parcel)
@parcel = parcel
end
def call
policy = InternalParcelPolicy[parcel]
return if policy.invalid?
policy = RuPostDomesticTariffPolicy[parcel]
return if policy.invalid?
response = RussianPost::API.get_tariff(parcel: parcel)
mapped_response = GetTariffResponseMapper[response]
policy = RuPostTariffResponsePolicy[response, mapped_response]
return if policy.invalid?
parcel.update_tariff!(russian_post: mapped_response)
ensure
@errors = policy.errors
end
end
if (service = RussianPost::Calculator.new(parcel)).call
render json: { tariff: parcel.reload.tariffs[:russian_post] }
else
render json: { errors: { base: service.errors } }
end
Нужно сократить затраты на отладку...
В ФУНКЦИОНАЛЬНЫХ ЯЗЫКАХ СУЩЕСТВУЕТ ЭЛЕГАНТНОЕ РЕШЕНИЕ ДЛЯ ПАРСИНГА И ВАЛИДАЦИИ
МОЖЕТ И В RUBY СРАБОТАЕТ?
data Parcel = Parcel { weight :: ParcelWeight
, value :: ParcelValue
, itemsCount :: ItemsCount
, origin :: OriginAddress
, destination :: DestinationAddress }
requestTariff :: Parcel -> (IO XML -> IO XML) -> Either ContractFailure Response
OK (Response)
FAIL
(ContractFailure)
let realParcel = Parcel { weight = 200, destination = "US, Brooklyn, 11201", ...}
case requestTariff realParcel (get' "GetQuote") of
Right KnownAPIError -> -- show error to user (to correct input for example)
Right Tariff -> -- do something when got valid tariff
Right TariffWithWarnings -> -- use Tariff but output warnings
Left (ContractFailure context) -> -- send to Honeybadger with whole context
{ n :: Integer | n > 5 }
Контекст, как мы поняли, что внутри не котенок
# types product
class ContactType < BaseType
attribute :email, Contact::EmailType
attribute :name, Contact::NameType
attribute :phone, Contact::PhoneType
end
# will just delegate validation to attributes
ContactType.match(data) # => #<ContactType ...>
# or #<ContractFailure ...>
class EmailType < BaseType
# in more sophisticated cases – delegate
def match
@unpacked = ::Mail::Address.new(@value)
self
rescue Mail::Field::IncompleteParseError => ex
ContractFailure.new(:email_parser_failed, ex)
end
end
# same but with composition (types sum) for response
AddContactResult = ContactAdded |
ContactUpdated |
ErrorsType
ErrorsType = DuplicateError |
RateLimitError |
ParseError
class CRM::API::Client
def add_contact(payload)
result = ContactType.match(payload)
return result if result.invalid?
response = post("/Contacts", result.unpack)
result = AddContactResult.match(response)
ensure
ApiSampler.register!(result)
end
end
result = CRM::API::Client.new.add_contact(payload)
case result
when RateLimitError
CRM::API::Client.cooldown(payload.token)
return "Please, try again"
when ContractFailure
Honeybadger.notify(result, context: {...})
return "Sorry, API is not working correctly"
when ContactAdded
return "Successfully added contact"
when ContactUpdated
return "Contact was updated"
else
raise "Unmatched behavior", result
end
def add_contact(data)
json_request = convert_to_json(data)
errors = validate(json_request,
"./add_contact_schemas/request.json")
raise InvalidRequest, errors unless errors.empty?
json_response = post("/Contacts", json_request)
errors = validate(json_response,
"./add_contact_schemas/response.json")
raise InvalidResponse, errors unless errors.empty?
json_response
end
Схемы | Типы | |
---|---|---|
Сложность имплементации | ||
Скорость работы | ||
Переиспользуемость кода | ||
Возможности отладки в production |
By Sergey Dolganov
Как применять контрактный подход в современном приложении? Конечно, для работы с API и валидации запросов и ответов. Однако, можно пойти дальше и использовать не только схемы запросов, но и Типы на уровне языка. А если применить еще пару приемов позаимствованных в функциональных языках, можно сделать работу вашего приложения не только надежной, но и прозрачной для отладки.
Evil Martians developer, open-source enthusiast, traveller and drummer.