现代Ruby序列化程序

2020-09-12 00:08:41

好久不见!这次我想写的是几年前我正在工作的一个库,Ruby中的一个序列化器库。我实际上大约一年前就完成了,我一直想创建这个博客帖子,但是去年对我来说太忙了,所以一直没有机会正确地展示它:)。

但首先,我想从历史的角度谈谈Ruby中的序列化程序:)。

早在2014年,当我开始使用API时,用于序列化Ruby类的首选库是ActiveModelSerializers,这是新发布的0.9.x版本。回到那时,在GraphQL甚至还没有发布之前,我们就已经使用这个了不起的库构建了基本的类似GraphQL的结构!使用这样一个功能强大的工具是一件令人愉快的事情,在这个工具中,客户端可以指定它需要来自特定资源的哪些字段,也可以指定哪些关系。您可以在实际呈现序列化程序时准确地注入所需的内容,这意味着您可以获得客户端所需的字段/关联、默认字段/关联(如果客户端的输入为空)以及允许访问哪个客户端的联合。结果是一个非常灵活的API。

除了AMS之外,还有一个相当不错的Oat小型库,它支持开箱即用的HAL、SIREN和JSON:API,以及jbuilder,我当时觉得它就像是哥斯拉(Godzilla),但老实说,这是用Ruby构建超媒体API的一种完全合适的方式,因为你可以用缓存做很多很酷的事情。但是问题是你不能创建可重用的适配器,基本上它就像Rails视图,一切都需要从头开始实现。

展望未来,2015年,0.10.x版本的AMS重写已经从2014年开始。由于种种原因,在我当时工作的公司里,决定用0.10.x,当然我们完全后悔了。在这一点上,我们使用的是0.10.RC4,在0.10.0之前只有一个RC版本。现在你可以告诉我,“当然,你想要什么?您使用的是RC版本“,您可能是对的。只是挫败感不是来自没有百分之百打磨的东西。我对此没有意见,我很想帮忙。挫败感来自于这样一个事实,即体系结构与0.9和0.8版本完全不同,一切都不同,代码和(稀疏的)文档都不同。我之所以这样说,是因为我确实试图帮助发送拉请求,而Morei正在处理代码,我越沮丧。

0.8/0.9和0.10.x都使用相同的repo,尽管它们共享完全不同的API/代码,从API开发人员的角度来看,这使得事情非常混乱,但从贡献者的角度来看,也很难管理。

GEM将JSON:API作为第一公民。这对于为不同的规范构建适配器是不灵活的,并且几乎影响了代码中的所有内容。以前的版本使用的是AMSAPI样式(主要是带有一些非常基本的模式的json)。0.10.x版本也支持该样式,但代码中的代码就像是一个完全不同的分支(不确定是否仍然是这样的,但我想是的)。我知道JSON:API是一个相当流行的规范,但我认为将整个泛型库紧跟该规范是一个糟糕的设计。

紧密耦合到ActiveRecord(至少在那时,不确定现在是否已更改。)

缓存是在同一个库中实现的。我真的认为这是一个完全不同的问题..。

某些部分与Ruby编码样式不匹配(如:‘a,string,of,resources’,default_include‘**’)。

维护人员不愿意合并Pull请求,因为即使在Github问题中,也是实现了什么、缺少了什么、存在什么bug等等的极端混杂。

总而言之,这是一次失败。比如,想象一下Ruby中最受欢迎的序列化程序gem,Rails团队正在推动它们在rails 5发布之前及时完成,这样他们就可以将其包含在同一版本中(虽然完全不同的gem,我猜他们会对此做个说明),维护人员正在努力实现这一点,当然没有。

现在我还应该稍作停顿,说我对提交者/维护者没有什么意见。他们肯定已经尽力了,问题不在那里。只是有时候开源项目会失败。根据我的经验,当没有一个非常紧密的核心团队拥有相同的愿景、编码风格,并且在拉请求中担任把关人时,他们就会失败,直到库证明了自己。然后,任何新的拉请求可能都会尊重现有的代码,不会对它构成挑战。在这一点上,合并代码会容易得多,因为在这一点上,合并代码会容易得多,直到库证明了自己。然后,任何新的拉请求可能都会尊重现有的代码,不会对其构成挑战。到那时,合并代码将会容易得多,因为在这一点上,合并代码将会容易得多。

