【译】可编程的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 国际许可协议进行许可。
相关文章
相关标签/搜索