diff --git a/jsonapi-swagger.gemspec b/jsonapi-swagger.gemspec index d9c1020..77e36ce 100644 --- a/jsonapi-swagger.gemspec +++ b/jsonapi-swagger.gemspec @@ -13,15 +13,6 @@ Gem::Specification.new do |spec| spec.summary = 'JSON API Swagger Doc Generator' spec.homepage = 'https://github.com/superiorlu/jsonapi-swagger' - # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' - # to allow pushing to a single host or delete this section to allow pushing to any host. - if spec.respond_to?(:metadata) - spec.metadata['allowed_push_host'] = 'https://rubygems.org' - else - raise 'RubyGems 2.0 or newer is required to protect against ' \ - 'public gem pushes.' - end - # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. spec.files = Dir['lib/**/*', 'LICENSE.md', 'README.md'] @@ -32,6 +23,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler', '~> 2.0' spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rubocop', '~> 0.67' - - spec.add_dependency 'rswag', '~>2.0' + spec.add_development_dependency 'rswag', '~>2.0' end diff --git a/lib/generators/jsonapi/swagger/swagger_generator.rb b/lib/generators/jsonapi/swagger/swagger_generator.rb index 1c1b999..1969ef2 100644 --- a/lib/generators/jsonapi/swagger/swagger_generator.rb +++ b/lib/generators/jsonapi/swagger/swagger_generator.rb @@ -4,15 +4,54 @@ module Jsonapi source_root File.expand_path('templates', __dir__) def create_swagger_file - swagger_file = File.join( + if Jsonapi::Swagger.use_rswag + template 'swagger.rb.erb', spec_file + else + template 'swagger.json.erb', json_file + end + end + + private + + def doc + @doc ||= swagger_json.parse_doc + end + + def spec_file + @spec_file ||= File.join( 'spec/requests', class_path, spec_file_name ) - template 'swagger.rb.erb', swagger_file end - private + def json_file + @json_file ||= File.join( + 'swagger', + class_path, + swagger_file_path + ) + end + + def swagger_version + Jsonapi::Swagger.version + end + + def swagger_info + JSON.pretty_generate(Jsonapi::Swagger.info) + end + + def swagger_base_path + Jsonapi::Swagger.base_path + end + + def swagger_file_path + Jsonapi::Swagger.file_path + end + + def swagger_json + @swagger_json ||= Jsonapi::Swagger::Json.new(json_file) + end def spec_file_name "#{file_name.downcase.pluralize}_spec.rb" @@ -38,6 +77,10 @@ module Jsonapi t(:sortable_fields) + ': (-)' + sortable_fields.join(',') end + def ori_sortable_fields_desc + tt(:sortable_fields) + ': (-)' + sortable_fields.join(',') + end + def model_klass model_class_name.safe_constantize end @@ -70,10 +113,11 @@ module Jsonapi resource_klass.filters end - def columns_with_comment + def columns_with_comment(need_encoding: true) @columns_with_comment ||= {}.tap do |clos| model_klass.columns.each do |col| - clos[col.name.to_sym] = { type: swagger_type(col), items_type: col.type, is_array: col.array, nullable: col.null, comment: safe_encode(col.comment) } + clos[col.name.to_sym] = { type: swagger_type(col), items_type: col.type, is_array: col.array, nullable: col.null, comment: col.comment } + clos[col.name.to_sym][:comment] = safe_encode(col.comment) if need_encoding end end end @@ -89,10 +133,14 @@ module Jsonapi end def t(key, options={}) + content = tt(key, options) + safe_encode(content) + end + + def tt(key, options={}) options[:scope] = :jsonapi_swagger options[:default] = key.to_s.humanize - content = I18n.t(key, options) - safe_encode(content) + I18n.t(key, options) end def safe_encode(content) diff --git a/lib/generators/jsonapi/swagger/templates/swagger.json.erb b/lib/generators/jsonapi/swagger/templates/swagger.json.erb new file mode 100644 index 0000000..7ec1650 --- /dev/null +++ b/lib/generators/jsonapi/swagger/templates/swagger.json.erb @@ -0,0 +1,319 @@ +{ + "swagger": "<%= swagger_version %>", + "info": <%= swagger_info %>, + "basePath" : "<%= swagger_base_path %>", + <%- + def list_resource_parameters + [].tap do |parameters| + parameters << { name: 'page[number]', in: :query, type: :string, description: tt(:page_num), required: false } + parameters << { name: 'page[size]', in: :query, type: :string, description: tt(:page_size), required: false } + if sortable_fields.present? + parameters << { name: 'sort', in: :query, type: :string, description: ori_sortable_fields_desc, required: false } + end + if relationships.present? + parameters << { name: :include, in: :query, type: :string, description: tt(:include_related_data), required: false } + end + filters.each do |filter_attr, filter_config| + parameters << { name: :"filter[#{filter_attr}]", in: :query, type: :string, description: tt(:filter_field), required: false} + end + parameters << { name: :"fields[#{route_resouces}]", in: :query, type: :string, description: tt(:display_field), required: false } + relationships.each_value do |relation| + parameters << { name: :"fields[#{relation.class_name.tableize}]", in: :query, type: :string, description: tt(:display_field), required: false } + end + end + end + + def show_resource_parameters + [].tap do |parameters| + parameters << { name: :id, in: :path, type: :integer, description: 'ID', required: true } + if relationships.present? + parameters << { name: :include, in: :query, type: :string, description: tt(:include_related_data), required: false } + end + parameters << { name: :"fields[#{route_resouces}]", in: :query, type: :string, description: tt(:display_field), required: false } + relationships.each_value do |relation| + parameters << { name: :"fields[#{relation.class_name.tableize}]", in: :query, type: :string, description: tt(:display_field), required: false } + end + end + end + + def create_resource_parameters + parameters = { + name: :data, + in: :body, + type: :object, + properties: { + data: { + type: :object, + properties: { + type: { type: :string, default: route_resouces }, + attributes: { + type: :object, + properties: properties(attrs: creatable_fields) + } + } + }, + }, + description: tt(:request_body) + } + parameters[:properties][:data][:properties][:relationships] ||= {} + parameters[:properties][:data][:properties][:relationships] = { type: :object, properties: create_relationships_properties } + parameters + end + + def patch_resource_parameters + patch_parameters = create_resource_parameters.dup + patch_parameters[:properties][:data][:properties][:id] ||= {} + patch_parameters[:properties][:data][:properties][:id].merge!({ type: :integer, description: 'ID' }) + parameters = [{ name: :id, in: :path, type: :integer, description: 'ID', required: true }] + parameters << patch_parameters + parameters + end + + def delete_resource_parameters + [{ name: :id, in: :path, type: :integer, description: 'ID', required: true }] + end + + def properties(attrs: []) + Hash.new{|h, k| h[k] = {}} .tap do |props| + attrs.each do |attr| + columns = columns_with_comment(need_encoding: false) + props[attr][:type] = columns[attr][:type] + props[attr][:items] = { type: columns[attr][:items_type] } if columns[attr][:is_array] + props[attr][:'x-nullable'] = columns[attr][:nullable] + props[attr][:description] = columns[attr][:comment] + end + end + end + + def relationships_properties + {}.tap do |relat_props| + relationships.each do |relation_name, relation| + relation_name_camelize = relation_name.to_s.camelize + relat_props[relation_name] = { + type: :object, + properties: { + links: { + type: :object, + properties: { + self: { type: :string, description: tt(:associate_list_link, model: relation_name_camelize) }, + related: { type: :string, description: tt(:related_link, model: relation_name_camelize) }, + }, + description: tt(:related_link, model: relation_name_camelize) + }, + }, + description: tt(:related_model, model: relation_name_camelize) + } + end + end + end + + def create_relationships_properties + {}.tap do |relat_props| + relationships.each do |relation_name, relation| + relation_name_camelize = relation_name.to_s.camelize + relat_props[relation_name] = { + type: :object, + properties: { + data: { + type: :array, + items: { + type: :object, + properties: { + type: { type: :string, default: relation.table_name }, + id: { type: :string, description: "#{relation_name_camelize} ID" }, + }, + }, + description: tt(:related_ids, model: relation_name_camelize) + } + }, + description: tt(:related_ids, model: relation_name_camelize) + } + if relation.belongs_to? + relat_props[relation_name][:properties][:data] = { + type: :object, + properties: { + type: { type: :string, default: relation.table_name }, + id: { type: :string, description: "#{relation_name_camelize} ID" }, + }, + description: tt(:related_id, model: relation_name_camelize) + } + end + end + end + end + + def list_resource_responses + { + '200' => { + description: tt(:get_list), + schema: { + type: :object, + properties: { + data: { + type: :array, + items: { + type: :object, + properties: { + id: { type: :string, description: 'ID'}, + links: { + type: :object, + properties: { + self: { type: :string, description: tt(:detail_link) }, + }, + description: tt(:detail_link) + }, + attributes: { + type: :object, + properties: properties(attrs: attributes.each_key), + description: tt(:attributes) + }, + relationships: { + type: :object, + properties: relationships_properties, + description: tt(:associate_data) + } + }, + }, + description: tt(:data) + }, + meta: { + type: :object, + properties: { + record_count: { type: :integer, description: tt(:record_count)}, + page_count: { type: :integer, description: tt(:page_count)}, + }, + description: tt(:meta) + }, + links: { + type: :object, + properties: { + first: { type: :string, description: tt(:first_page_link) }, + next: { type: :string, description: tt(:next_page_link) }, + last: { type: :string, description: tt(:last_page_link) }, + }, + description: tt(:page_links) }, + }, + required: [:data] + } + } + } + end + + def show_resource_responses + { + '200' => { + description: tt(:get_detail), + schema: show_resource_schema + } + } + end + + def create_resource_responses + { + '201' => { + description: tt(:create), + schema: show_resource_schema + } + } + end + + def delete_resource_responses + { + '204' => { description: tt(:delete) } + } + end + + def show_resource_schema + { + type: :object, + properties: { + data: { + type: :object, + properties: { + id: { type: :string, description: 'ID'}, + type: { type: :string, description: 'Type'}, + links: { + type: :object, + properties: { + self: { type: :string, description: tt(:detail_link) }, + }, + description: tt(:detail_link) + }, + attributes: { + type: :object, + properties: properties(attrs: attributes.each_key), + description: tt(:attributes) + }, + relationships: { + type: :object, + properties: relationships_properties, + description: tt(:associate_data) + } + }, + description: tt(:data) + } + }, + required: [:data] + } + end + + doc['paths']["/#{route_resouces}"] = { + get: { + summary: "#{route_resouces} #{tt(:list)}", + tags: [route_resouces], + produces: ['application/vnd.api+json'], + parameters: list_resource_parameters, + responses: list_resource_responses + } + } + + doc['paths']["/#{route_resouces}/{id}"] = { + get: { + summary: "#{route_resouces} #{tt(:detail)}", + tags: [route_resouces], + produces: ['application/vnd.api+json'], + parameters: show_resource_parameters, + responses: show_resource_responses + } + } + +if resource_klass.mutable? + doc['paths']["/#{route_resouces}"].merge!({ + post: { + summary: "#{route_resouces} #{tt(:create)}", + tags: [route_resouces], + consumes: ['application/vnd.api+json'], + produces: ['application/vnd.api+json'], + parameters: [create_resource_parameters], + responses: create_resource_responses + } + }) + + doc['paths']["/#{route_resouces}/{id}"].merge!({ + patch: { + summary: "#{route_resouces} #{tt(:patch)}", + tags: [route_resouces], + consumes: ['application/vnd.api+json'], + produces: ['application/vnd.api+json'], + parameters: patch_resource_parameters, + responses: show_resource_responses + } + }) + + doc['paths']["/#{route_resouces}/{id}"].merge!({ + delete: { + summary: "#{route_resouces} #{tt(:delete)}", + tags: [route_resouces], + produces: ['application/vnd.api+json'], + parameters: delete_resource_parameters, + responses: delete_resource_responses + } + }) +else + doc['paths']["/#{route_resouces}"].delete(:post) + doc['paths']["/#{route_resouces}/{id}"].delete(:patch) + doc['paths']["/#{route_resouces}/{id}"].delete(:delete) +end +-%> + "paths": <%= JSON.pretty_generate(doc['paths'] ) %> +} \ No newline at end of file diff --git a/lib/generators/jsonapi/swagger/templates/swagger.rb.erb b/lib/generators/jsonapi/swagger/templates/swagger.rb.erb index d0fc818..e19e6fe 100644 --- a/lib/generators/jsonapi/swagger/templates/swagger.rb.erb +++ b/lib/generators/jsonapi/swagger/templates/swagger.rb.erb @@ -3,7 +3,11 @@ RSpec.describe '<%= resouces_name %>', type: :request do let(:include) {''} #see https://github.com/domaindrivendev/rswag/issues/188 before(:each) do + <% if defined?(FactoryBot) -%> @<%= model_name %> = create :<%= model_name %> + <% else -%> + @<%= model_name %> = <%= model_class_name %>.create + <% end -%> end path '/<%= route_resouces %>' do diff --git a/lib/i18n/en.yml b/lib/i18n/en.yml index c893537..0658fd0 100644 --- a/lib/i18n/en.yml +++ b/lib/i18n/en.yml @@ -1,6 +1,7 @@ en: jsonapi_swagger: page_num: 'Page Number' + page_size: 'Page Size' include_related_data: 'Include Related Data' sortable_fields: 'Sortable Fields' display_field: 'Display Field' diff --git a/lib/i18n/zh-CN.yml b/lib/i18n/zh-CN.yml index a6888ae..90dc5fc 100644 --- a/lib/i18n/zh-CN.yml +++ b/lib/i18n/zh-CN.yml @@ -1,6 +1,7 @@ zh-CN: jsonapi_swagger: page_num: '页码' + page_size: '每页条数' include_related_data: '包含关联数据' sortable_fields: '排序字段' display_field: '显示字段' diff --git a/lib/jsonapi/swagger.rb b/lib/jsonapi/swagger.rb index d7b164f..36febac 100644 --- a/lib/jsonapi/swagger.rb +++ b/lib/jsonapi/swagger.rb @@ -2,9 +2,38 @@ require 'jsonapi/swagger/version' require 'jsonapi/swagger/railtie' if defined?(Rails) +require 'jsonapi/swagger/json' module Jsonapi module Swagger class Error < StandardError; end + + class << self + attr_accessor :version, :info, :file_path, :base_path, :use_rswag + + def config + yield self + end + + def version + @version ||= '2.0' + end + + def info + @info ||= { title: 'API V1', version: 'V1' } + end + + def file_path + @file_path ||= 'v1/swagger.json' + end + + def base_path + @base_path + end + + def use_rswag + @use_rswag ||= false + end + end end end diff --git a/lib/jsonapi/swagger/json.rb b/lib/jsonapi/swagger/json.rb new file mode 100644 index 0000000..b23c3e9 --- /dev/null +++ b/lib/jsonapi/swagger/json.rb @@ -0,0 +1,31 @@ +module Jsonapi + module Swagger + class Json + + attr_accessor :path + + def initialize(path = 'swagger/v1/swagger.json') + @path = path + end + + def parse_doc + @doc ||= JSON.parse(load) rescue Hash.new{ |h, k| h[k]= {} } + end + + def base_path + Jsonapi::Swagger.base_path + end + + def load + @data ||= if File.exist?(path) + IO.read(path) + else + puts "create swagger.json in #{path}" + '{}' + end + end + + + end + end +end \ No newline at end of file