取而代之的是AMS,不同的人开始返工,不同的人接手并尽最大努力完成它,在整个过程中,不同的人发送各种功能和错误的拉式请求,当然是为了帮助解决问题,但发生的是一个非常复杂的架构,没有明确的设计和愿景。实际上,当时接受项目贡献者的一些采访是一个很好的想法。我认为他们会对所发生的事情有一些见解:)。

在我对AMS感到失望期间,我一直在想:构建一个功能完整的Ruby序列化程序会有多难呢?嗯,结果比我最初想象的要复杂一些。更加复杂:)。

我想我不是那个对AMS有点失望的人。AMS核心成员Beauby创建了一个JSON:API特定的序列化器gem,即jsonapi-rb。它很好地遵循规范,应该可以很好地使用它。它也是无依赖的。

另一方面,一年后(2018年),Netflix发布了他们自己的JSON:API序列化程序gem,老实说,这是非常快的。比如Super FAST。不幸的是,它需要ActiveSupport作为依赖项(不确定为什么,以前的版本没有),这对一些应用程序来说可能是个麻烦。此外,它可能没有AMS或jsonapi-RB那么灵活(尽管最新版本缩小了很多差距),但根据我的经验,它是灵活的。

今天,如果有人问我应该使用哪个序列化程序gem,我会告诉他/hergo使用fast_jsonapi gem。它非常快,它支持一个相当流行的APIspec,你必须有一个非常好的理由不使用它。

就我个人而言,我有点反对在响应中有链接,把客户视为愚蠢的。这就是我不喜欢JSON:API的原因之一。我认为它太冗长了。(另一个原因是命名:他们挑选了关于API的两个最流行的词,JSON和API,把它们粘在一起,并命名为JSON:API,感觉太欺骗了……)我喜欢用内省的方法把一些工作卸给客户。我前段时间已经谈过了,但是如果有人来问我对一个全新的API有什么建议,我的回答会相当简单:JSON:API,除非您有充分的理由不这样做。

更有经验的API设计人员可能想要实现更具可逆性和更高级的东西,为特定的用例量身定做。或者,可能想尝试一下。例如,另一个与JSON理念相同的很酷的API规范:API是Ion。不像JSON:API那么流行,但值得一试!问题是,当您需要在Ruby中实现与JSON:API不同的东西时,您实际上没有太多的选择。你有,它是AMS,但是从它创建一个自定义序列化程序会很麻烦。而且它会非常慢。而且你不能有更高级的概念,比如窗体,集合集合上的关系。或者你可以使用jbuilder,但是jbuilder的问题是它不能有适配器的概念,因此你总是需要从头开始构建最终的结果。一点也不好玩。

当我开始开发SimpleAMS的原型时,我设定了几个目标。

我希望这个库非常简单,易于使用,具有可注入的API和干净的代码。你见过权威人士吗?我需要一个连载专家。

我有时不喜欢Ruby代码风格中的一件事,那就是代码代表您所做的假设的级别。Ruby/Rails中曾经是一种相当常见的模式,导致很多人对API的确切含义感到困惑,同时这样的模式降低了灵活性。代码试图表现得智能,但这种智能的缺点超过了收益。我觉得理想的是可以随时覆盖的一些基本假设。用大括号括起干净、明确的代码是我想要的,所以我想要的是这样的代码:我想要的是智能,但是这种智能的缺点超过了收益。我觉得理想的是可以随时覆盖的一些基本假设。用大括号括起干净、明确的代码是我想要的,所以。

作为一等公民,我想创建一个通用的抽象概念。因此,您应该能够实现所需的任何序列化程序,但该抽象应该足够强大,甚至可以覆盖最极端的情况。毕竟,要击败fast_jsonapi是很困难的,这不应该是我的目标;)

我想要超级干净的代码,代码中没有聪明的复杂元元素,我还想要内部的预期行为,以及当有人开始查看代码库时它是如何工作的。当然,除了DSL部分,它使用了一些高级的Ruby元编程概念,但如果我们希望它只与include一起工作,这是必要的。

AMS比AMS快得多,众所周知,AMS相当慢,因此这应该很容易:)。

在开始任何新的项目之前,我喜欢发挥我的想象力,想出一个有用的API和用例。在上帝模式下,把所有的约束放在一边,你可以想出一些非常酷的API。

那么如何在Ruby中使用序列化器库呢?为了使用SimpleAMS,我希望避免任何继承,因为它有很多限制(假设您只能从Ruby中的一个类继承)。最好包括一个常规模块,并让它使用Ruby的钩子完成所有必要的工作(这是大量的工作)。在SimpleAMS中,DSL非常有表现力,类似于其他gem的DSL:

类UserSerializer包括SimpleAMS::DSL适配器SimpleAMS::Adapters::JSONAPI Attributes:ID,:Name,:Email,:Created_at,:Role type:User Collection:Users Has_Many:MicroPosts End。

适配器接受一个类,这意味着您可以注入您的自定义适配器(可能继承自JSON:API默认适配器)。

SimpleAMS是无依赖关系的,这就是为什么你还需要指定集合的名称(这就是Collection:Users所做的)。这是必要的,除非我们想引入一个Inflector库(如ActiveSupport),但是我真的不明白引入另一个依赖关系的意义,特别是一个和ActiveSupport一样大的依赖关系。

这是一个简单的示例,但是SimpleAMS DSL可以归结为以下模式:

这些是类似Adapter的指令。它们接受一个值,还可以选择一个哈希图,这些选项将直接传递给适配器,因此它们是特定于适配器的。例如:

在这里,root选项被向下传递给指定的适配器,并且特定于该适配器。这些选项包括适配器、类型和primary_id。

当然,因为我们在这里谈论的是Ruby,所以不允许动态值/hashmap组合将是一个很大的限制。基本上,任何这样的指令都可以接受lambda(通常是任何响应调用的指令),并且应该返回一个数组,其中第一部分是值,第二部分是选项。有一个向下传递给函数/lambda的参数,那就是实际的资源。但是,在尝试呈现资源集合(这是未定义行为的定义)时更改适配器是没有意义的,因此对于该特定指令,lambda将失败。但是对于primary_id或更重要的类型,这是救命稻草,因为它是呈现多态关联的唯一方法:

它们与上面的类似,只是它们也有一个实际值,该值通过适配器转换为表示形式。

现在,如果我们希望将其转换为序列化程序,则链接可能如下所示:

这里很明显,链接上下文是序列化程序本身,链接关系是提要,值是/api/v1/me/feed。现在可以说,提要应该是与关系类型不同的链接名称。关系类型可以是micropost。实际上,JSONAPI v1.1就是这种情况。在这种情况下,提要应该仅被视为名称(无论这意味着什么),并且关系类型将放在链接选项中,如下所示:

SimpleAMS有各种这样的选项,比如:链接、元、表单和更抽象的泛型,其余所有选项都继承自并打算用于特定于适配器的场景或我们在API中尚未弄清楚的事情。

我相信这才是库的真正力量:不要假设任何事情,一定要给一些促进者,但最终,让开发人员决定如何在您的库的基础上构建。因为会有一些您甚至没有想到的用例,限制这些用例将是一个糟糕的设计。

出于历史原因,我保留了属性,但是使用字段也是有效的,毕竟它只是一个别名:

当然,任何字段都可以通过在序列化程序中定义同名方法来覆盖。在那里,您可以访问一个名为Object的方法,该方法保存要序列化的实际资源:

这些指令允许我们在资源中附加关系。HAS_ONE只是OWNSES_TO的别名,因为API之间没有真正的区别。

实际上,关系要复杂一些。具体来说,什么是微帖子,符号?是关系名称吗?是关系类型吗?您如何指定您想要该关系的特定属性。

如果您仔细想想,就会发现关系只是一种递归思想。让我来解释一下:在幕后,上面的关系试图使用一个名为MicropostsSerializer的序列化程序来呈现方法结果。如果这样的序列化程序不存在,它将失败并抛出错误。假设您有自己的序列化程序,如下所示:

有趣的部分从这里开始:您可以重写序列化程序中定义的任何指令来获取子集,但不能获取超集。例如,如果您只想显示内容,可以执行以下操作:

但是,有时令人讨厌的规范可能在主体中定义关系的部分,而在其他地方定义关系的部分。例如,JSON:API通过在主体中有一些链接,其余在包含的部分中有一些链接来做到这一点。如果您在Relationship指令中传递一个块,也可以做到这一点:

HAS_MANY:micropost,序列化程序:MicropostsSerializer,field:[:content]do#这些转到名为`Embedded`的类,连接到关系链接:self,->;(Obj){";/api/v1/Users/#{obj.。Id}/Relationship/micropost";}link:Related,-&>;(Obj){[";/api/v1/Users/1";,rel::user]}结束

