mortymer 0.0.2
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Guardfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +167 -0
- data/Rakefile +12 -0
- data/config.ru +5 -0
- data/docs/.vitepress/config.mts +73 -0
- data/docs/.vitepress/theme/Layout.vue +21 -0
- data/docs/.vitepress/theme/index.ts +5 -0
- data/docs/.vitepress/theme/style.css +5 -0
- data/docs/advanced/dependency-injection.md +21 -0
- data/docs/advanced/openapi.md +16 -0
- data/docs/guide/api-metadata.md +142 -0
- data/docs/guide/introduction.md +31 -0
- data/docs/guide/models.md +16 -0
- data/docs/guide/quick-start.md +190 -0
- data/docs/index.md +26 -0
- data/lib/mortymer/api_metadata.rb +79 -0
- data/lib/mortymer/container.rb +122 -0
- data/lib/mortymer/dependencies_dsl.rb +65 -0
- data/lib/mortymer/dry_swagger.rb +10 -0
- data/lib/mortymer/endpoint.rb +139 -0
- data/lib/mortymer/endpoint_registry.rb +16 -0
- data/lib/mortymer/model.rb +8 -0
- data/lib/mortymer/openapi_generator.rb +76 -0
- data/lib/mortymer/rails/configuration.rb +16 -0
- data/lib/mortymer/rails/endpoint_wrapper_controller.rb +44 -0
- data/lib/mortymer/rails/routes.rb +62 -0
- data/lib/mortymer/rails.rb +19 -0
- data/lib/mortymer/railtie.rb +14 -0
- data/lib/mortymer/utils/string_transformations.rb +28 -0
- data/lib/mortymer/version.rb +5 -0
- data/lib/mortymer.rb +15 -0
- data/lib/mortymermer.rb +15 -0
- data/mortymer.gemspec +45 -0
- data/package-lock.json +2429 -0
- data/package.json +13 -0
- metadata +172 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "endpoint_registry"
|
4
|
+
require_relative "utils/string_transformations"
|
5
|
+
|
6
|
+
module Mortymer
|
7
|
+
# Generate an openapi doc based on the registered endpoints
|
8
|
+
class OpenapiGenerator
|
9
|
+
include Utils::StringTransformations
|
10
|
+
|
11
|
+
def initialize(prefix: "", title: "Rick on Rails API", version: "v1", description: "", registry: [])
|
12
|
+
@prefix = prefix
|
13
|
+
@title = title
|
14
|
+
@version = version
|
15
|
+
@description = description
|
16
|
+
@endpoints_registry = registry
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate
|
20
|
+
{
|
21
|
+
openapi: "3.0.1",
|
22
|
+
info: { title: @title, version: @version, description: @description },
|
23
|
+
paths: generate_paths,
|
24
|
+
components: { schemas: generate_schemas }
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def generate_paths
|
31
|
+
@endpoints_registry.each_with_object({}) do |endpoint, paths|
|
32
|
+
next unless endpoint.routeable?
|
33
|
+
|
34
|
+
schema = endpoint.generate_openapi_schema || {}
|
35
|
+
schema = schema.transform_keys { |k| @prefix + k }
|
36
|
+
paths.merge!(schema)
|
37
|
+
end || {}
|
38
|
+
end
|
39
|
+
|
40
|
+
def generate_schemas # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
41
|
+
schemas = {
|
42
|
+
"Error422" => {
|
43
|
+
type: "object",
|
44
|
+
required: %w[error details],
|
45
|
+
properties: {
|
46
|
+
error: {
|
47
|
+
type: "string",
|
48
|
+
description: "Error type identifier",
|
49
|
+
example: "Validation Failed"
|
50
|
+
},
|
51
|
+
details: {
|
52
|
+
type: "string",
|
53
|
+
description: "Detailed error message",
|
54
|
+
example: 'type_error: ["foo"] is not a valid Integer'
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
@endpoints_registry.each do |endpoint|
|
61
|
+
next unless endpoint.routeable?
|
62
|
+
|
63
|
+
if endpoint.input_class && !schemas.key?(demodulize(endpoint.input_class.name))
|
64
|
+
schemas[demodulize(endpoint.input_class.name)] =
|
65
|
+
Dry::Swagger::DocumentationGenerator.new.from_struct(endpoint.input_class)
|
66
|
+
end
|
67
|
+
|
68
|
+
if endpoint.output_class && !schemas.key?(demodulize(endpoint.output_class.name))
|
69
|
+
schemas[demodulize(endpoint.output_class.name)] =
|
70
|
+
Dry::Swagger::DocumentationGenerator.new.from_struct(endpoint.output_class)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
schemas
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mortymer
|
4
|
+
module Rails
|
5
|
+
# A configuration api for Rails & Morty integration
|
6
|
+
class Configuration
|
7
|
+
attr_accessor :base_controller_class, :error_handler, :wrap_methods
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@base_controller_class = "ApplicationController"
|
11
|
+
@error_handler = ->(error) { raise error }
|
12
|
+
@wrap_methods = true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mortymer
|
4
|
+
module Rails
|
5
|
+
# This controller acts as a wrapper for Morty endpoints, handling param conversion
|
6
|
+
# and response rendering
|
7
|
+
class EndpointWrapperController < ::ActionController::API
|
8
|
+
rescue_from Dry::Struct::Error, with: :handle_invalid_params
|
9
|
+
rescue_from Dry::Types::CoercionError, with: :handle_invalid_params
|
10
|
+
|
11
|
+
# This method should be used when inheriting from a Controller
|
12
|
+
def execute
|
13
|
+
endpoint_class = request.env["morty.endpoint_class"]
|
14
|
+
dispatch_action_to(self, endpoint_class.action, endpoint_class.input_class)
|
15
|
+
end
|
16
|
+
|
17
|
+
def handle
|
18
|
+
endpoint_class = request.env["morty.endpoint_class"]
|
19
|
+
controller_class = endpoint_class.controller_class
|
20
|
+
handler = controller_class.new
|
21
|
+
dispatch_action_to(handler, endpoint_class.action, endpoint_class.input_class)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def action_params
|
27
|
+
p = params.to_unsafe_h.to_h.deep_transform_keys(&:to_sym)
|
28
|
+
p.delete(:controller)
|
29
|
+
p.delete(:actioncontroller)
|
30
|
+
p
|
31
|
+
end
|
32
|
+
|
33
|
+
def dispatch_action_to(controller, method, input_class)
|
34
|
+
input = input_class.new(action_params)
|
35
|
+
output = controller.send(method, input)
|
36
|
+
render json: output.to_h, status: :ok
|
37
|
+
end
|
38
|
+
|
39
|
+
def handle_invalid_params(error)
|
40
|
+
render json: { error: "Validation Failed", details: error.message }, status: :unprocessable_entity
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mortymer
|
4
|
+
module Rails
|
5
|
+
# This module is in charge of making each registered Morty Endpoint
|
6
|
+
# routeable through rails
|
7
|
+
class Routes
|
8
|
+
def initialize(drawer)
|
9
|
+
@drawer = drawer
|
10
|
+
end
|
11
|
+
|
12
|
+
def mount_morty_endpoints
|
13
|
+
Morty::EndpointRegistry.registry.each do |endpoint|
|
14
|
+
mount_endpoint(endpoint)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def mount_controllers
|
19
|
+
Morty::EndpointRegistry.registry.each do |endpoint|
|
20
|
+
mount_controller(endpoint)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def mount_controller(endpoint)
|
27
|
+
return unless endpoint.routeable?
|
28
|
+
|
29
|
+
@drawer.send(
|
30
|
+
endpoint.http_method,
|
31
|
+
endpoint.path,
|
32
|
+
to: "#{endpoint.controller_name.gsub(/_controller$/, "")}##{endpoint.action}",
|
33
|
+
as: "#{endpoint.http_method}_#{endpoint.api_name}"
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def mount_endpoint(endpoint)
|
38
|
+
return unless endpoint.routeable?
|
39
|
+
|
40
|
+
path = endpoint.path || endpoint.infer_path_from_class
|
41
|
+
|
42
|
+
# Store the endpoint class in the request env for the wrapper to access
|
43
|
+
defaults = { controller: "morty/rails/endpoint_wrapper", action: "handle" }
|
44
|
+
constraints = lambda do |request|
|
45
|
+
request.env["morty.endpoint_class"] = endpoint
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
case endpoint.http_method
|
50
|
+
when :get
|
51
|
+
@drawer.get path, to: "morty/rails/endpoint_wrapper#handle", defaults: defaults, constraints: constraints
|
52
|
+
when :post
|
53
|
+
@drawer.post path, to: "morty/rails/endpoint_wrapper#handle", defaults: defaults, constraints: constraints
|
54
|
+
when :put
|
55
|
+
@drawer.put path, to: "morty/rails/endpoint_wrapper#handle", defaults: defaults, constraints: constraints
|
56
|
+
when :delete
|
57
|
+
@drawer.delete path, to: "morty/rails/endpoint_wrapper#handle", defaults: defaults, constraints: constraints
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "morty/rails/configuration"
|
4
|
+
require "morty/rails/endpoint_wrapper_controller"
|
5
|
+
require "morty/rails/routes"
|
6
|
+
|
7
|
+
module Mortymer
|
8
|
+
module Rails
|
9
|
+
class << self
|
10
|
+
def configure
|
11
|
+
yield(configuration)
|
12
|
+
end
|
13
|
+
|
14
|
+
def configuration
|
15
|
+
@configuration ||= Configuration.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rails/routes"
|
4
|
+
require_relative "rails/configuration"
|
5
|
+
require_relative "rails/endpoint_wrapper_controller"
|
6
|
+
|
7
|
+
module Mortymer
|
8
|
+
class Railtie < ::Rails::Railtie
|
9
|
+
config.morty = Mortymer::Rails::Configuration.new
|
10
|
+
|
11
|
+
initializer "mortymer.initialize" do |app|
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mortymer
|
4
|
+
module Utils
|
5
|
+
# Provides string transformation utilities similar to ActiveSupport's methods
|
6
|
+
module StringTransformations
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def underscore(camel_cased_word)
|
10
|
+
return camel_cased_word unless camel_cased_word.is_a?(String)
|
11
|
+
|
12
|
+
word = camel_cased_word.to_s.dup
|
13
|
+
word.gsub!(/::/, "/")
|
14
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
15
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
16
|
+
word.tr!("-", "_")
|
17
|
+
word.downcase!
|
18
|
+
word
|
19
|
+
end
|
20
|
+
|
21
|
+
def demodulize(path)
|
22
|
+
return path unless path.is_a?(String)
|
23
|
+
|
24
|
+
path.split("::").last
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/mortymer.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
require "dry/struct"
|
5
|
+
require "mortymer/model"
|
6
|
+
require "mortymer/endpoint"
|
7
|
+
require "mortymer/dry_swagger"
|
8
|
+
require "mortymer/endpoint_registry"
|
9
|
+
require "mortymer/utils/string_transformations"
|
10
|
+
require "mortymer/api_metadata"
|
11
|
+
require "mortymer/openapi_generator"
|
12
|
+
require "mortymer/container"
|
13
|
+
require "mortymer/dependencies_dsl"
|
14
|
+
require "mortymer/rails" if defined?(Rails)
|
15
|
+
require "mortymer/railtie" if defined?(Rails::Railtie)
|
data/lib/mortymermer.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
require "dry/struct"
|
5
|
+
require "mortymer/model"
|
6
|
+
require "mortymer/endpoint"
|
7
|
+
require "mortymer/dry_swagger"
|
8
|
+
require "mortymer/endpoint_registry"
|
9
|
+
require "mortymer/utils/string_transformations"
|
10
|
+
require "mortymer/api_metadata"
|
11
|
+
require "mortymer/openapi_generator"
|
12
|
+
require "mortymer/container"
|
13
|
+
require "mortymer/dependencies_dsl"
|
14
|
+
require "mortymer/rails" if defined?(Rails)
|
15
|
+
require "mortymer/railtie" if defined?(Rails::Railtie)
|
data/mortymer.gemspec
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/mortymer/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "mortymer"
|
7
|
+
spec.version = Mortymer::VERSION
|
8
|
+
spec.authors = ["Adrian Gonzalez"]
|
9
|
+
spec.email = ["adriangonzalezsanchez1996@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "A really simple DSL for writing API endpoints with openapi support"
|
12
|
+
spec.description = "A simple DSL to describe metadata for endpoints and automatic params construction based on dry-struct schemas. Support for openapi"
|
13
|
+
spec.homepage = "https://github.com/adriangs1996/morty"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
21
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
(File.expand_path(f) == __FILE__) ||
|
28
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
spec.bindir = "exe"
|
32
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
33
|
+
spec.require_paths = ["lib"]
|
34
|
+
|
35
|
+
# Uncomment to register a new dependency of your gem
|
36
|
+
spec.add_dependency "dry-monads", "~> 1.7"
|
37
|
+
spec.add_dependency "dry-struct", "~> 1.7"
|
38
|
+
spec.add_dependency "dry-swagger", "~> 2.0"
|
39
|
+
spec.add_dependency "dry-system", "~> 1.2"
|
40
|
+
spec.add_dependency "dry-validation", "~> 1.11"
|
41
|
+
|
42
|
+
spec.add_development_dependency "rails", "~> 8.0"
|
43
|
+
# For more information and examples about making a new gem, check out our
|
44
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
45
|
+
end
|