【譯】可編程的web項目 (二)API設計

API 設計

在本篇中你將學會如何設計本身的超媒體接口,同時學會如何利用Apiary來建立專業的接口文檔。html

在實現本身的接口前完成一份專業的API文檔十分重要。第一,實現接口前你須要考慮到客戶端訪問API的最佳方式,從而得出最好的設計方案,若是沒有提早寫好的設計文檔,直接完成的API的就會過度基於接口實現的代碼,從而會有必定的侷限性。第二,在設計文檔的過程當中你須要考慮到API的返回狀況,即能在實現接口前接收到API的反饋,與修改已實現的API相比,對設計和文檔的更正會容易不少。前端

API概念

本章練習中的API是一個基於音樂元數據存儲的服務接口,主要能夠用來管理與完善和音樂有關的數據。在這個例子當中,數據結構並非很複雜:音樂元數據總共分爲三個模塊:藝術家(artist),專輯(album)和播放記錄(track);藝術家是專輯的做者,每一個專輯會有一個播放記錄。擁有一個清晰的結構後就很容易去建立數據庫。python

難點一

在這個例子中,第一個難點是藝術家或專輯的重名問題,記錄重名的藝術家和專輯是一件很棘手的事情;其次對於同一張專輯來講,可能會出現多個‘未命名’的播放記錄。因此說,在設計API以前,須要找到一個方法或者調用其餘接口來解決‘不惟一’這個難題。git

難點二

第二個難點是‘羣星’(various artists)問題,簡稱VA。因爲會存在多個藝術家合做的狀況,同一張專輯就會有多個藝術家,因此在這種狀況下,對這張專輯的播放記錄就須要根據不一樣藝術家來分開處理。github

相關服務

爲了更好地理解本例中的API,咱們在此提供幾個類似的基於音樂數據的服務:Musicbrainz, FreeDB. 此外,咱們提供一個能夠用到本例中API的數據源:last.fmweb

數據庫設計

根據上面所提到的概念,咱們能夠建立一個擁有三個模型(models)的數據庫:album,artist和track. 同時,咱們在建立數據庫的時候還須要考慮到‘羣星’的狀況,即還有兩個須要特別注意的存在:擁有多個藝術家的專輯(VA album)和基於多個藝術家的播放記錄(VA track)。在建立數據庫的過程當中,咱們要考慮到每一個模塊的‘惟一性約束’;同時在此提醒,在建立模塊時,咱們應該避免使用原始數據庫ID來定位API中的資源,第一是由於原始ID並不具備任何意義,第二是由於這樣會給一些不但願未經受權用戶推斷出有關信息的API帶來漏洞。正則表達式

‘惟一性約束’容許咱們定義更復雜的惟一性,而不只僅是將單個列定義爲惟一。若是想定義模塊中的多個列爲惟一,即這些列中的特定值組合只能出現一次,咱們能夠將多個列設進‘惟一性約束’中。例如,咱們能夠假設同一個藝術家不會有兩個相同名字的專輯(不考慮屢次編輯的狀況),可是不一樣藝術家的專輯可能有相同的名字,所以咱們並不能將單張專輯的標題定爲惟一,而是應該將專輯標題與藝術家ID的組合定爲惟一,因此此時咱們能夠將這兩列一塊兒設進‘惟一性約束’中。spring

def Album(db.Model):
    __table_args__ = (db.UniqueConstraint("title", "artist_id", name="_artist_title_uc"), )
複製代碼

注意上述代碼中最後的逗號:這是告訴Python這是一個元組而不是一個正常插入的普通值。你能夠在‘惟一性約束’中的元組參數中加上任何想加的列。上述代碼中的‘name’是必須存在的,因此請儘可能讓它具備表達意義。對單個播放記錄來講,咱們應該給它一個更完善的‘惟一性約束’:對一張專輯來講,每一個播放記錄都應該有一個單獨的索引號,因此對播放記錄來講,‘惟一性約束’應該是專輯ID,播放記錄數量和播放記錄索引號的組合。sql

def Track(db.Model):
    __table_args__ = (db.UniqueConstraint("disc_number", "track_number", "album_id", name="_track_index_uc"), )
複製代碼

爲了解決‘羣星’問題,咱們將容許album模塊中的外鍵‘artist’爲空,而且加一個可選字段‘va_artist’。最終的數據庫代碼以下:數據庫

models.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.engine import Engine
from sqlalchemy import event
from sqlalchemy.exc import IntegrityError, OperationalError

app = Flask(__name__, static_folder="static")
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///development.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)

@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
    cursor = dbapi_connection.cursor()
    cursor.execute("PRAGMA foreign_keys=ON")
    cursor.close()

va_artist_table = db.Table("va_artists", 
    db.Column("album_id", db.Integer, db.ForeignKey("album.id"), primary_key=True),
    db.Column("artist_id", db.Integer, db.ForeignKey("artist.id"), primary_key=True)
)