在该块中,您可以传递原始DSL支持的任何参数,这些参数将存储在MicropostsSerializer下的嵌入式类中。

顺便说一句,SimpleAMS足够聪明(这是极少数这样做的情况之一),可以计算出如果lambda返回不是数组的内容,那么这一定是值,而选项只是空的。

另一件事是,有时候,我们想把亲戚的名字从类型中分离出来。这里的micropost是关系名称(同样,不管这是什么意思),而类型是由MicropostsSerializer定义的,除非我们覆盖它,这既可以在关系序列化程序本身中完成,也可以在使用来自父序列化程序的关系时完成:

HAS_MANY:micropost,序列化程序:MicropostsSerializer,字段:[:Content],type::feed do link:self,->;(Obj){";/api/v1/Users/#{obj.。Id}/Relationship/micropost";}link:Related,-&>;(Obj){[";/api/v1/Users/1";,rel::user]}结束。

在内部,SimpleAMS区分类型和名称,通常类型是语义上比名称更强的东西(类似于关系类型)。您甚至可以使用Name选项注入关系的名称:

HAS_MANY:micropost,序列化程序:MicropostsSerializer,field:[:content],type::feed,name::post do link:self,->;(Obj){";/api/v1/Users/#{obj.。Id}/Relationship/micropost";}link:Related,-&>;(Obj){[";/api/v1/Users/1";,rel::user]}结束。

正如我所说的,名称(通常是JSON格式的包含关系的属性的名称)在大多数规范中实际上没有任何语义含义。至少我还没有看到任何依赖于关系的根属性名称的规范。相反,它是重要的类型,因为类型是Web链接RFC定义的。但是,能够指定像名称这样的东西,对于超出通常API的琐碎边界的情况是至关重要的,因为它启用了API或SimpleAMS适配器。

集合指令是我在任何其他序列化程序库中都没有看到的另一件事。我们以前看到它是:

它基本上以复数形式告诉资源的名称。如果您的适配器使用根元素序列化集合,这是必需的。但它可以做的远不止这些:它允许您在集合级别定义指令。例如,如果您想要一个应该应用于集合而不是集合的每个资源的链接,那么您需要在集合的块中定义它:

或者,如果我们还想要获得集合的总计数,那么实际上应该包含在其中:

同样,在该块内,您可以使用常规DSL定义,无论您将在资源级定义什么。这只是另一个递归级别,因为我在这里向您展示的相同内容可以应用于块内的集合级。例如,在理论上(如果适配器支持它),您可以指定仅应用于集合级的关系:

类UserSerializer包括SimpleAMS::DSL适配器SimpleAMS::Adapters::JSONAPI Attributes:id,:Name,:Email,:Created_at,:Role type:User Collection:Users do link:self,";/api/v1/Users";meta:count,->;(集合,s){集合。Count}Has_One:S3_Uploader#不管是什么意思:p end has_more:微帖子结束。

SimpleAMS定义了一个健壮的DSL,可以应用于很多部分,直到现在,我们已经看到它可以应用于序列化程序本身、关系和集合内部,所以这样的设计让事情变得相当健壮,因为您没有更多的用例和不确定的情况,所以SimpleAMS定义了一个健壮的DSL,它可以应用于许多部分,直到现在我们已经看到它可以应用于序列化程序本身、关系和集合内部。正如我们将看到的,还有一个地方可以应用:当尝试使用序列化程序呈现资源时。

您只需指定一个序列化程序。在上面的示例中,产生的资源反映了序列化程序内部定义的内容。但是,正如我们前面所说的,序列化程序充当一种过滤机制,这意味着您可以覆盖序列化程序定义的任何内容,因为结果创建的是子集而不是超集(任何超集选项都将被忽略)。

或者,您可以覆盖这些关系,并指定不希望包含序列化程序中定义的任何关系:

请注意,在某些AMS版本中没有只有或例外功能,虽然很方便,但它们在序列化库中造成了更多的复杂性(=更多的错误),并且真正的好处非常小(事实上,这里只缺少例外功能)。

正如我们已经注意到的,您不能覆盖字段、关系、链接等,因为最终结果有更多未由序列化程序定义的链接。在内部,即使您在呈现资源时添加额外的字段,SimpleAMS也会忽略这些字段。更好的、更具防御性的做法是抛出一个错误,可能是下一个版本的一个特性:)。

我从事了多年的api开发工作,我所见过的最有用的模式是您注入EVE的模式。

.