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 |