class Track(db.Model):
    
    __table_args__ = (db.UniqueConstraint("disc_number", "track_number", "album_id", name="_track_index_uc"), )
    
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String, nullable=False)
    disc_number = db.Column(db.Integer, default=1)
    track_number = db.Column(db.Integer, nullable=False)
    length = db.Column(db.Time, nullable=False)
    album_id = db.Column(db.ForeignKey("album.id", ondelete="CASCADE"), nullable=False)
    va_artist_id = db.Column(db.ForeignKey("artist.id", ondelete="SET NULL"), nullable=True)
    
    album = db.relationship("Album", back_populates="tracks")
    va_artist = db.relationship("Artist")

    def __repr__(self):
        return "{} <{}> on {}".format(self.title, self.id, self.album.title)
    
    
class Album(db.Model):
    
    __table_args__ = (db.UniqueConstraint("title", "artist_id", name="_artist_title_uc"), )
    
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String, nullable=False)
    release = db.Column(db.Date, nullable=False)
    artist_id = db.Column(db.ForeignKey("artist.id", ondelete="CASCADE"), nullable=True)
    genre = db.Column(db.String, nullable=True)
    discs = db.Column(db.Integer, default=1)
    
    artist = db.relationship("Artist", back_populates="albums")
    va_artists = db.relationship("Artist", secondary=va_artist_table)
    tracks = db.relationship("Track",
        cascade="all,delete",
        back_populates="album",
        order_by=(Track.disc_number, Track.track_number)
    )
    
    sortfields = ["artist", "release", "title"]
    
    def __repr__(self):
        return "{} <{}>".format(self.title, self.id)


class Artist(db.Model):
    
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, nullable=False)
    unique_name = db.Column(db.String, nullable=False, unique=True)
    formed = db.Column(db.Date, nullable=True)
    disbanded = db.Column(db.Date, nullable=True)
    location = db.Column(db.String, nullable=False)
    
    albums = db.relationship("Album", cascade="all,delete", back_populates="artist")
    va_albums = db.relationship("Album",
        secondary=va_artist_table,
        back_populates="va_artists",
        order_by=Album.release
    )

    def __repr__(self):
        return "{} <{}>".format(self.name, self.id)
複製代碼

資源設計

在定義完數據庫模塊後,咱們能夠開始考慮設計資源。在RESTful API中,資源能夠是任何一個客戶想要獲取的東西。如今咱們須要根據上面定義的三個數據庫模塊來定義咱們的資源。以後,咱們還將解釋在這個API例子中,該如何基於RESTful規則使用HTTP方法來對咱們的資源進行操做。

數據庫模塊中的資源

一個資源,應該是一個對客戶有足夠吸引力,值得咱們爲其定義一個URI(統一資源標識符)的數據,一樣地,每一個資源都應該經過本身的URI被定義爲惟一資源。對一個API來講,資源的數量有數據庫表數量的兩倍之可能是一件很正常的事情,第一是由於對每一個數據庫表來講,客戶可能會想要獲取整個表做爲一個‘集合’數據,也可能會想要單獨訪問表中某一行來獲取‘個體’數據。有時候,即便‘集合’數據中已經包含了全部‘個體’數據,可是爲了能讓客戶操縱數據,咱們仍須要將‘個體’數據設置爲一個單獨的資源。

根據上面的這個解釋,咱們能夠暫時定義6個資源:

  1. artist collection
  2. artist item
  3. album collection
  4. album item
  5. track collection
  6. track item

值得注意的是,一個‘集合’類資源並非必定要包含相關表中的全部內容。舉例說明,對在整個數據層次中處於最高層的藝術家而言,擁有一個包含全部藝術家的集合是一個有意義的操做; 但像track表中包含了全部有關播放記錄的數據,這全部的播放記錄放在一塊兒做爲一個集合的意義並不大,咱們須要的更有意義的集合,例如根據不一樣專輯分組的播放記錄集合。 對專輯來講,和播放記錄的處理方式同樣,將根據不一樣藝術家分組的專輯合集定爲資源更有意義(一個特定藝術家的全部專輯)。同時,咱們還須要考慮到‘羣星’問題,那麼咱們能夠定義兩個不一樣的‘集合’資源:一個是某個特定藝術家的全部專輯,另外一個是多個藝術家的全部專輯。 最終咱們將資源定義爲:

  1. artist collection
  2. artist item
  3. albums by artist collection
  4. VA albums collection
  5. album item(incorporates track collection)
  6. track item

與普通專輯相比,咱們對VA專輯的處理方式略有不一樣。爲了更好地記錄VA專輯的播放數據,咱們須要再加一些單獨的數據表達做爲資源。最後咱們也能夠加上全部專輯的集體資源,這是爲了讓客戶能夠看到咱們的API提供了哪些專輯數據。

  1. artist collection
  2. artist item
  3. all albums collection
  4. albums by artist collection
  5. VA albums collection
  6. album item(incorporates track collection)
  7. track item
  8. VA track item

定位資源

