barley 0.2.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +47 -8
- data/lib/barley/serializable.rb +18 -2
- data/lib/barley/serializer.rb +180 -17
- data/lib/barley/version.rb +1 -1
- data/lib/barley.rb +1 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: faa7d627d11906ae112f0a8fd640f0a2d90acacc114c63297a26e227d5120c3d
|
4
|
+
data.tar.gz: c36d22bee59469e7e98f481086a5a32a75ac96d9bab984330a4d945c97efa5f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3adb8da65be384daf38feaf056541f6b3e2c2f7f400917ed2da43a9f382e3bbc28afdad2437fe7489bdcd1ccd3980a587697525f0b493e1ec7b5374acfb61bb8
|
7
|
+
data.tar.gz: a359ed1a3c15f8efdd62ec86112163edc8d423f9d0d8c8790e781c651ac208dfa722a42bb4c93f822b5bbfc2a072626bbf1c9ad088c24354828745476247fc58
|
data/README.md
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
-
![Barley loqo](
|
1
|
+
![Barley loqo](https://i.imgur.com/am0emi4.png)
|
2
2
|
|
3
3
|
Barley is a dead simple, fast, and efficient ActiveModel JSON serializer.
|
4
4
|
|
5
|
-
Cerealize your ActiveModel objects into flat
|
5
|
+
Cerealize your ActiveModel objects into flat hashes with a dead simple, yet versatile DSL, and caching baked in. Our daily bread is to make your API faster.
|
6
6
|
|
7
7
|
You don't believe us? Check out the [benchmarks](#benchmarks). 😎
|
8
8
|
|
9
|
+
## API documentation
|
10
|
+
[Check out the API documentation here](https://rubydoc.info/github/MoskitoHero/barley/main).
|
11
|
+
|
9
12
|
## Usage
|
10
13
|
Add the `Barley::Serializable` module to your ActiveModel object.
|
11
14
|
|
@@ -21,10 +24,20 @@ Then define your attributes and associations in a serializer class.
|
|
21
24
|
```ruby
|
22
25
|
# /app/serializers/user_serializer.rb
|
23
26
|
class UserSerializer < Barley::Serializer
|
24
|
-
attributes :
|
25
|
-
|
26
|
-
|
27
|
-
|
27
|
+
attributes id: Types::Strict::Integer, :name # multiple attributes, optional type checking with dry-types
|
28
|
+
attribute :email # single attribute
|
29
|
+
attribute :value, type: Types::Coercible::Integer # optional type checking with dry-types
|
30
|
+
|
31
|
+
many :posts # relations
|
32
|
+
one :group, serializer: CustomGroupSerializer # custom serializer
|
33
|
+
many :related_users, key: :friends, cache: true # custom key, and caching
|
34
|
+
one :profile, cache: { expires_in: 1.day } do # cache definition, and block (on associations) for nested, on-the-fly serializer
|
35
|
+
attributes :avatar, :social_url
|
36
|
+
attribute :badges do
|
37
|
+
object.badges.map(&:display_name) # use object in a block to return custom code
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
28
41
|
end
|
29
42
|
```
|
30
43
|
|
@@ -234,6 +247,27 @@ Barley.configure do |config|
|
|
234
247
|
end
|
235
248
|
```
|
236
249
|
|
250
|
+
## Type checking
|
251
|
+
Barley can check the type of the object you are serializing with the [dry-types](https://dry-rb.org/gems/dry-types/main/) gem.
|
252
|
+
|
253
|
+
It will raise an error if the object is not of the expected type, or coerce it to the correct type and perform constraints checks.
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
module Types
|
257
|
+
include Dry.Types()
|
258
|
+
end
|
259
|
+
|
260
|
+
class UserSerializer < Barley::Serializer
|
261
|
+
attributes id: Types::Strict::Integer, name: Types::Strict::String, email: Types::Strict::String.constrained(format: URI::MailTo::EMAIL_REGEXP)
|
262
|
+
|
263
|
+
attribute :role, type: Types::Coercible::String do
|
264
|
+
object.role.integer_or_string_coercible_value
|
265
|
+
end
|
266
|
+
end
|
267
|
+
```
|
268
|
+
|
269
|
+
Check out [dry-types](https://dry-rb.org/gems/dry-types/main/) for all options and available types.
|
270
|
+
|
237
271
|
## Breakfast mode 🤡 (coming soon)
|
238
272
|
You will soon be able to replace all occurrences of `Serializer` with `Cerealizer` in your codebase. Just for fun. And for free.
|
239
273
|
|
@@ -264,14 +298,14 @@ Ah ah ah. This is so funny.
|
|
264
298
|
*Note: we are thinking about adding a `Surrealizer` class for the most advanced users. Stay tuned.*
|
265
299
|
|
266
300
|
## JSON:API
|
267
|
-
|
301
|
+
Barley does not serialize to the JSON:API standard. We prefer to keep it simple and fast.
|
268
302
|
|
269
303
|
## Benchmarks
|
270
304
|
This gem is blazing fast and efficient. It is 2 to 3 times faster than [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers) and twice as fast as [FastJsonapi](https://github.com/Netflix/fast_jsonapi). Memory object allocation is also much lower.
|
271
305
|
|
272
306
|
With caching enabled, it is just mind-blowing. We think. *Disclaimer: we do not serialize to the JSON:API standard, so that might be the reason why we are so fast.*
|
273
307
|
|
274
|
-
This is the result we get with the benchmark script used in the AMS repo on an Apple Silicon M1Pro processor.
|
308
|
+
This is the result we get with the benchmark script used in the AMS repo on an Apple Silicon M1Pro processor. [Check it out for yourself here](https://github.com/MoskitoHero/active_model_serializers/tree/benchmarks).
|
275
309
|
|
276
310
|
```shell
|
277
311
|
bundle exec ruby benchmark.rb
|
@@ -351,3 +385,8 @@ ams : 1299674 allocated - 28.20x more
|
|
351
385
|
|
352
386
|
## License
|
353
387
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
388
|
+
|
389
|
+
## Credits
|
390
|
+
Barley is brought to you by the developer team from [StockPro](https://www.stock-pro.fr/).
|
391
|
+
|
392
|
+
[![Barley is brought to you by StockPro](https://i.imgur.com/5a0veEG.png)](https://www.stock-pro.fr/)
|
data/lib/barley/serializable.rb
CHANGED
@@ -4,6 +4,7 @@ module Barley
|
|
4
4
|
# Makes a Model serializable
|
5
5
|
#
|
6
6
|
# * Allows setting a default model Serializer
|
7
|
+
#
|
7
8
|
# @example
|
8
9
|
# class Item < ApplicationRecord
|
9
10
|
# include Barley::Serializable
|
@@ -19,10 +20,15 @@ module Barley
|
|
19
20
|
class_methods do
|
20
21
|
# @example without cache
|
21
22
|
# serializer ItemSerializer
|
23
|
+
#
|
22
24
|
# @example with cache
|
23
25
|
# serializer ItemSerializer, cache: true
|
26
|
+
#
|
24
27
|
# @example with cache and expires_in
|
25
28
|
# serializer ItemSerializer, cache: {expires_in: 1.hour}
|
29
|
+
#
|
30
|
+
# @param klass [Class] the serializer class
|
31
|
+
# @param cache [Boolean, Hash<Symbol, ActiveSupport::Duration>] whether to cache the result, or a hash with options for the cache
|
26
32
|
def serializer(klass, cache: false)
|
27
33
|
define_method(:serializer) do
|
28
34
|
klass.new(self, cache: cache)
|
@@ -33,9 +39,19 @@ module Barley
|
|
33
39
|
included do
|
34
40
|
serializer "#{self}Serializer".constantize
|
35
41
|
|
36
|
-
|
42
|
+
# Serializes the model
|
43
|
+
#
|
44
|
+
# @note this method does not provide default rails options like `only` or `except`.
|
45
|
+
# This is because the Barley serializer should be the only place where the attributes are defined.
|
46
|
+
#
|
47
|
+
# @param serializer [Class] the serializer to use
|
48
|
+
# @param cache [Boolean, Hash<Symbol, ActiveSupport::Duration>] whether to cache the result, or a hash with options for the cache
|
49
|
+
# @param root [Boolean] whether to include the root key in the hash
|
50
|
+
#
|
51
|
+
# @return [Hash] the serialized attributes
|
52
|
+
def as_json(serializer: nil, cache: false, root: false)
|
37
53
|
serializer ||= self.serializer.class
|
38
|
-
serializer.new(self, cache: cache).
|
54
|
+
serializer.new(self, cache: cache, root: root).serializable_hash
|
39
55
|
end
|
40
56
|
end
|
41
57
|
end
|
data/lib/barley/serializer.rb
CHANGED
@@ -5,29 +5,118 @@ module Barley
|
|
5
5
|
attr_accessor :object
|
6
6
|
|
7
7
|
class << self
|
8
|
+
# Defines attributes for the serializer
|
9
|
+
#
|
10
|
+
# Accepts either a list of symbols or a hash of symbols and Dry::Types, or a mix of both
|
11
|
+
#
|
12
|
+
# @example only symbols
|
13
|
+
# attributes :id, :name, :email
|
14
|
+
# # => {id: 1234, name: "John Doe", email: "john.doe@example"}
|
15
|
+
#
|
16
|
+
# @example with types
|
17
|
+
# attributes id: Types::Strict::Integer, name: Types::Strict::String, email: Types::Strict::String
|
18
|
+
# # => {id: 1234, name: "John Doe", email: "john.doe@example"}
|
19
|
+
#
|
20
|
+
# @example with types and symbols
|
21
|
+
# attributes :id, name: Types::Strict::String, email: Types::Strict::String
|
22
|
+
# # => {id: 1234, name: "John Doe", email: "john.doe@example"}
|
23
|
+
#
|
24
|
+
# @see Serializer#attribute
|
25
|
+
#
|
26
|
+
# @param keys [Hash<Symbol, Dry::Types>, Array<Symbol>] mix of symbols and hashes of symbols and Dry::Types
|
8
27
|
def attributes(*keys)
|
28
|
+
if keys.last.is_a?(Hash)
|
29
|
+
keys.pop.each do |key, type|
|
30
|
+
attribute(key, type: type)
|
31
|
+
end
|
32
|
+
end
|
9
33
|
keys.each do |key|
|
10
|
-
|
11
|
-
|
34
|
+
if key.is_a?(Hash)
|
35
|
+
attribute(key.keys.first, type: key.values.first)
|
36
|
+
else
|
37
|
+
attribute(key)
|
12
38
|
end
|
13
|
-
set_class_iv(:@defined_attributes, key)
|
14
39
|
end
|
15
40
|
end
|
16
41
|
|
17
|
-
|
42
|
+
# Defines a single attribute for the serializer
|
43
|
+
#
|
44
|
+
# Type checking is done with Dry::Types. If a type is not provided, the value is returned as is.
|
45
|
+
# Dry::Types can be used to coerce the value to the desired type and to check constraints.
|
46
|
+
#
|
47
|
+
# @see https://dry-rb.org/gems/dry-types/main/
|
48
|
+
#
|
49
|
+
# @raise [Dry::Types::ConstraintError] if the type does not match
|
50
|
+
#
|
51
|
+
# @example simple attribute
|
52
|
+
# attribute :id
|
53
|
+
# # => {id: 1234}
|
54
|
+
#
|
55
|
+
# @example attribute with a different key name
|
56
|
+
# attribute :name, key_name: :full_name
|
57
|
+
# # => {full_name: "John Doe"}
|
58
|
+
#
|
59
|
+
# @example attribute with a type
|
60
|
+
# attribute :email, type: Types::Strict::String
|
61
|
+
# # => {email: "john.doe@example"}
|
62
|
+
#
|
63
|
+
# @example attribute with a type and a block
|
64
|
+
# attribute :email, type: Types::Strict::String do
|
65
|
+
# object.email.upcase
|
66
|
+
# end
|
67
|
+
# # => {email: "JOHN.DOE@EXAMPLE"}
|
68
|
+
#
|
69
|
+
# @param key [Symbol] the attribute name
|
70
|
+
# @param key_name [Symbol] the key name in the hash
|
71
|
+
# @param type [Dry::Types] the type to use, or coerce the value to
|
72
|
+
# @param block [Proc] a block to use to compute the value
|
73
|
+
def attribute(key, key_name: nil, type: nil, &block)
|
18
74
|
key_name ||= key
|
19
75
|
if block
|
20
76
|
define_method(key_name) do
|
21
|
-
instance_eval(&block)
|
77
|
+
type.nil? ? instance_eval(&block) : type[instance_eval(&block)]
|
22
78
|
end
|
23
79
|
else
|
24
80
|
define_method(key_name) do
|
25
|
-
object.send(key)
|
81
|
+
type.nil? ? object.send(key) : type[object.send(key)]
|
26
82
|
end
|
27
83
|
end
|
28
84
|
set_class_iv(:@defined_attributes, key_name)
|
29
85
|
end
|
30
86
|
|
87
|
+
# Defines a single association for the serializer
|
88
|
+
#
|
89
|
+
# @example using the default serializer of the associated model
|
90
|
+
# one :group
|
91
|
+
# # => {group: {id: 1234, name: "Group 1"}}
|
92
|
+
#
|
93
|
+
# @example using a custom serializer
|
94
|
+
# one :group, serializer: MyCustomGroupSerializer
|
95
|
+
# # => {group: {id: 1234, name: "Group 1"}}
|
96
|
+
#
|
97
|
+
# @example using a block with an inline serializer definition
|
98
|
+
# one :group do
|
99
|
+
# attributes :id, :name
|
100
|
+
# end
|
101
|
+
# # => {group: {id: 1234, name: "Group 1"}}
|
102
|
+
#
|
103
|
+
# @example using a different key name
|
104
|
+
# one :group, key_name: :my_group
|
105
|
+
# # => {my_group: {id: 1234, name: "Group 1"}}
|
106
|
+
#
|
107
|
+
# @example using cache
|
108
|
+
# one :group, cache: true
|
109
|
+
# # => {group: {id: 1234, name: "Group 1"}}
|
110
|
+
#
|
111
|
+
# @example using cache and expires_in
|
112
|
+
# one :group, cache: {expires_in: 1.hour}
|
113
|
+
# # => {group: {id: 1234, name: "Group 1"}}
|
114
|
+
#
|
115
|
+
# @param key [Symbol] the association name
|
116
|
+
# @param key_name [Symbol] the key name in the hash
|
117
|
+
# @param serializer [Class] the serializer to use
|
118
|
+
# @param cache [Boolean, Hash<Symbol, ActiveSupport::Duration>] whether to cache the result, or a hash with options for the cache
|
119
|
+
# @param block [Proc] a block to use to define the serializer inline
|
31
120
|
def one(key, key_name: nil, serializer: nil, cache: false, &block)
|
32
121
|
key_name ||= key
|
33
122
|
if block
|
@@ -40,11 +129,44 @@ module Barley
|
|
40
129
|
return {} if element.nil?
|
41
130
|
|
42
131
|
el_serializer = serializer || element.serializer.class
|
43
|
-
el_serializer.new(element, cache: cache).
|
132
|
+
el_serializer.new(element, cache: cache).serializable_hash
|
44
133
|
end
|
45
134
|
set_class_iv(:@defined_attributes, key_name)
|
46
135
|
end
|
47
136
|
|
137
|
+
# Defines a collection association for the serializer
|
138
|
+
#
|
139
|
+
# @example using the default serializer of the associated model
|
140
|
+
# many :groups
|
141
|
+
# # => {groups: [{id: 1234, name: "Group 1"}, {id: 5678, name: "Group 2"}]}
|
142
|
+
#
|
143
|
+
# @example using a custom serializer
|
144
|
+
# many :groups, serializer: MyCustomGroupSerializer
|
145
|
+
# # => {groups: [{id: 1234, name: "Group 1"}, {id: 5678, name: "Group 2"}]}
|
146
|
+
#
|
147
|
+
# @example using a block with an inline serializer definition
|
148
|
+
# many :groups do
|
149
|
+
# attributes :id, :name
|
150
|
+
# end
|
151
|
+
# # => {groups: [{id: 1234, name: "Group 1"}, {id: 5678, name: "Group 2"}]}
|
152
|
+
#
|
153
|
+
# @example using a different key name
|
154
|
+
# many :groups, key_name: :my_groups
|
155
|
+
# # => {my_groups: [{id: 1234, name: "Group 1"}, {id: 5678, name: "Group 2"}]}
|
156
|
+
#
|
157
|
+
# @example using cache
|
158
|
+
# many :groups, cache: true
|
159
|
+
# # => {groups: [{id: 1234, name: "Group 1"}, {id: 5678, name: "Group 2"}]}
|
160
|
+
#
|
161
|
+
# @example using cache and expires_in
|
162
|
+
# many :groups, cache: {expires_in: 1.hour}
|
163
|
+
# # => {groups: [{id: 1234, name: "Group 1"}, {id: 5678, name: "Group 2"}]}
|
164
|
+
#
|
165
|
+
# @param key [Symbol] the association name
|
166
|
+
# @param key_name [Symbol] the key name in the hash
|
167
|
+
# @param serializer [Class] the serializer to use
|
168
|
+
# @param cache [Boolean, Hash<Symbol, ActiveSupport::Duration>] whether to cache the result, or a hash with options for the cache
|
169
|
+
# @param block [Proc] a block to use to define the serializer inline
|
48
170
|
def many(key, key_name: nil, serializer: nil, cache: false, &block)
|
49
171
|
key_name ||= key
|
50
172
|
if block
|
@@ -57,11 +179,17 @@ module Barley
|
|
57
179
|
return [] if elements.empty?
|
58
180
|
|
59
181
|
el_serializer = serializer || elements.first.serializer.class
|
60
|
-
elements.map { |element| el_serializer.new(element, cache: cache).
|
182
|
+
elements.map { |element| el_serializer.new(element, cache: cache).serializable_hash }.reject(&:blank?)
|
61
183
|
end
|
62
184
|
set_class_iv(:@defined_attributes, key_name)
|
63
185
|
end
|
64
186
|
|
187
|
+
# Either sets or appends a key to an instance variable
|
188
|
+
#
|
189
|
+
# @api private
|
190
|
+
#
|
191
|
+
# @param iv [Symbol] the instance variable to set
|
192
|
+
# @param key [Symbol] the key to add to the instance variable
|
65
193
|
def set_class_iv(iv, key)
|
66
194
|
instance_variable_defined?(iv) ? instance_variable_get(iv) << key : instance_variable_set(iv, [key])
|
67
195
|
end
|
@@ -69,10 +197,16 @@ module Barley
|
|
69
197
|
|
70
198
|
# @example with cache
|
71
199
|
# Barley::Serializer.new(object, cache: true)
|
200
|
+
#
|
72
201
|
# @example with cache and expires_in
|
73
202
|
# Barley::Serializer.new(object, cache: {expires_in: 1.hour})
|
74
|
-
|
203
|
+
#
|
204
|
+
# @param object [Object] the object to serialize
|
205
|
+
# @param cache [Boolean, Hash<Symbol, ActiveSupport::Duration>] a boolean to cache the result, or a hash with options for the cache
|
206
|
+
# @param root [Boolean] whether to include the root key in the hash
|
207
|
+
def initialize(object, cache: false, root: false)
|
75
208
|
@object = object
|
209
|
+
@root = root
|
76
210
|
@cache, @expires_in = if cache.is_a?(Hash)
|
77
211
|
[true, cache[:expires_in]]
|
78
212
|
else
|
@@ -80,39 +214,68 @@ module Barley
|
|
80
214
|
end
|
81
215
|
end
|
82
216
|
|
83
|
-
|
217
|
+
# Serializes the object
|
218
|
+
#
|
219
|
+
# @return [Hash] the serializable hash
|
220
|
+
def serializable_hash
|
84
221
|
if @cache
|
85
|
-
|
86
|
-
|
87
|
-
_as_json
|
222
|
+
Barley::Cache.fetch(cache_base_key, expires_in: @expires_in) do
|
223
|
+
_serializable_hash
|
88
224
|
end
|
89
225
|
else
|
90
|
-
|
226
|
+
_serializable_hash
|
91
227
|
end
|
92
228
|
end
|
93
229
|
|
230
|
+
# Clears the cache for the object
|
231
|
+
#
|
232
|
+
# @param key [String] the cache key
|
233
|
+
#
|
234
|
+
# @return [Boolean] whether the cache was cleared
|
94
235
|
def clear_cache(key: cache_base_key)
|
95
236
|
Barley::Cache.delete(key)
|
96
237
|
end
|
97
238
|
|
98
239
|
private
|
99
240
|
|
241
|
+
# @api private
|
242
|
+
#
|
243
|
+
# @return [String] the cache key
|
100
244
|
def cache_base_key
|
101
|
-
|
245
|
+
if object.updated_at.present?
|
246
|
+
"#{object.class.name&.underscore}/#{object.id}/#{object.updated_at&.to_i}/barley_cache/"
|
247
|
+
else
|
248
|
+
"#{object.class.name&.underscore}/#{object.id}/barley_cache/"
|
249
|
+
end
|
102
250
|
end
|
103
251
|
|
252
|
+
# @api private
|
253
|
+
#
|
254
|
+
# @return [Array<Symbol>] the defined attributes
|
104
255
|
def defined_attributes
|
105
256
|
self.class.instance_variable_get(:@defined_attributes)
|
106
257
|
end
|
107
258
|
|
108
|
-
|
259
|
+
# Serializes the object
|
260
|
+
#
|
261
|
+
# @api private
|
262
|
+
#
|
263
|
+
# @return [Hash] the serializable hash
|
264
|
+
def _serializable_hash
|
109
265
|
hash = {}
|
110
266
|
|
111
267
|
defined_attributes.each do |key|
|
112
268
|
hash[key] = send(key)
|
113
269
|
end
|
114
270
|
|
115
|
-
hash
|
271
|
+
@root ? {root_key => hash} : hash
|
272
|
+
end
|
273
|
+
|
274
|
+
# @api private
|
275
|
+
#
|
276
|
+
# @return [Symbol] the root key, based on the class name
|
277
|
+
def root_key
|
278
|
+
object.class.name.underscore.to_sym
|
116
279
|
end
|
117
280
|
end
|
118
281
|
end
|
data/lib/barley/version.rb
CHANGED
data/lib/barley.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: barley
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cedric Delalande
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-10-
|
11
|
+
date: 2023-10-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 6.1.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dry-types
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.7.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.7.1
|
27
41
|
description: Cerealize your ActiveModel objects into flat JSON objects with a dead
|
28
42
|
simple DSL. Our daily bread is to make your API faster.
|
29
43
|
email:
|