api_maker 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +476 -0
  4. data/Rakefile +27 -0
  5. data/app/channels/api_maker/subscriptions_channel.rb +80 -0
  6. data/app/controllers/api_maker/base_controller.rb +32 -0
  7. data/app/controllers/api_maker/commands_controller.rb +26 -0
  8. data/app/controllers/api_maker/devise_controller.rb +60 -0
  9. data/app/controllers/api_maker/session_statuses_controller.rb +33 -0
  10. data/app/services/api_maker/application_service.rb +7 -0
  11. data/app/services/api_maker/collection_command_service.rb +24 -0
  12. data/app/services/api_maker/command_response.rb +67 -0
  13. data/app/services/api_maker/command_service.rb +31 -0
  14. data/app/services/api_maker/create_command.rb +62 -0
  15. data/app/services/api_maker/create_command_service.rb +18 -0
  16. data/app/services/api_maker/destroy_command.rb +39 -0
  17. data/app/services/api_maker/destroy_command_service.rb +22 -0
  18. data/app/services/api_maker/generate_react_native_api_service.rb +61 -0
  19. data/app/services/api_maker/index_command.rb +96 -0
  20. data/app/services/api_maker/index_command_service.rb +22 -0
  21. data/app/services/api_maker/js_method_namer_service.rb +11 -0
  22. data/app/services/api_maker/member_command_service.rb +25 -0
  23. data/app/services/api_maker/model_content_generator_service.rb +108 -0
  24. data/app/services/api_maker/models_finder_service.rb +22 -0
  25. data/app/services/api_maker/models_generator_service.rb +104 -0
  26. data/app/services/api_maker/update_command.rb +43 -0
  27. data/app/services/api_maker/update_command_service.rb +21 -0
  28. data/app/services/api_maker/valid_command.rb +35 -0
  29. data/app/services/api_maker/valid_command_service.rb +21 -0
  30. data/app/views/api_maker/_data.html.erb +15 -0
  31. data/config/rails_best_practices.yml +55 -0
  32. data/config/routes.rb +7 -0
  33. data/lib/api_maker.rb +36 -0
  34. data/lib/api_maker/ability.rb +39 -0
  35. data/lib/api_maker/ability_loader.rb +21 -0
  36. data/lib/api_maker/action_controller_base_extensions.rb +5 -0
  37. data/lib/api_maker/base_command.rb +81 -0
  38. data/lib/api_maker/base_resource.rb +78 -0
  39. data/lib/api_maker/collection_serializer.rb +69 -0
  40. data/lib/api_maker/command_spec_helper.rb +57 -0
  41. data/lib/api_maker/configuration.rb +34 -0
  42. data/lib/api_maker/engine.rb +5 -0
  43. data/lib/api_maker/individual_command.rb +37 -0
  44. data/lib/api_maker/javascript/api.js +92 -0
  45. data/lib/api_maker/javascript/base-model.js +543 -0
  46. data/lib/api_maker/javascript/bootstrap/attribute-row.jsx +16 -0
  47. data/lib/api_maker/javascript/bootstrap/attribute-rows.jsx +47 -0
  48. data/lib/api_maker/javascript/bootstrap/card.jsx +79 -0
  49. data/lib/api_maker/javascript/bootstrap/checkbox.jsx +127 -0
  50. data/lib/api_maker/javascript/bootstrap/checkboxes.jsx +105 -0
  51. data/lib/api_maker/javascript/bootstrap/live-table.jsx +168 -0
  52. data/lib/api_maker/javascript/bootstrap/money-input.jsx +136 -0
  53. data/lib/api_maker/javascript/bootstrap/radio-buttons.jsx +80 -0
  54. data/lib/api_maker/javascript/bootstrap/select.jsx +168 -0
  55. data/lib/api_maker/javascript/bootstrap/string-input.jsx +203 -0
  56. data/lib/api_maker/javascript/cable-connection-pool.js +169 -0
  57. data/lib/api_maker/javascript/cable-subscription-pool.js +111 -0
  58. data/lib/api_maker/javascript/cable-subscription.js +33 -0
  59. data/lib/api_maker/javascript/collection.js +186 -0
  60. data/lib/api_maker/javascript/commands-pool.js +123 -0
  61. data/lib/api_maker/javascript/custom-error.js +14 -0
  62. data/lib/api_maker/javascript/deserializer.js +35 -0
  63. data/lib/api_maker/javascript/devise.js.erb +113 -0
  64. data/lib/api_maker/javascript/error-logger.js +119 -0
  65. data/lib/api_maker/javascript/event-connection.jsx +24 -0
  66. data/lib/api_maker/javascript/event-created.jsx +26 -0
  67. data/lib/api_maker/javascript/event-destroyed.jsx +26 -0
  68. data/lib/api_maker/javascript/event-emitter-listener.jsx +32 -0
  69. data/lib/api_maker/javascript/event-listener.jsx +41 -0
  70. data/lib/api_maker/javascript/event-updated.jsx +26 -0
  71. data/lib/api_maker/javascript/form-data-to-object.js +70 -0
  72. data/lib/api_maker/javascript/included.js +39 -0
  73. data/lib/api_maker/javascript/key-value-store.js +47 -0
  74. data/lib/api_maker/javascript/logger.js +23 -0
  75. data/lib/api_maker/javascript/model-name.js +21 -0
  76. data/lib/api_maker/javascript/model-template.js.erb +110 -0
  77. data/lib/api_maker/javascript/models-response-reader.js +43 -0
  78. data/lib/api_maker/javascript/paginate.jsx +128 -0
  79. data/lib/api_maker/javascript/params.js +68 -0
  80. data/lib/api_maker/javascript/resource-route.jsx +75 -0
  81. data/lib/api_maker/javascript/resource-routes.jsx +36 -0
  82. data/lib/api_maker/javascript/result.js +25 -0
  83. data/lib/api_maker/javascript/session-status-updater.js +113 -0
  84. data/lib/api_maker/javascript/sort-link.jsx +88 -0
  85. data/lib/api_maker/javascript/updated-attribute.jsx +60 -0
  86. data/lib/api_maker/loader.rb +14 -0
  87. data/lib/api_maker/memory_storage.rb +65 -0
  88. data/lib/api_maker/model_extensions.rb +96 -0
  89. data/lib/api_maker/permitted_params_argument.rb +12 -0
  90. data/lib/api_maker/preloader.rb +91 -0
  91. data/lib/api_maker/preloader_belongs_to.rb +58 -0
  92. data/lib/api_maker/preloader_has_many.rb +69 -0
  93. data/lib/api_maker/preloader_has_one.rb +70 -0
  94. data/lib/api_maker/preloader_through.rb +101 -0
  95. data/lib/api_maker/railtie.rb +14 -0
  96. data/lib/api_maker/relationship_includer.rb +42 -0
  97. data/lib/api_maker/resource_routing.rb +8 -0
  98. data/lib/api_maker/result_parser.rb +50 -0
  99. data/lib/api_maker/serializer.rb +86 -0
  100. data/lib/api_maker/spec_helper.rb +100 -0
  101. data/lib/api_maker/version.rb +3 -0
  102. data/lib/tasks/api_maker_tasks.rake +5 -0
  103. metadata +581 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7d65964fbfeb5bd8a5d89702083930f77e3d9c46956695c4f0877039a43c3010