在定義完資源後,而且分析出資源重要性排名後,咱們須要給每一個資源定義一個URI,使每一個資源是被惟必定義的(addressability principle)。 咱們須要定義URI層次結構,咱們但願經過URIs來傳達資源之間的聯繫。對於普通專輯來講,資源層次結構以下:

artist collection
└── artist
    └── album collection
        └── album
            └── track
複製代碼

咱們設定專輯標題加上藝術家ID是惟一的,同時,識別一個惟一的播放記錄的最好方式就是將其索引號和播放名稱與一張具體的專輯結合起來做爲識別符。 將上面全部的因素考慮進去,咱們最終能夠獲得一個路徑:

/api/artists/{artist_unique_name}/albums/{album_title}/{disc}/{track}/
複製代碼

上面這個路徑能夠惟一地定義每個播放記錄,而且將數據層次清晰地表達了出來,全部在層次結構中的資源(包括集合和個體),均可以經過從上面這個路徑的結尾逐漸刪除一個或多個部分來得到。 對於VA專輯的播放記錄,咱們能夠經過將上面路徑中的{artist_unique_name}換成VA來區分,路徑以下:

/api/artists/VA/albums/{album_title}/{disc}/{track}/
複製代碼

除此以外咱們還須要專門爲存儲全部專輯的數據加另外一個分支:

/api/albums/
複製代碼

那麼整個URI樹變成了下面這樣:

api
├── artists
│   ├── {artist}
│   │   └── albums
│   │       └── {album}
│   │           └── {disc}
│   │               └── {track}
│   └── VA
│       └── albums
│           └── {album}
│               └── {disc}
│                   └── {track}
└── albums
複製代碼

對資源的操做

遵循REST原則,咱們的API應該提供針對資源的HTTP方法。咱們在此重申每一個HTTP方法該如何使用:

  • GET - 返回一個資源的表達形式;不作出任何修改
  • POST - 對目標集合添加一個新的示例
  • PUT - 將一個目標資源用新的表達形式替換(只當目標資源存在的時候)
  • DELETE - 刪除目標資源

大多數資源都應該實現GET方法;POST方法通常針對‘集合’資源,PUT和DELETE方法通常針對於‘個體’資源實現。 在咱們這個例子中有兩個例外,第一:album資源既能夠做爲‘集體’資源也能夠做爲‘個體’資源,因此這四個HTTP方法均可以實現; 第二:對於all album這個資源來講,做爲一個‘集體’資源,它並不能實現POST方法,由於咱們能夠看到它的路徑是/api/albums/,咱們從URI中並不能知道這張專輯的做者是誰,即路徑中缺乏咱們建立新專輯須要的藝術家的信息,而藝術家是做爲專輯的父節點存在的,即必需要先有藝術家纔能有專輯。 因此若是咱們想建立一個新的子節點,這個子節點的父節點應該要在URI中能夠被找到,而不是被放在請求中。

咱們將每一個資源對應的HTTP方法在下表列出:

Resource URI GET POST PUT DELETE
artist collection /api/artists/ X X - -
artist item /api/artists/{artist}/ X - X X
albums by artist /api/artists/{artist}/albums/ X X - -
albums by VA /api/artists/VA/albums/ X X - -
all albums /api/albums/ X - - -
album /api/artists/{artist}/albums/{album}/ X X X X
VA album /api/artists/VA/albums/{album}/ X X X X
track /api/artists/{artist}/albums/{album}/{disc}/{track}/ X - X X
VA track /api/artists/VA/albums/{album}/{disc}/{track}/ X - X X

能夠看到咱們遵照了REST原則,每一個HTTP方法都按在預期執行。上面這張表告訴了咱們不少有用的信息:它顯示了能夠發出的全部可能的HTTP請求,甚至提示了它們的含義。 例如,若是你向track資源提交一個PUT申請,它將修改track的數據(更具體地,它會用請求中的數據代替原數據).

練習一:添加一個播放記錄

利用上面所學到的概念,你是否能寫出一個URI來添加一個新的名爲‘Happiness’的播放記錄(該播放記錄是專輯‘Kallocain’中第三個播放記錄,該專輯的做者是‘Paatos’),在此練習中假設這個藝術家的名字是惟一的,而且請在URI中將藝術家的名字所有小寫。

答案:
/api/artists/paatos/albums/Kallocain/

解釋:咱們第一步須要肯定的是這個操做須要用哪種HTTP方法,因爲咱們想要給某一張專輯加播放記錄,那咱們須要用到的方法是POST,因此根據上面資源表中的信息,能用POST方法的資源只有五個(通常只有‘集合’資源才能使用POST方法):artist collection, albums by artist, albums by VA, album 和 VA album. 若是咱們想給一張只有一個藝術家的專輯加播放記錄,很明顯咱們須要操做的資源是album/api/artists/{artist}/albums/{album}/. 那麼咱們可能會好奇播放記錄的信息{track:Happiness; disc:3}該如何加進去呢?

