From d9cff6e8b4649028a0217d1f00523e4af62de85d Mon Sep 17 00:00:00 2001 From: Piotr Sarnacki Date: Wed, 26 Aug 2015 13:28:23 +0200 Subject: [PATCH] Create adapters and serializers working with v3 and v2 APIs This commit adds adapters and serializers for v3, but also a fallback serializer for v2, which allows to handle v2 and v3 payloads at the same time. This is needed, because when we use v3 endpoint for one of the models (in this case repo), we can also get embedded records of other types (like branch or build). --- app/adapters/repo.js | 24 +++++ app/adapters/v3.js | 60 +++++++++++ app/models/build.coffee | 2 +- app/models/repo.coffee | 46 +++----- app/routes/repo.coffee | 6 +- app/routes/repo/index.coffee | 2 +- app/serializers/application.coffee | 6 +- app/serializers/build.coffee | 16 +-- app/serializers/repo.coffee | 5 +- app/serializers/v2_fallback.js | 41 ++++++++ app/serializers/v3.js | 105 +++++++++++++++++++ app/templates/components/repos-list-item.hbs | 33 +++--- 12 files changed, 279 insertions(+), 67 deletions(-) create mode 100644 app/adapters/repo.js create mode 100644 app/adapters/v3.js create mode 100644 app/serializers/v2_fallback.js create mode 100644 app/serializers/v3.js diff --git a/app/adapters/repo.js b/app/adapters/repo.js new file mode 100644 index 00000000..560506ee --- /dev/null +++ b/app/adapters/repo.js @@ -0,0 +1,24 @@ +import V3Adapter from 'travis/adapters/v3'; + +export default V3Adapter.extend({ + buildUrl(modelName, id, snapshot, requestType, query) { + var url = this._super(...arguments); + + return url; + }, + + ajaxOptions(url, type, options) { + var hash = options || {}; + if(!hash.data) { + hash.data = {}; + } + + if(hash.data.include) { + hash.data.include += ',repository.last_build,build.commit'; + } else { + hash.data.include = 'repository.last_build,build.commit'; + } + + return this._super(url, type, hash); + } +}); diff --git a/app/adapters/v3.js b/app/adapters/v3.js new file mode 100644 index 00000000..fd18b613 --- /dev/null +++ b/app/adapters/v3.js @@ -0,0 +1,60 @@ +import Ember from 'ember'; +import DS from 'ember-data'; +import config from 'travis/config/environment'; + +export default DS.RESTAdapter.extend({ + auth: Ember.inject.service(), + host: config.apiEndpoint, + + defaultSerializer: '-repo', + + sortQueryParams: false, + coalesceFindRequests: false, + headers: { + 'Travis-API-Version': '3', + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + + ajaxOptions: function(url, type, options) { + var hash = this._super(...arguments); + + hash.headers = hash.headers || {}; + + var token; + if(token = this.get('auth').token()) { + hash.headers['Authorization'] = "token " + token; + } + + return hash; + }, + + // TODO: I shouldn't override this method as it's private, a better way would + // be to create my own URL generator + _buildURL: function(modelName, id) { + var url = []; + var host = get(this, 'host'); + var prefix = this.urlPrefix(); + var path; + + if (modelName) { + path = this.pathForType(modelName, id); + if (path) { url.push(path); } + } + + if (id) { url.push(encodeURIComponent(id)); } + if (prefix) { url.unshift(prefix); } + + url = url.join('/'); + if (!host && url && url.charAt(0) !== '/') { + url = '/' + url; + } + + return url; + }, + + pathForType: function(modelName, id) { + var underscored = Ember.String.underscore(modelName); + return id ? underscored : Ember.String.pluralize(underscored); + } +}); diff --git a/app/models/build.coffee b/app/models/build.coffee index 10f455ab..336cda61 100644 --- a/app/models/build.coffee +++ b/app/models/build.coffee @@ -21,7 +21,7 @@ Build = Model.extend DurationCalculations, eventType: DS.attr('string') repo: DS.belongsTo('repo', async: true) - commit: DS.belongsTo('commit', async: true) + commit: DS.belongsTo('commit') jobs: DS.hasMany('job', async: true) config: (-> diff --git a/app/models/repo.coffee b/app/models/repo.coffee index 9fc2d8cc..bace43f2 100644 --- a/app/models/repo.coffee +++ b/app/models/repo.coffee @@ -11,31 +11,12 @@ Repo = Model.extend slug: DS.attr() description: DS.attr() private: DS.attr('boolean') - lastBuildNumber: DS.attr('number') - lastBuildState: DS.attr() - lastBuildStartedAt: DS.attr() - lastBuildFinishedAt: DS.attr() githubLanguage: DS.attr() - _lastBuildDuration: DS.attr('number') - lastBuildLanguage: DS.attr() active: DS.attr() - lastBuildId: DS.attr('number') - lastBuildHash: (-> - { - id: @get('lastBuildId') - number: @get('lastBuildNumber') - repo: this - } - ).property('lastBuildId', 'lastBuildNumber') - - lastBuild: (-> - if id = @get('lastBuildId') - @store.find('build', id) - @store.recordForId('build', id) - ).property('lastBuildId') + lastBuild: DS.belongsTo('build') withLastBuild: -> - @filter( (repo) -> repo.get('lastBuildId') ) + @filter( (repo) -> repo.get('lastBuild') ) sshKey: (-> @store.find('ssh_key', @get('id')) @@ -150,7 +131,10 @@ Repo.reopenClass @find() accessibleBy: (store, login) -> - repos = store.query('repo', { member: login, orderBy: 'name' }) + # this fires only for authenticated users and with API v3 that means getting + # only repos of currently logged in owner, but in the future it would be + # nice to not use that as it may change in the future + repos = store.query('repo', { 'repository.active': 'true' }) repos.then () -> repos.set('isLoaded', true) @@ -169,7 +153,7 @@ Repo.reopenClass withLastBuild: (store) -> repos = store.filter('repo', {}, (build) -> - build.get('lastBuildId') + build.get('lastBuild') ) repos.then () -> @@ -177,20 +161,16 @@ Repo.reopenClass repos - bySlug: (store, slug) -> - # first check if there is a repo with a given slug already ordered - repos = store.peekAll('repo').filterBy('slug', slug) - if repos.get('length') > 0 - repos - else - store.query('repo', { slug: slug }) - fetchBySlug: (store, slug) -> - repos = @bySlug(store, slug) + repos = store.peekAll('repo').filterBy('slug', slug) if repos.get('length') > 0 repos.get('firstObject') else - repos.then (repos) -> + adapter = store.adapterFor('repo') + modelClass = store.modelFor('repo') + adapter.findRecord(store, modelClass, slug).then (resourceHash) -> + store.push(store.normalize('repo', resourceHash)); + , -> error = new Error('repo not found') error.slug = slug Ember.get(repos, 'firstObject') || throw(error) diff --git a/app/routes/repo.coffee b/app/routes/repo.coffee index ef9a1b95..eb80dcc4 100644 --- a/app/routes/repo.coffee +++ b/app/routes/repo.coffee @@ -1,7 +1,9 @@ `import TravisRoute from 'travis/routes/basic'` `import Repo from 'travis/models/repo'` +`import Ember from 'ember'` Route = TravisRoute.extend + store: Ember.inject.service() titleToken: (model) -> model.get('slug') @@ -11,7 +13,7 @@ Route = TravisRoute.extend setupController: (controller, model) -> # TODO: if repo is just a data hash with id and slug load it # as incomplete record - model = @store.find('repo', model.id) if model && !model.get + model = @get('store').find('repo', model.id) if model && !model.get controller.set('repo', model) serialize: (repo) -> @@ -21,7 +23,7 @@ Route = TravisRoute.extend model: (params) -> slug = "#{params.owner}/#{params.name}" - Repo.fetchBySlug(@store, slug) + Repo.fetchBySlug(@get('store'), slug) resetController: -> @controllerFor('repo').deactivate() diff --git a/app/routes/repo/index.coffee b/app/routes/repo/index.coffee index 0715b315..6c32b7d3 100644 --- a/app/routes/repo/index.coffee +++ b/app/routes/repo/index.coffee @@ -6,7 +6,7 @@ Route = TravisRoute.extend @controllerFor('repo').activate('current') renderTemplate: -> - if @modelFor('repo').get('lastBuildId') + if @modelFor('repo').get('lastBuild') @render 'build' else @render 'builds/not_found' diff --git a/app/serializers/application.coffee b/app/serializers/application.coffee index b1916d37..1e23b40b 100644 --- a/app/serializers/application.coffee +++ b/app/serializers/application.coffee @@ -1,7 +1,7 @@ `import DS from 'ember-data'` +`import V2FallbackSerializer from 'travis/serializers/v2_fallback'` -Serializer = DS.ActiveModelSerializer.extend - defaultSerializer: 'application' - serializer: 'application' +Serializer = V2FallbackSerializer.extend + isNewSerializerAPI: true `export default Serializer` diff --git a/app/serializers/build.coffee b/app/serializers/build.coffee index c663c045..3212b68d 100644 --- a/app/serializers/build.coffee +++ b/app/serializers/build.coffee @@ -1,19 +1,19 @@ `import Ember from 'ember'` -`import ApplicationSerializer from 'travis/serializers/application'` +`import V2FallbackSerializer from 'travis/serializers/v2_fallback'` -Serializer = ApplicationSerializer.extend +Serializer = V2FallbackSerializer.extend + isNewSerializerAPI: true attrs: { - repo: { key: 'repository_id' } _config: { key: 'config' } _finishedAt: { key: 'finished_at' } _startedAt: { key: 'started_at' } _duration: { key: 'duration' } } - extractSingle: (store, primaryType, rawPayload, recordId) -> - if commit = rawPayload.commit - rawPayload.commits = [commit] - - @_super(store, primaryType, rawPayload, recordId) + keyForV2Relationship: (key, typeClass, method) -> + if key == 'repo' + 'repository_id' + else + @_super.apply(this, arguments) `export default Serializer` diff --git a/app/serializers/repo.coffee b/app/serializers/repo.coffee index ebdb0287..5bba95f8 100644 --- a/app/serializers/repo.coffee +++ b/app/serializers/repo.coffee @@ -1,7 +1,8 @@ `import Ember from 'ember'` -`import ApplicationSerializer from 'travis/serializers/application'` +`import V2FallbackSerializer from 'travis/serializers/v2_fallback'` -Serializer = ApplicationSerializer.extend +Serializer = V2FallbackSerializer.extend + isNewSerializerAPI: true attrs: { _lastBuildDuration: { key: 'last_build_duration' } } diff --git a/app/serializers/v2_fallback.js b/app/serializers/v2_fallback.js new file mode 100644 index 00000000..eb687e88 --- /dev/null +++ b/app/serializers/v2_fallback.js @@ -0,0 +1,41 @@ +import Ember from 'ember'; +import V3Serializer from 'travis/serializers/v3'; + +export default V3Serializer.extend({ + isNewSerializerAPI: true, + + extractRelationships(modelClass, resourceHash) { + if(resourceHash['@type']) { + return this._super(...arguments); + } else { + let relationships = {}; + + modelClass.eachRelationship((key, relationshipMeta) => { + // V2 API payload + let relationship = null; + let relationshipKey = this.keyForV2Relationship(key, relationshipMeta.kind, 'deserialize'); + + if (resourceHash.hasOwnProperty(relationshipKey)) { + let data = null; + let relationshipHash = resourceHash[relationshipKey]; + if (relationshipMeta.kind === 'belongsTo') { + data = this.extractRelationship(relationshipMeta.type, relationshipHash); + } else if (relationshipMeta.kind === 'hasMany') { + data = relationshipHash.map((item) => this.extractRelationship(relationshipMeta.type, item)); + } + relationship = { data }; + } + + if (relationship) { + relationships[key] = relationship; + } + }); + + return relationships; + } + }, + + keyForV2Relationship(key, typeClass, method) { + return key.underscore() + '_id'; + } +}); diff --git a/app/serializers/v3.js b/app/serializers/v3.js new file mode 100644 index 00000000..05b1a55f --- /dev/null +++ b/app/serializers/v3.js @@ -0,0 +1,105 @@ +import Ember from 'ember'; +import DS from 'ember-data'; + +export default DS.JSONSerializer.extend({ + isNewSerializerAPI: true, + + extractRelationship() { + let relationshipHash = this._super(...arguments); + if(relationshipHash && relationshipHash['@type']) { + relationshipHash.type = relationshipHash['@type']; + } + return relationshipHash; + }, + + extractRelationships() { + let relationships = this._super(...arguments); + return relationships; + }, + + keyForRelationship(key, typeClass, method) { + if(key && key.underscore) { + return key.underscore(); + } else { + return key; + } + }, + + extractAttributes() { + let attributes = this._super(...arguments); + for(let key in attributes) { + if(key.startsWith('@')) { + delete attributes.key; + } + } + + return attributes; + }, + + normalizeArrayResponse(store, primaryModelClass, payload, id, requestType) { + let documentHash = { + data: null, + included: [] + }; + + let meta = this.extractMeta(store, primaryModelClass, payload); + if (meta) { + Ember.assert('The `meta` returned from `extractMeta` has to be an object, not "' + Ember.typeOf(meta) + '".', Ember.typeOf(meta) === 'object'); + documentHash.meta = meta; + } + + let items, type; + if(type = payload['@type']) { + items = payload[type]; + } else { + items = payload[primaryModelClass.modelName + 's']; + } + + documentHash.data = items.map((item) => { + let { data, included } = this.normalize(primaryModelClass, item); + if (included) { + documentHash.included.push(...included); + } + return data; + }); + + return documentHash; + }, + + normalize(modelClass, resourceHash) { + let { data, included } = this._super(...arguments); + if(!included) { + included = []; + } + let store = this.store; + + if(data.relationships) { + Object.keys(data.relationships).forEach(function (key) { + let relationship = data.relationships[key]; + let process = function(data) { + if(data['@representation'] !== 'standard') { + return; + } + let type = data['@type']; + let serializer = store.serializerFor(type); + let modelClass = store.modelFor(type); + let normalized = serializer.normalize(modelClass, data); + included.push(normalized.data); + if(normalized.included) { + normalized.included.forEach(function(item) { + included.push(item); + }); + } + }; + + if(Array.isArray(relationship)) { + relationship.forEach(process); + } else if(relationship && relationship.data) { + process(relationship.data); + } + }); + } + + return { data, included }; + } +}); diff --git a/app/templates/components/repos-list-item.hbs b/app/templates/components/repos-list-item.hbs index 9967ca73..77e8dc00 100644 --- a/app/templates/components/repos-list-item.hbs +++ b/app/templates/components/repos-list-item.hbs @@ -1,5 +1,5 @@ -
-

+
+

{{#if repo.slug}} {{#link-to "repo" repo}} {{status-icon status=repo.lastBuildState}} @@ -7,32 +7,31 @@ {{/link-to}} {{/if}}

- {{#with repo.lastBuildHash as lastBuild}} - {{#if repo.slug}} - {{#if lastBuild.id}} -

- {{#link-to "build" repo lastBuild.id}} - - {{lastBuild.number}} - {{/link-to}} -

- {{/if}} + + {{#if repo.slug}} + {{#if repo.lastBuild.id}} +

+ {{#link-to "build" repo repo.lastBuild.id}} + + {{lastBuild.number}} + {{/link-to}} +

{{/if}} - {{/with}} + {{/if}}

Duration: - - {{format-duration repo.lastBuildDuration}} + + {{format-duration repo.lastBuild.duration}}

Finished: - - {{format-time repo.lastBuildFinishedAt}} + + {{format-time repo.lastBuild.finishedAt}}