4
+ data.tar.gz: 2a9431af5f0bfebcfb9605d85fafc40b824448aeac9ccd5a0f595b8e9f6d642d
5
+ SHA512:
6
+ metadata.gz: bf2858ef950edf1aa8cc9ce45938ebd1b5ea5f3dd1f11223fd42959bff910a5cceae21599abbfdce7dbabedcdcd481c54c832c77712417436d02f423285fb48d
7
+ data.tar.gz: 7f63b82811ef1a5207d2ecbdcc62072b4d4297263884de1c4b36704e7b114a2d027cbbb3dac324de69af6c19fc79f185f273b4bf070def5f7c9282e1b1caa404
@@ -0,0 +1,20 @@
1
+ Copyright 2018 kjabtion
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,476 @@
1
+ # ApiMaker
2
+
3
+ Generates Rails API endpoints and JavaScript API files for Webpack and more by inspecting your models and serializers.
4
+
5
+ ## Installation
6
+ Add this line to your application's Gemfile:
7
+
8
+ ```ruby
9
+ gem "api_maker"
10
+ ```
11
+
12
+ ApiMaker requires [Webpacker](https://github.com/rails/webpacker), so make sure you have that set up as well. It also uses an extension called [qs](https://www.npmjs.com/package/qs), that you should add to your packages, but that is probally already there by default.
13
+
14
+ ApiMaker makes use of [CanCanCan](https://github.com/CanCanCommunity/cancancan) to keep track of what models a given user should have access to. Each resource defines its own abilities under `app/api_maker/resources/user_resource` like this:
15
+ ```ruby
16
+ class Resources::UserResource < Resources::ApplicationResource
17
+ def abilities
18
+ can :update, User if current_user&.admin?
19
+ can :update, User, id: current_user&.id if current_user.present?
20
+ can :read, User
21
+ end
22
+ end
23
+ ```
24
+
25
+ Add an `api_maker_args` method to your application controller. This controls what arguments will be passed to the CanCan ability and the serializers:
26
+ ```ruby
27
+ class ApplicationController
28
+ private
29
+
30
+ def api_maker_args
31
+ @api_maker_args ||= {current_user: current_user}
32
+ end
33
+ end
34
+ ```
35
+
36
+ Insert this mount into `config/routes.rb`:
37
+ ```ruby
38
+ Rails.application.routes.draw do
39
+ mount ApiMaker::Engine => "/api_maker"
40
+ end
41
+ ```
42
+
43
+ ApiMaker will only create models, endpoints and serializers for ActiveRecord models that are defined as resources. So be sure to add resources under `app/api_maker/resources` for your models first. You can add some helper methods if you want to use in your resources like `current_user` and `signed_in_as_admin?`.
44
+ ```ruby
45
+ class Resources::ApplicationResource < ApiMaker::BaseResource
46
+ def current_user
47
+ args&.dig(:current_user)
48
+ end
49
+
50
+ def signed_in_as_admin?
51
+ current_user&.role == "admin"
52
+ end
53
+ end
54
+ ```
55
+
56
+ ```ruby
57
+ class Resources::UserResources < Resources::ApplicationResource
58
+ attributes :id, :email, :custom_attribute
59
+ attributes :calculated_attribute, selected_by_default: false
60
+ attributes :secret_attribute, if: :signed_in_as_admin?
61
+ collection_commands :count_users
62
+ member_commands :calculate_age
63
+ relationships :account, :tasks
64
+
65
+ def custom_attribute
66
+ "Hello world! Current user is: #{args.fetch(:current_user).email}"
67
+ end
68
+ end
69
+ ```
70
+
71
+ You should also create an application command here: `app/api_maker/commands/application_command` with content like this:
72
+ ```ruby
73
+ class Commands::ApplicationCommand < ApiMaker::BaseCommand
74
+ end
75
+ ```
76
+
77
+ Add this to your application model:
78
+ ```ruby
79
+ class ApplicationRecord < ActiveRecord::Base
80
+ include ApiMaker::ModelExtensions
81
+ end
82
+ ```
83
+
84
+ ApiMaker uses that to keep track of what attributes, relationships and commands you want exposed through the API.
85
+
86
+ Its now time to generate everything like this:
87
+ ```bash
88
+ rake api_maker:generate_models
89
+ ```
90
+
91
+ If you want to be able to create and update models, then you should go into each resource and create a params method to define, which attributes can be written on each model like this:
92
+ ```ruby
93
+ class Resources::TaskResource < ApiMaker::ModelController
94
+ def permitted_params(arg)
95
+ arg.params.require(:project).permit(:name)
96
+ end
97
+ end
98
+ ```
99
+
100
+ ### I18n
101
+
102
+ In order to use the built in text support, you need to add `i18n-js` to your project.
103
+
104
+ Start by adding to your Gemfile:
105
+ ```ruby
106
+ gem "i18n-js"
107
+ ```
108
+
109
+ Then add `config/i18n-js.yml`:
110
+ ```yml
111
+ translations:
112
+ - file: "app/assets/javascripts/i18n/translations.js"
113
+ only: ["*.activerecord.attributes.*", "*.activerecord.models.*", "*.date.*", "*.js.*", "*.number.currency.*", "*.time.*"]
114
+ ```
115
+
116
+ Then add this to `app/assets/javascript/application.js.erb`:
117
+ ```js
118
+ //= require i18n
119
+ //= require i18n/translations
120
+
121
+ var locale = document.querySelector("html").getAttribute("lang")
122
+ I18n.locale = locale
123
+
124
+ <% if Rails.env.development? || Rails.env.test? %>
125
+ I18n.missingTranslation = function(key) {
126
+ console.error(`No translation for: ${key}`)
127
+ return `translation missing: ${key}`
128
+ }
129
+ <% end %>
130
+ ```
131
+
132
+ Add this to the `<html>`-tag:
133
+ ```html
134
+ <html lang="<%= I18n.locale %>">
135
+ ```
136
+
137
+ Add this to `config/application.rb` to ease development:
138
+ ```ruby
139
+ config.middleware.use I18n::JS::Middleware
140
+ ```
141
+
142
+ ### ActionCable
143
+
144
+ Your `connection.rb` should look something like this:
145
+ ```rb
146
+ class ApplicationCable::Connection < ActionCable::Connection::Base
147
+ identified_by :current_user
148
+
149
+ def connect
150
+ self.current_user = find_verified_user
151
+ end
152
+
153
+ private
154
+
155
+ def find_verified_user
156
+ verified_user = User.find_by(id: cookies.signed["user.id"])
157
+
158
+ if verified_user && cookies.signed["user.expires_at"] > Time.zone.now
159
+ verified_user
160
+ else
161
+ reject_unauthorized_connection
162
+ end
163
+ end
164
+ end
165
+ ```
166
+
167
+ Your `channel.rb` should look something like this:
168
+ ```rb
169
+ class ApplicationCable::Channel < ActionCable::Channel::Base
170
+ private # rubocop:disable Layout/IndentationWidth
171
+
172
+ def current_ability
173
+ @current_ability ||= ApiMakerAbility.for_user(current_user)
174
+ end
175
+
176
+ def current_user
177
+ @current_user ||= env["warden"].user
178
+ end
179
+ end
180
+ ```
181
+
182
+ ## Usage
183
+
184
+ ### Creating a new model from JavaScript
185
+
186
+ ```js
187
+ import Task from "api-maker/models/task"
188
+
189
+ var task = new Task()
190
+ task.assignAttributes({name: "New task"})
191
+ task.create().then(status => {
192
+ if (status.success) {
193
+ console.log(`Task was created with ID: ${task.id()}`)
194
+ } else {
195
+ console.log("Task wasnt created")
196
+ }
197
+ })
198
+ ```
199
+
200
+ ### Finding an existing model
201
+
202
+ ```js
203
+ Task.find(5).then(task => {
204
+ console.log(`Task found: ${task.name()}`)
205
+ })
206
+ ```
207
+
208
+ ### Updating a model
209
+
210
+ ```js
211
+ task.assignAttributes({name: "New name"})
212
+ task.save().then(status => {
213
+ if (status.success) {
214
+ console.log(`Task was updated and name is now: ${task.name()}`)
215
+ } else {
216
+ console.log("Task wasnt updated")
217
+ }
218
+ })
219
+ ```
220
+
221
+ ```js
222
+ task.update({name: "New name"}).then(status => {
223
+ if (status.success) {
224
+ console.log(`Task was updated and name is now: ${task.name()}`)
225
+ } else {
226
+ console.log("Task wasnt updated")
227
+ }
228
+ })
229
+ ```
230
+
231
+ ### Deleting a model
232
+
233
+ ```js
234
+ task.destroy().then(status => {
235
+ if (status.success) {
236
+ console.log("Task was destroyed")
237
+ } else {
238
+ console.log("Task wasnt destroyed")
239
+ }
240
+ })
241
+ ```
242
+
243
+ ### Preloading models
244
+
245
+ ```js
246
+ Task.ransack().preload("project.customer").toArray().then(tasks => {
247
+ for(var task of tasks) {
248
+ console.log(`Project of task ${task.id()}: ${task.project().name()}`)
249
+ console.log(`Customer of task ${task.id()}: ${task.project().customer().name()}`)
250
+ }
251
+ })
252
+ ```
253
+
254
+ ### Query models
255
+
256
+ ApiModels uses [Ransack](https://github.com/activerecord-hackery/ransack) to expose a huge amount of options to query data.
257
+
258
+ ```js
259
+ Task.ransack({name_cont: "something"}).toArray().then(tasks => {
260
+ console.log(`Found: ${tasks.length} tasks`)
261
+ })
262
+ ```
263
+
264
+ Distinct:
265
+ ```js
266
+ var tasks = await Task.ransack({relationships_something_eq: "something"}).distinct().toArray()
267
+ ```
268
+
269
+ ### Selecting only specific attributes
270
+
271
+ ```js
272
+ Task.ransack().select({Task: ["id", "name"]}).toArray().then(tasks => this.setState({tasks}))
273
+ ```
274
+
275
+ ### Sorting models
276
+
277
+ ```js
278
+ Task.ransack({s: "id desc"})
279
+ ```
280
+
281
+ ### Attributes
282
+
283
+ Each attribute is defined as a method on each model. So if you have an attribute called `name` on the `Task`-model, then it be read by doing this: `task.name()`.
284
+
285
+ ### Relationships
286
+
287
+ #### Has many
288
+
289
+ A `has many` relationship will return a collection the queries the sub models.
290
+
291
+ ```js
292
+ project.tasks().toArray().then(tasks => {
293
+ console.log(`Project ${project.id()} has ${tasks.length} tasks`)
294
+
295
+ for(var key in tasks) {
296
+ var task = tasks[key]
297
+ console.log(`Task ${task.id()} is named: ${task.name()}`)
298
+ }
299
+ })
300
+ ```
301
+
302
+ #### Belongs to
303
+
304
+ A `belongs to` relationship will return a promise that will get that model:
305
+
306
+ ```js
307
+ task.project().then(project => {
308
+ console.log(`Task ${task.id()} belongs to a project called: ${project.name()}`)
309
+ })
310
+ ```
311
+
312
+ #### Has one
313
+
314
+ A `has one` relationship will also return a promise that will get that model like a `belongs to` relationship.
315
+
316
+ #### Getting the current user
317
+
318
+ First include this in your layout, so JS can know which user is signed in:
319
+ ```erb
320
+ <body>
321
+ <%= render "/api_maker/data" %>
322
+ ```
323
+
324
+ Then you can do like this in JS:
325
+ ```js
326
+ import Devise from "api-maker/devise"
327
+
328
+ Devise.currentUser().then(user => {
329
+ console.log(`The current user has this email: ${user.email()}`)
330
+ })
331
+ ```
332
+
333
+ ## Events from the backend
334
+
335
+ ### Custom events
336
+
337
+ Add the relevant access to your abilities:
338
+
339
+ ```ruby
340
+ class ApiMakerAbility < ApplicationAbility
341
+ def initialize(args:)
342
+ can :event_new_message, User, id: 5
343
+ end
344
+ end
345
+ ```
346
+
347
+ ```ruby
348
+ user = User.find(5)
349
+ user.api_maker_event("new_message", message: "Hello world")
350
+ ```
351
+
352
+ ```js
353
+ User.find(5).then(user => {
354
+ user.connect("new_message", args => {
355
+ console.log(`New message: ${args.message}`)
356
+ })
357
+ })
358
+ ```
359
+
360
+ ### Update models
361
+
362
+ Add this to your abilities:
363
+ ```ruby
364
+ class ApiMakerAbility < ApplicationAbility
365
+ def initialize(args:)
366
+ can [:create_events, :destroy_events, :update_events], User, id: 5
367
+ end
368
+ end
369
+ ```
370
+
371
+ Add this to the model you want to broadcast updates:
372
+ ```ruby
373
+ class User < ApplicationRecord
374
+ api_maker_broadcast_creates
375
+ api_maker_broadcast_destroys
376
+ api_maker_broadcast_updates
377
+ end
378
+ ```
379
+
380
+ ```js
381
+ User.find(5).then(user => {
382
+ let subscription = user.connectUpdated(args => {
383
+ console.log(`Model was updated: ${args.model.id()}`)
384
+ })
385
+ })
386
+ ```
387
+
388
+ Remember to unsubscrube again:
389
+ ```js
390
+ subscription.unsubscribe()
391
+ ```
392
+
393
+ You can also use a React component if you use React and dont want to keep track of when to unsubscribe:
394
+ ```jsx
395
+ import EventUpdated from "api-maker/event-created"
396
+ import EventUpdated from "api-maker/event-destroyed"
397
+ import EventUpdated from "api-maker/event-updated"
398
+ ```
399
+
400
+ ```jsx
401
+ <EventCreated modelClass={User} onCreated={(args) => this.onUserCreated(args)} />
402
+ <EventDestroyed model={user} onDestroyed={(args) => this.onUserDestroyed(args)} />
403
+ <EventUpdated model={user} onUpdated={(args) => this.onUserUpdated(args)} />
404
+ ```
405
+
406
+ ```jsx
407
+ onUserCreated(args) {
408
+ this.setState({user: args.model})
409
+ }
410
+
411
+ onUserDestroyed(args) {
412
+ this.setState({user: args.model})
413
+ }
414
+
415
+ onUserUpdated(args) {
416
+ this.setState({user: args.model})
417
+ }
418
+ ```
419
+
420
+ You can also use this React component to show a models attribute with automatic updates:
421
+
422
+ ```jsx
423
+ import UpdatedAttribute from "api-maker/updated-attribute"
424
+ ```
425
+
426
+ ```jsx
427
+ <UpdatedAttribute model={user} attribute="email" />
428
+ ```
429
+
430
+ You can also use the `EventConnection` React component so you don't need to keep track of your subscription and unsubscribe:
431
+ ```jsx
432
+ import EventConnection from "api-maker/event-connection"
433
+ ```
434
+
435
+ ```jsx
436
+ <EventConnection model={this.state.user} event="eventName" onCall={(data) => this.onEvent(data)} />
437
+ ```
438
+
439
+ ## Serializing
440
+
441
+ ### Conditional attributes
442
+
443
+ This will only include the email for users, if the current user signed in is an admin.
444
+
445
+ ```ruby
446
+ class Resources::UserResource < Resources::ApplicationResource
447
+ attributes :id
448
+ attributes :email, if: :signed_in_as_admin?
449
+
450
+ private
451
+
452
+ def signed_in_as_admin?
453
+ args[:current_user]&.admin?
454
+ end
455
+ end
456
+ ```
457
+
458
+
459
+ ## Reporting errors
460
+
461
+ Add an intializer with something like this:
462
+
463
+ ```ruby
464
+ ApiMaker::Configuration.configure do |config|
465
+ config.on_error do |controller:, error:|
466
+ ExceptionNotifier.notify_exception(error, env: controller&.request&.env)
467
+ end
468
+ end
469
+ ```
470
+
471
+
472
+ ## Contributing
473
+ Contribution directions go here.
474
+
475
+ ## License
476
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).