注意,咱們不能將播放記錄的信息放在URI中,而是應該將播放記錄的信息放進POST方法的請求中(request body):在這裏把全部信息都寫在URI中提交給track資源是不正確的(/api/artists/paatos/albums/Kallocain/3/Happiness/),由於track做爲一個‘個體’資源,並不支持POST方法,咱們只能給一個‘集合’中添加新元素,而不能給一個‘個體’添加新記錄。而/api/artists/paatos/albums/Kallocain/3/Happiness/這個URI支持的操做是GET,PUT,DELETE,即當咱們想要獲取,修改或刪除某一個確切的track數據時能夠調用該URI。

進入超媒體世界

爲了讓前端開發者瞭解到底前端須要傳入什麼數據以及所期待的返回值,咱們須要將API詳細記載。 在本課程中,咱們將在API給出的響應中運用超媒體,在本章的例子中,咱們選擇用Mason做爲咱們的超媒體格式,由於對於定義超媒體元素並將它們鏈接到數據中,Mason有着很是清晰的語法。

數據表達形式

咱們的API是經過JSON交流的,對數據表達來講,這是一個很簡單的序列化過程。若是客戶端給/api/artists/scandal/發出了一個GET請求,返回的數據將會被序列化,以下:

{
    "name": "Scandal",
    "unique_name": "scandal",
    "location": "Osaka, JP",
    "formed": "2006-08-21",
    "disbanded": null
}
複製代碼

若是客戶想要添加一個新的藝術家,他們須要發送一個幾乎相同的JSON數據(除去unique_name,由於這個是API服務器自動生成的)。 這個數據的序列化過程幾乎能夠運用到全部模塊上。

對於‘集體’資源來講,在它們的數據體中會包含一個‘items’的屬性--‘items’是一個包含了這個集體資源中一部分數據或所有數據的列表。 例如albums資源中不只包含描述自身信息的根級數據,還包括一個存儲了track信息的列表。 值得注意的是,‘items’中並不用將相應的資源數據所有包含進去,只須要包含必要的信息,例如對於album collections來講,在‘items’中包含的數據只須要有專輯標題和藝術家名字就足夠了:

{
    "items": [
        {
            "artist": "Scandal",
            "title": "Hello World"
        },
    ]
}
複製代碼

若是客戶想要獲得‘items’中某個個體的更多詳細信息,能夠直接經過URI訪問album個體資源來獲取。

超媒體控件(Hypermedia Controls)

你能夠將API想象成一張地圖,而每個資源就是地圖中一個點,一個你最近發送了GET請求的資源就像是一個在說‘你在這裏’的點。而‘超媒體控件’能描述邏輯上的下一步操做:下一步你將走到哪裏,或者是你在的這個點下一步能夠作什麼。 ‘超媒體控件’與資源一塊兒造成了一個能解釋說明該如何在API中‘航行’的客戶端狀態圖(state diagram).在咱們剛剛學到的數據表達中,‘超媒體控件’是做爲一個額外屬性存在於其中的。

超媒體控件是至少兩件事情的組合:連接關係("rel")和目標URI("href").這說明了兩個問題:這個控件作了什麼,以及在哪裏能夠激活這個動做。請注意,連接關係是機器可讀的關鍵字,而不是面向人類的描述。 許多咱們經常使用的連接關係正在標準化,可參考(完整列表),可是API也能夠在須要的時候給出本身的定義 - 只要每一個連接關係是一直表達同一種意思便可。 當客戶想要作某事時,他將使用可用的連接關係來找到這個請求應該用到的URI。這意味着使用咱們API的客戶端永遠不須要知道硬編碼的URIs - 他們將經過搜索正確的連接關係來找到URI。

Mason還爲超媒體控件定義了一些額外的屬性。其中「method」是咱們將會常用的屬性,由於它告訴應該使用哪一個HTTP方法來發出請求(因爲默認方法是GET,因此一般GET方法會被省略)。 還有「title」可幫助客戶(人類用戶)弄清楚控件的做用。除此以外,咱們還能夠定義一個JSON架構來規定發送到API的數據表達格式。

在Mason中,能夠經過添加"@controls"屬性將超媒體控件附加給任何對象。"@controls"自己就是一個對象,其中的屬性是‘連接關係’,其值是至少具備一個屬性(href)的對象。例如,這是一個帶有多媒體控件的track個體資源,用於返回其所在的專輯的連接關係爲(「向上」),編輯其信息的連接關係爲(「編輯」):

{
    "title": "Wings of Lead Over Dormant Seas",
    "disc_number": 2,
    "track_number": 1,
    "length": "01:00:00",
    "@controls": {
        "up": {
            "href": "/api/artists/dirge/albums/Wings of Lead Over Dormant Seas/"
        },
        "edit": {
            "href": "/api/artists/dirge/albums/Wings of Lead Over Dormant Seas/2/1/",
            "method": "PUT"
        }
    }
}
複製代碼

或者,若是咱們但願集合中的每一個個例上都有本身的URI可供客戶端使用:

{
    "items": [
        {
            "artist": "Scandal",
            "title": "Hello World",
            "@controls": {
                "self": {
                    "href": "/api/artists/scandal/albums/Hello World/"
                }
            }
        },
        {
            "artist": "Scandal",
            "title": "Yellow",
            "@controls": {
                "self": {
                    "href": "/api/artists/scandal/albums/Yellow/"
                }
            }
        }
    ]
}
複製代碼

自定義連接關係

在定義咱們的連接關係的時候,雖然儘量使用標準是好的,但實際上每一個API都有許多控件,其含義沒法用任何標準化關係明確表達。所以,Mason文檔可使用連接關係命名空間來擴展可用的連接關係。Mason命名空間定義了前綴及其關聯的命名空間(相似於XML命名空間,請參閱CURIE)。該前綴將被添加到IANA列表中未定義的連接關係上。 當一個連接關係以命名空間前綴爲前綴時,它應被解釋爲在命名空間的末尾附加了關係並使關係惟一 - 即便另外一個API定義了具備相同名稱的關係,它也會有不一樣的命名空間前綴。例如,若是想要一個名爲「albums-va」的關係來標明一個指向全部VA專輯集合的控件,則其完整標識符能夠是http://wherever.this.server.is/musicmeta/link-relations#albums-by, 咱們能夠定義一個名爲「mumeta」的命名空間前綴,而後這個控件看上去將會是這樣:

{
    "@namespaces": {
        "mumeta": {
            "name": "http://wherever.this.server.is/musicmeta/link-relations#"
        }
    },
    "@controls": {
        "mumeta:albums-va": {
            "href": "/api/artists/VA/albums"
        }
    }
}
複製代碼

此外,若是客戶端開發人員訪問完整的URL,他們應該找到有關連接關係的描述。另請注意,一般這是一個完整的URL,由於服務器部分保證了惟一性。在後面的示例中,你將看到咱們正在使用相對URI - 這樣即便服務器在不一樣的地址(最有可能的是localhost:someport)中運行,指向關係描述的連接也會起做用。

有關連接關係的信息必須存儲在某處。請注意,這適用於客戶端開發人員,即人類。在咱們的例子中,一個簡單的HTML文檔應該足以支持每一個關係。這就是咱們的命名空間名稱以#結尾的緣由。它能夠方便地找到每一個關係的描述。在繼續以前,這裏是咱們的API使用的自定義連接關係的完整列表: add-album, add-artist, add-track, albums-all, albums-by, albums-va, artists-all, delete

API 地圖

設計API的最後一項業務是建立一個包含全部資源和超媒體控件的完整地圖。在這個狀態圖中,資源是狀態,超媒體控件是轉換。通常來講,只有GET方法適用於從一種狀態移動到另外一種狀態,由於其餘方法不會返回資源表達。咱們已經提出了其餘方法做爲箭頭回到相同的狀態。這是完整地圖:

MusicMeta API state diagram

  • 注意 1:地圖中每一個盒子顏色的代碼僅用於教育目的,以顯示數據庫中的數據是如何鏈接到資源 - 你不須要在現實生活或本身的項目中實現這樣的細節。

  • 注意 2:地圖中的連接關係「item」並不存在,實際上,這應該是「self」。在此圖中,「item」用於表示這是經過個體數據的「self」連接從集合轉換到個體。

這樣的映射圖在設計API時頗有用,並且都應該在設計API返回每一個單獨的資源表達以前完成。因爲全部操做都在這個地圖中可見,所以更容易查看是否缺乏某些內容。在製做圖表時請記住,必需要有從一個狀態到另外一個狀態的聯通路徑(連通原理)。在咱們的例子中,咱們在URI樹中有三個獨立的分支,所以咱們必須確保每一個分支之間的轉換(例如,AlbumCollection資源具備「artists-all」和「albums-va」)。

練習二:The Road to Transcendence

參考上面的狀態圖。咱們假設你是一個機器客戶端。你當前正站在ArtistCollection節點上,你的目標是要查找和修改有關一個有羣星藝術家的專輯「Transcendental」(Mono和The Ocean的合做)的數據。爲了作到這一點,必須遵循哪些連接?這條路徑有意義嗎?請給出最短的連接關係列表(使用與上面狀態圖中相同的名稱),從而將你從ArtistCollection導出到修改VA專輯的數據(edit)。

答案:
albums-all,albums-va,item,edit

注意,最後咱們是須要修改VA專輯中的數據,因此到達了VAAlbum咱們還須要經過訪問‘edit’連接關係來修改數據。

API入口

