Каков наилучший способ сопоставления шаблонов URL-адресов REST с объектами модели для структуры Siesta?

Я хотел бы использовать ResponseTransformer (или их ряд), чтобы автоматически сопоставлять классы объектной модели с ответами, возвращаемыми из службы Siesta, так что мои ресурсы Siesta являются экземплярами моих классов моделей. У меня есть рабочая реализация для одного класса, но я хотел бы знать, есть ли более безопасный, умный или более эффективный способ сделать это, прежде чем я создам отдельный ResponseTransformer для каждого типа ресурсов (модели).

Вот пример модельного класса:

import SwiftyJSON

class Challenge {
    var id:String?
    var name:String?

    init(fromDictionary:JSON) {
        if let challengeId = fromDictionary["id"].int {
            self.id = String(challengeId)
        }
        self.name = fromDictionary["name"].string
    }
}

extension Challenge {

    class func parseChallengeList(fromJSON:JSON) -> [Challenge] {
        var list = [Challenge]()

        switch fromJSON.type {
        case .Array:
            for itemDictionary in fromJSON.array! {
                let item = Challenge(fromDictionary: itemDictionary)
                list.append(item)
            }
        case .Dictionary:
            list.append(Challenge(fromDictionary: fromJSON))
        default: break
        }

        return list
    }
}

И вот обработчик ResponseTransformer, который я построил для сопоставления ответа от любой конечной точки, которая возвращает либо коллекцию этого типа модели, либо один экземпляр этого типа модели:

public func ChallengeListTransformer(transformErrors: Bool = true) -> ResponseTransformer {
    return ResponseContentTransformer(transformErrors: transformErrors)
        {
            (content: NSJSONConvertible, entity: Entity) throws -> [Challenge] in        
            let itemJSON = JSON(content)        
            return Challenge.parseChallengeList(itemJSON)
    }
}

И, наконец, вот картирование URL-шаблона, которое я выполняю при настройке службы Siesta:

class _GFSFAPI: Service {

    ...

    configure("/Challenge/*")    { $0.config.responseTransformers.add(ChallengeListTransformer()) }
}

Я планирую создать отдельный ResponseTransformer для каждого типа модели, а затем индивидуально сопоставить каждый шаблон URL с этим трансформатором. Это лучший подход? Кстати, я очень взволнована новой картой Siesta. Мне нравится идея ресурсно-ориентированной сетевой библиотеки REST.

Ответ 1

Ваш подход прочен! Вы получили это. Есть несколько вещей, которые вы можете сделать, чтобы упростить трансформаторы.

Большое изображение

Похоже, вы уже понимаете этот компромисс, но для других, которые находят этот ответ... у вас есть два общих подхода на выбор:

  • создайте свой объект модели в вашем наблюдателе Siesta или
  • создайте объект модели в трансформаторе.

Вариант 1 проще настроить - просто сделайте модель на месте, и вы сделали!

func resourceChanged(resource: Resource, event: ResourceEvent) {
    let challenges = Challenge.parseChallengeList(
        JSON(resource.latestData?.jsonDict))
    ...
}

Это хорошо работает для многих проектов. У этого есть недостатки, однако:

  • Вариант 1 создает экземпляр нового объекта модели для каждого события, умноженного на каждого наблюдателя; опция 2 только создает экземпляр объекта модели за ответ "новые данные".
  • В параметре 1 нет центрального места, которое отслеживает, какие маршруты сопоставляются с объектами модели.
  • Вариант 2 дает лучшие ошибки, если сервер не возвращает тип содержимого, который вы ожидаете.

Я предпочитаю вариант 1, если (и только если) проект мал, а модели легкие.

Подробную документацию по опции 2 вы найдете в разделе Pipeline в руководстве пользователя. Вот краткий обзор.

Используя configureTransformer

Вы можете упростить свой ChallengeListTransformer, используя configureTransformer(...):

configureTransformer("/Challenge/*") {
    (content: NSJSONConvertible, entity: Entity) throws -> [Challenge] in        
    let itemJSON = JSON(content)        
    return Challenge.parseChallengeList(itemJSON)
}

Но подождите, theres больше! Смотрите как Swifts замечательные кусочки вывода и кубики для вас:

configureTransformer("/Challenge/*") {
    Challenge.parseChallengeList(
        JSON($0.content as NSJSONConvertible))
}

(Обратите внимание, что configureTransformer устанавливает transformErrors в false. Это почти наверняка то, что вы хотите... если ваш сервер не отправит JSON "пробную" модель в качестве тела ответа об ошибке! Параметр transformErrors, как правило, только для трансформаторов общего назначения, таких как текст и разбор JSON, которые связаны с типом контента, а не с теми, которые привязаны к маршруту.)

Трансформатор Global SwiftyJSON

Если вы используете SwiftyJSON (который мне тоже нравится, BTW), тогда вы можете применить его во всех ответах JSON:

private let SwiftyJSONTransformer =
    ResponseContentTransformer(skipWhenEntityMatchesOutputType: false)
        { JSON($0.content as AnyObject) }

... и затем:

service.configure {
    $0.config.responseTransformers.add(
        SwiftyJSONTransformer, contentTypes: ["*/json"])
}

... что еще больше упрощает трансформацию содержимого каждого маршрута:

configureTransformer("/Challenge/*") {
    Challenge.parseChallengeList($0.content)
}

Обратите внимание, что вывод типа Swifts указывает Siesta, что этот трансформатор ожидает ввода JSON в качестве входных данных, а Siesta использует это, чтобы помечать его как ошибку, если он не вышел из конвейера трансформатора таким образом. Трансформаторы, связанные с JSON, привязаны к типам контента */json, поэтому, если сервер возвращает что-либо неожиданное, ваши наблюдатели видят хороший порядок "Эй, это не JSON!". ошибка.

См. руководство пользователя для получения более подробной информации обо всем этом.

Получение модели из ресурса

Поскольку Siesta API в настоящее время стоит, вам нужно понизить содержание моделей:

func resourceChanged(resource: Resource, event: ResourceEvent) {
    let challenges = resource.latestData?.content as? [Challenge]
    ...
}

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

func resourceChanged(resource: Resource, event: ResourceEvent) {
    let challenges = resource.typedContent(ifNone: [Challenge]())
    ...
}

Siesta в настоящее время не предоставляет статически типизированный способ привязки типа модели к ресурсу; вам нужно сделать бросок. Это связано с тем, что ограничения системы типа Swifts не позволяют на практике использовать обобщенный тип ресурса (например, Resource<[Challenge]>). Надеюсь, что Swift 3 решает эти проблемы, поэтому некоторые будущие версии Siesta могут это предоставить. Обновление: Необходимые улучшения дженериков для Swift 3, поэтому, надеюсь, в Swift 4....