關於API映射圖的最後一點概念是入口點(Entry Point)。這應該是API的根源(在咱們的例子中應該是:/api/,它有點像API的索引頁面。它不是資源,一般不返回(因此它不在狀態圖中)。它只是顯示了客戶在「進入」API時的合理啓動選項。在咱們的例子中,它應該包含多媒體控件來調用GET方法來獲取藝術家集合或專輯集合(也有多是VA專輯集合)。

練習三:「進入迷宮」

建立一個MusicMeta API入口點的JSON文檔。它應該包含兩個超媒體控件:連接到藝術家集合(Artist Collection),並連接到專輯集合(Album Collection)。你應該可以從上面的狀態圖中找出這些控件的連接關係。不要忘記使用mumeta命名空間!

答案_t3

帶架構的高級控件

到目前爲止,咱們已經使用超媒體定義了可能的操做。每一個操做都帶有一個具備明確含義的連接關係,相關資源的地址以及要使用的HTTP方法。這些信息對於GET和DELETE請求是足夠的,但對於POST和PUT來講還不夠 - 由於咱們仍然不知道應該在請求體中放什麼。Mason支持將JSON架構添加到超媒體控件中。該架構定義了API將認爲哪一種JSON文檔是有效的。例如,這是專輯資源的完整架構:

{
    "type": "object",
    "properties": {
        "title": {
            "description": "Album title",
            "type": "string"
        },
        "release": {
            "description": "Release date",
            "type": "string",
            "pattern": "^[0-9]{4}-[01][0-9]-[0-3][0-9]$"
        },
        "genre": {
            "description": "Album's genre(s)",
            "type": "string"
        },
        "discs": {
            "description": "Number of discs",
            "type": "integer",
            "default": 1
        }
    },
    "required": ["title", "release"]
}
複製代碼

對於上面這個對象,架構自己由三個屬性組成:

  • 「type」 - 這定義了資源數據類型,一般是「對象」但有時是「數組」
  • 「properties」 - 一個定義了全部可能/預期屬性的對象
  • 「required」 - 一個列出必需屬性的數組

「properties」中的值一般具備「描述」(「description」)(面向人類讀者)和「類型」(「type」)。它們還能夠具備一些其餘屬性,如示例中所示:pattern - 一種正則表達式,定義哪一種值對此屬性有效(僅與字符串兼容); default,該屬性在缺失時所使用的值。這些只是JSON架構能夠作的一些基本事情。你能夠從其詳述中閱讀更多內容。

像這樣的JSON架構對象能夠經過被添加到Mason超媒體控件的「schema」屬性中來發揮做用。若是你的架構特別大或者你有其餘理由不將其包含在響應正文中,你能夠選擇從API服務器上的URL(例如/ schema / album /)提供架構,並將URL分配給「schemaUrl」屬性,以便客戶端能夠檢索它。這樣客戶端就能夠在將數據發送到API時使用架構造成正確的請求。機器客戶端是否可以肯定每一個屬性的該放的內容是一個不一樣的事,一種選擇是使用符合標準的名稱,例如咱們可使用與MP3文件中的IDv2標籤相同的屬性名稱。

架構對於(部分)生成的具備人類用戶的客戶端特別有用。根據架構編寫一段生成表單的代碼很是簡單,以便人類用戶能夠填充它。咱們將在課程的最後一次練習中展現這一點。在API方面,架構實際上有雙重任務 - 它還可用於驗證客戶端的請求(和使用該功能同樣)。值得注意的是,咱們示例中的日期架構並不是萬無一失(它會接受2000-19-39之類不正確的數據),在實現中必須注意到這一點。一個徹底萬無一失的正則表達式會很長 - 你能夠看看本身能不能想出一個合適的正則表達式。

架構也可被用於使用了查詢參數的資源。在這種狀況下,他們描述了可接受的參數和值。 做爲示例,咱們能夠添加一個影響專輯集合排序方式的查詢參數。下面是一個添加了架構的「mumeta:albums-all」控件例子。另請注意「isHrefTemplate」的添加。

{
    "@controls": {
        "mumeta:albums-all": {
            "href": "/api/albums/?{sortby}",
            "title": "All albums",
            "isHrefTemplate": true,
            "schema": {
                "type": "object",
                "properties": {
                    "sortby": {
                        "description": "Field to use for sorting",
                        "type": "string",
                        "default": "title",
                        "enum": ["artist", "title", "genre", "release"]
                    }
                },
                "required": []
            }
        }
    }
}
複製代碼

客戶端示例

爲了讓你瞭解咱們爲何要經歷全部上面那些麻煩併爲咱們的有效負載添加一堆字節,讓咱們從客戶的角度考慮一個小例子。假設咱們的客戶端是一個提交機器人(bot),能夠瀏覽其本地音樂集合,並能夠將尚不存在的藝術家/專輯元數據發送API。 假設它的本地集合按藝術家和專輯分組。而且假設它正在檢查一個包含一個專輯文件夾(「All Around Us」)的藝術家文件夾(「Miaou」)。 目標是看這個藝術家是否在該集合中,以及它是否有這個專輯。

  1. bot進入api並經過尋找名爲「mumeta:artists-all」的超媒體控件找到藝術家集合
  2. bot使用超媒體控件的href屬性向藝術家集合發送GET
  3. bot尋找一位名叫「Miaou」的藝術家,卻找不到它
  4. 機器人尋找「mumeta:add-artist」超媒體控制
  5. bot使用「mumeta:add-artist」控件的href屬性和關聯的JSON模式編譯發送POST請求
  6. 在發送POST請求後,bot從響應中的「location header」中獲取新加的藝術家的地址(URI)
  7. bot發送GET給它收到的地址
  8. 從藝術家出發,bot將尋找「mumeta:albums-by」超媒體控制
  9. bot發送GET到該控件的href屬性,接收一個空的專輯集合
  10. 因爲專輯不存在,bot尋找「mumeta:add-album」超媒體控件
  11. bot使用控件的href屬性和關聯的JSON模式編譯發送POST請求

這個例子的重要的部分是機器人如今除了/api/以外不須要任何URI。對於其餘的資源的URI,均可以經過尋找連接關係來獲取。它訪問的全部地址都是從它得到的響應中解析出來的。這些地址多是徹底隨意的,但機器人仍然能夠工做。 根據機器人的AI,它能夠在至關劇烈的API變化中存活下來(例如,當它獲取藝術家表示並找到一堆控件時,它是如何被編程爲遵循「mumeta:albums-by」?)

關於超媒體APIs的一個很是酷的事情是它們一般擁有一個通用客戶端來瀏覽任何有效的API。客戶端將使用超媒體控件生成適用於人類的網站,以提供從一個視圖到另外一個視圖的連接,以及用於生成表單的架構。

超媒體檔案(Hypermedia Profiles)

經過添加超媒體,咱們已經建立了一個機器客戶端能夠在其中「航行」的API,由於它已經能瞭解每一個連接關係的含義以及資源表達中每一個屬性的含義。但機器到底是如何學習這些東西的呢?這是API開發的持續挑戰 - 如今一種方法是讓人類開發人員學習資源檔案。檔案文件會用人類可讀的格式描述資源的語義。這樣人類開發人員能夠將這些知識傳遞給他們的客戶端,或者客戶的人類用戶能夠在使用API時使用這些知識。

什麼是檔案文件?

關於檔案文件究竟應該是什麼,或者如何編寫配置文件,沒有廣泛的共識。可是不管如何編寫,檔案中都應該具備(資源表達中)屬性的語義描述符和能夠採起的操做的協議語義(或與資源相關聯的連接關係列表)。集合不必定有本身的文檔,例如本章練習中的例子。除了專輯資源,由於它既能夠是一個集合,也能夠是一個個體。

若是你的資源中有相對常見的內容表達,建議使用標準(或標準提案)中定義的屬性。若是你的整個資源表示符合標準就更好。你能夠在schema.org/中查找標準表達。咱們的示例API的一個重要的將來步驟是使用此架構中的屬性做爲專輯和播放記錄的屬性。

分佈式檔案

與連接關係同樣,關於你的檔案文件的信息應該能夠從某個位置訪問。在咱們的示例中,咱們選擇使用一個路徑/profiles/{profile_name}/將它們做爲HTML頁面從服務器分發。同時可使用「profile」連接關係將檔案文件的連接做爲超媒體控件插入數據表達中。例如,要從track表達中獲取track檔案文件:

{
    "@controls": {
        "profile": {
            "href": "/profiles/track/"
        }
    }
}
複製代碼

另外一種方式是在迴應中使用HTTP Link header:

Link: <http://where.ever.the.server.is/profiles/track/>; rel="profile"
複製代碼

然而,這有點模棱兩可。咱們的專輯資源是一個應該連接到兩個檔案文件的示例 - 專輯和播放記錄。出於這個緣由,咱們將檔案文件做爲超媒體控件包含在數據表達內,對於集合類型的資源,咱們在每一個個體資源中都包含了一個檔案控件。

API 文檔

爲了使咱們最終的API和它的文檔同樣完美,應參考一種流行的標準記錄API,例如API BlueprintOpenAPI。這兩個標準都帶有一套很好的相關工具:從文檔瀏覽到自動化測試生成(更多示例請參見API Blueprint工具部分)。在本練習中,咱們選擇使用API​​ Blueprint,並使用Apiary編輯器來建立交互式文檔。

API Blueprint的語法相對簡單。你能夠先閱讀官方教程,你還能夠從咱們的示例中學習其他部分。你應該建立一個Apiary賬戶並使用其中的編輯器來完成剩餘的示例和任務。

描述一個資源

這是一個很是簡短的指南,說明如何在文檔中表示每一個資源。資源描述以其名稱開頭,後面跟着的方括號中的包含了它的URI,除此以外你能夠在這一行下面加上面向人類的描述性語言,例如:

## Album Collection [/api/albums/]
This is a collection of all the albums
複製代碼

若是資源的URI中包含變量,則應將這些變量描述爲參數,以下所示:

## Albums by Artist [/api/albums/{artist}/]

+ Parameters

    + artist (string) - artist's unique name (unique_name)
複製代碼

在此以後,每一個操做都須要被描述,包括一個描述性標題和HTTP方法,一樣你能夠在下方加上面向人類的描述。

### List all albums [GET]
複製代碼

對於每一個操做,其中應該包含其連接關係。還須要包含請求部分和響應部分(每一個可能的狀態代碼)。全部這些部分還應包含有效請求和API響應的示例。 例如,Artist的GET方法的專輯文檔(爲簡潔起見省略了消息體,稍後參見完整示例)。

### List albums by artist [GET]

+ Relation: albums-by
+ Request

    + Headers
    
            Accept: application/vnd.mason+json
    
+ Response 200 (application/vnd.mason+json)

    + Body
            ...

+ Response 404 (application/vnd.mason+json)

    + Body
            ...
複製代碼

超媒體問題

使用這些很是好的標準時咱們有一個不便之處:它們不支持超媒體。也就是說,該語法沒有任何方式能將連接關係或資源檔案文件包含到同一文檔中。這就是咱們實際上只是將服務器做爲HTML文件提供服務的緣由。可是對於咱們的API藍圖示例,以及最後的任務,咱們實際上會作一些過分使用。 具體地說,咱們將包括兩個組:連接關係和檔案文件,在這些組內部,每一個連接關係和檔案文件都將按照資源的語法添加。

這樣作能夠建立更好的瀏覽文檔,由於全部內容都會整齊地顯示在索引中,咱們能夠在文檔中放置錨連接以便快速訪問不一樣的部分。可是,這種故意濫用與自動化工具不能很好地兼容,由於自動化工具試圖將全部內容視爲資源。 現有的提議是將超媒體正確地包含在語法中,可是就目前爲止,咱們只有這兩個選項:要麼咱們不在Apiary文檔中包含連接關係和資源檔案文件信息,要麼將它們做爲「資源」放入。

API Blueprint 示例

如下是記錄API中專輯相關資源的示例。因爲文本文件自己過長,咱們建議你將內容複製到新的Apiary項目中。

musicmeta.md

Apiary editor view after pasting

重要提示:該編輯器彷佛沒有自動保存功能。確保在每次更改後交替按下「保存」按鈕 - 首先確保文檔不存在語法警告。 除了主體元素外,全部內容都應縮進1個製表符或4個空格 - 這些空格元素應相對於節標題縮進兩次 + Body.

你還能夠轉到「文檔」選項卡,使用整個屏幕寬度瀏覽API文檔。你能夠單擊文檔中的各類請求在文檔瀏覽器的右側查看請求的詳細信息(以及可能的響應)。

練習四:API Blueprint - Artist

爲了完成本練習並學習API Blueprint,咱們但願你完成Music Meta API文檔的一部分。咱們提供的示例包含專輯的資源組。你的工做是爲藝術家添加資源組。

學習目標:瞭解如何編寫有效的API Blueprint。瞭解如何正確記錄資源。

如何開始:你應該在咱們給出的示例中添加你本身的部分。若是你還沒有下載咱們提供的示例並將其放入Apiary,請當即執行此操做。添加藝術家的資源組,並開始對兩個新資源的描述。 你還應該保留前面的狀態圖,以及咱們在開始時顯示的數據庫模型。提示:按照示例進行操做。你的資源描述必須包含全部相同的信息。你能夠爲數據提供本身的藝術家示例。

藝術家集合資源(Artist Collection) 藝術家集合包括全部藝術家。對於每一個藝術家,除ID以外的全部列值都顯示在其集合條目中,使用與數據庫列相同的名稱。該資源支持兩種方法:GET用於檢索描述,POST用於建立新的藝術家。
對於GET,必須包含一個示例響應主體,其中包含從狀態圖中的ArtistCollection資源引出的全部控件。請注意,某些控件位於藝術家條目中,而不是在根級別,而且不要忘記使用名稱空間。另請注意,add-artist須要包含JSON模式。響應機構還應包括至少一位藝術家的數據。藝術家條目應該是「items」屬性中的數組,而且第一個藝術家必須是你知道存在的一個(即一個來自其餘示例,或者你的POST示例請求中的一個)。
對於POST,必須包含一個有效的示例請求正文,其中包含全部字段的值。同時還必須包含如下錯誤代碼的響應:400和415.你不須要包含響應正文。

藝術家資源(Artist Item) 藝術家資源包括與藝術家集合資源中的一個藝術家相同的信息。該資源支持三種方法:GET,PUT和DELETE。資源應描述你知道存在的藝術家。 對於GET,必須包含一個示例響應主體,其中包含從狀態圖中Artist資源引出的全部控件。編輯連接(edit)還必須包含JSON模式。除了200響應,還添加404(不須要響應正文)。 對於PUT,必須包含具備全部字段值的有效示例請求正文。還必須包含如下錯誤代碼的響應:400,404,415。 對於DELETE,你只須要使用正確的狀態代碼進行回覆,惟一的錯誤碼是404。

答案_t4

本譯文源自芬蘭奧盧大學Ivan Sanchez的課程Programmable web project,由三位在奧盧大學交換生分享譯製,若有措辭不當或任何不妥,請前輩們多多在評論中指點。原課程programmable-web-project。課程中的練習本來有上傳自動檢驗,但須要學校帳號選課登錄,在此直接分享答案。
本文由LL翻譯。


知識共享許可協議
本做品採用 知識共享署名-相同方式共享 4.0 國際許可協議進行許可。
相關文章
相關標籤/搜索