├── .github └── dependabot.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console ├── openapi2ruby └── setup ├── exe └── openapi2ruby ├── lib ├── openapi2ruby.rb └── openapi2ruby │ ├── cli.rb │ ├── generator.rb │ ├── openapi.rb │ ├── openapi │ ├── schema.rb │ └── schema │ │ └── property.rb │ ├── parser.rb │ ├── templates │ └── serializer.rb.erb │ └── version.rb ├── openapi2ruby.gemspec └── spec ├── fixtures └── files │ ├── link-example.yaml │ ├── oneof.yaml │ ├── pet_serializer.rb │ ├── pet_serializer_oneof.rb │ └── petstore.yaml ├── openapi2ruby ├── generator_spec.rb ├── oneof_spec.rb ├── openapi_schema_property_spec.rb ├── openapi_schema_spec.rb ├── openapi_spec.rb └── parser_spec.rb ├── openapi2ruby_spec.rb ├── spec_helper.rb └── tmp └── .gitkeep /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "20:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /spec/tmp/* 10 | /test/tmp/ 11 | /test/version_tmp/ 12 | /tmp/ 13 | .rspec_status 14 | 15 | !.gitkeep 16 | 17 | # Used by dotenv library to load environment variables. 18 | # .env 19 | 20 | ## Specific to RubyMotion: 21 | .dat* 22 | .repl_history 23 | build/ 24 | *.bridgesupport 25 | build-iPhoneOS/ 26 | build-iPhoneSimulator/ 27 | 28 | ## Specific to RubyMotion (use of CocoaPods): 29 | # 30 | # We recommend against adding the Pods directory to your .gitignore. However 31 | # you should judge for yourself, the pros and cons are mentioned at: 32 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 33 | # 34 | # vendor/Pods/ 35 | 36 | ## Documentation cache and generated files: 37 | /.yardoc/ 38 | /_yardoc/ 39 | /doc/ 40 | /rdoc/ 41 | 42 | ## Environment normalization: 43 | /.bundle/ 44 | /vendor/bundle 45 | /lib/bundler/man/ 46 | 47 | # for a library or gem, you might want to ignore these files since the code is 48 | # intended to run in multiple environments; otherwise, check them in: 49 | Gemfile.lock 50 | .ruby-version 51 | .ruby-gemset 52 | 53 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 54 | .rvmrc 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | cache: bundler 4 | rvm: 5 | - 2.3.0 6 | - 2.3.8 7 | - 2.4.5 8 | - 2.5.3 9 | - 2.6.1 10 | - ruby-head 11 | matrix: 12 | fast_finish: true 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at takanamito0928@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in openapi2ruby.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 takanamito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Openapi2ruby 2 | 3 | [![Build Status](https://travis-ci.com/takanamito/openapi2ruby.svg?branch=master)](https://travis-ci.com/takanamito/openapi2ruby) 4 | 5 | A library to generate ruby class from openapi.yaml. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'openapi2ruby' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install openapi2ruby 22 | 23 | ## Usage 24 | 25 | You can generate ruby class from openapi.yaml (Now support OpenAPI Specification 3.0 only) 26 | 27 | For example, you uses OpenAPI-Specification [link-example schema](https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/link-example.yaml#L178-L203). 28 | 29 | ```sh 30 | # generate ruby class 31 | $ openapi2ruby generate path/to/link-example.yaml --out ./ 32 | 33 | $ ls 34 | pullrequest_serializer.rb repository_serializer.rb user_serializer.rb 35 | ``` 36 | 37 | Generated class is below. 38 | Default, this gem generates `ActiveModel::Serializer` class. 39 | 40 | ```ruby 41 | class RepositorySerializer < ActiveModel::Serializer 42 | attributes :slug, :owner 43 | 44 | def owner 45 | UserSerializer.new(object.user) 46 | end 47 | 48 | 49 | def slug 50 | type_check(:slug, [String]) 51 | object.slug 52 | end 53 | 54 | private 55 | 56 | def type_check(name, types) 57 | raise "Field type is invalid. #{name}" unless types.include?(object.send(name).class) 58 | end 59 | end 60 | ``` 61 | 62 | ### Use original template 63 | 64 | If you wants to generate from other template with ERB. 65 | You can specify it with cli option. 66 | 67 | Write original template. 68 | 69 | ```erb 70 | class <%= @schema.name %> 71 | attr_accessor <%= @schema.properties.map{ |p| ":#{p.name}" }.join(', ') %> 72 | 73 | def intiialize(args) 74 | <%- @schema.properties.each do |p| -%> 75 | <%= "@#{p.name} = args[:#{p.name}]" %> 76 | <%- end -%> 77 | end 78 | end 79 | ``` 80 | 81 | Generate with `--template` option. 82 | 83 | ```sh 84 | $ openapi2ruby generate path/to/link-example.yaml \ 85 | --template path/to/original_template.rb.erb \ 86 | --out ./ 87 | ``` 88 | 89 | ```ruby 90 | class Repository 91 | attr_accessor :slug, :owner 92 | 93 | def intiialize(args) 94 | @slug = args[:slug] 95 | @owner = args[:owner] 96 | end 97 | end 98 | ``` 99 | 100 | For more template value information, please check [default template](https://github.com/takanamito/openapi2ruby/blob/master/lib/openapi2ruby/templates/serializer.rb.erb). 101 | 102 | ## Development 103 | 104 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 105 | 106 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 107 | 108 | ## Contributing 109 | 110 | Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/openapi2ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 111 | 112 | ## License 113 | 114 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 115 | 116 | ## Code of Conduct 117 | 118 | Everyone interacting in the Openapi2ruby project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/takanamito/openapi2ruby/blob/master/CODE_OF_CONDUCT.md). 119 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "openapi2ruby" 5 | 6 | require "pry" 7 | Pry.start 8 | -------------------------------------------------------------------------------- /bin/openapi2ruby: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'openapi2ruby' 3 | 4 | Openapi2ruby::Cli.start 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /exe/openapi2ruby: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'openapi2ruby' 3 | 4 | Openapi2ruby::Cli.start 5 | -------------------------------------------------------------------------------- /lib/openapi2ruby.rb: -------------------------------------------------------------------------------- 1 | require "openapi2ruby/version" 2 | require 'openapi2ruby/cli' 3 | require 'openapi2ruby/parser' 4 | require 'openapi2ruby/generator' 5 | require 'openapi2ruby/openapi' 6 | require 'openapi2ruby/openapi/schema' 7 | require 'openapi2ruby/openapi/schema/property' 8 | 9 | require 'active_support/core_ext/string/inflections' 10 | -------------------------------------------------------------------------------- /lib/openapi2ruby/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | 3 | module Openapi2ruby 4 | class Cli < Thor 5 | desc 'parse', 'load openapi.yaml' 6 | def parse(path) 7 | puts 'Loading OpenAPI yaml file...' 8 | raise "File not found. #{path}" unless File.exist?(path) 9 | 10 | openapi = Openapi2ruby::Parser.parse(path) 11 | p openapi.schemas 12 | end 13 | 14 | option :template, type: :string 15 | option :out, required: true, type: :string 16 | desc 'generate', 'load openapi.yaml and generate serializer' 17 | def generate(path) 18 | puts 'Loading OpenAPI yaml file...' 19 | raise "File not found. #{path}" unless File.exist?(path) 20 | 21 | openapi = Openapi2ruby::Parser.parse(path) 22 | openapi.schemas.each do |schema| 23 | serializer = Openapi2ruby::Generator.generate(schema, options[:out], options[:template]) 24 | puts "Created: #{serializer}" 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/openapi2ruby/generator.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/string/inflections' 2 | require 'erb' 3 | require 'pathname' 4 | 5 | module Openapi2ruby 6 | class Generator 7 | TEMPLATE_PATH = File.expand_path('../templates/serializer.rb.erb', __FILE__) 8 | 9 | # Generate ruby class from OpenAPI schema 10 | # @param schema [Openapi2ruby::Openapi::Schema] parsed OpenAPI schema 11 | # @param output_path [String] parsed OpenAPI YAML 12 | # @param template_path [String] original template path 13 | def self.generate(schema, output_path, template_path) 14 | new(schema).generate(output_path, template_path) 15 | end 16 | 17 | def initialize(schema) 18 | @schema = schema 19 | end 20 | 21 | # Generate ruby class from OpenAPI schema 22 | # @param output_path [String] parsed OpenAPI YAML 23 | # @param template_path [String] original template path 24 | def generate(output_path, template_path) 25 | template_path = TEMPLATE_PATH if template_path.nil? 26 | template = File.read(template_path) 27 | generated_class = ERB.new(template, nil, '-').result(binding) 28 | 29 | output_file = Pathname.new(output_path).join("#{@schema.name.underscore}_serializer.rb") 30 | File.open(output_file.to_s, 'w') { |file| file << generated_class } 31 | output_file.to_s 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/openapi2ruby/openapi.rb: -------------------------------------------------------------------------------- 1 | module Openapi2ruby 2 | class Openapi 3 | def initialize(content) 4 | @content = content 5 | end 6 | 7 | # Creates OpenAPI Schema array 8 | # @return [Array[Openapi2ruby::Openapi::Schema]] 9 | def schemas 10 | @content['components']['schemas'].each_with_object([]) do |(key, value), results| 11 | schema_content = { name: key, definition: value} 12 | schema = Openapi2ruby::Openapi::Schema.new(schema_content) 13 | results << schema unless schema.properties.empty? 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/openapi2ruby/openapi/schema.rb: -------------------------------------------------------------------------------- 1 | module Openapi2ruby 2 | class Openapi::Schema 3 | def initialize(content) 4 | @name = content[:name] 5 | @definition = content[:definition] 6 | end 7 | 8 | # OpenAPI camelcase schema name 9 | # @return [String] 10 | def name 11 | @name.camelcase 12 | end 13 | 14 | # OpenAPI required properties name 15 | # @return [Array[String]] 16 | def requireds 17 | @definition['required'] 18 | end 19 | 20 | # OpenAPI schema properties 21 | # @return [Array[Openapi2ruby::Openapi::Schema]] 22 | def properties 23 | return [] if @definition['properties'].nil? 24 | @definition['properties'].each_with_object([]) do |(key, value), results| 25 | content = { name: key, definition: value } 26 | results << Openapi2ruby::Openapi::Schema::Property.new(content) 27 | end 28 | end 29 | 30 | # Whether property is required or not 31 | # @param [Openapi2ruby::Openapi::Schema::Property] OpenAPI schema property 32 | # @return [Boolean] 33 | def required?(property) 34 | return false if requireds.nil? 35 | requireds.include?(property.name) 36 | end 37 | 38 | def one_ofs 39 | return [] if properties.empty? 40 | properties.each_with_object([]) do |value, results| 41 | if value.one_of? 42 | results << value.one_of_refs 43 | else 44 | results 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/openapi2ruby/openapi/schema/property.rb: -------------------------------------------------------------------------------- 1 | module Openapi2ruby 2 | class Openapi::Schema::Property 3 | attr_reader :name 4 | 5 | def initialize(content) 6 | @name = content[:name] 7 | @type = content[:definition]['type'] 8 | @items = content[:definition]['items'] 9 | @format = content[:definition]['format'] 10 | @ref = content[:definition]['$ref'] 11 | if content[:definition]['oneOf'] 12 | @type = 'oneOf' 13 | @one_of_refs = content[:definition]['oneOf'].map do |content| 14 | content['$ref'].split('/').last 15 | end 16 | else 17 | @one_of_refs = [] 18 | end 19 | end 20 | 21 | # OpenAPI schema ref property name 22 | # @return [String] 23 | def ref 24 | return @items['$ref'].split('/').last if ref_items? 25 | @ref.split('/').last 26 | end 27 | 28 | # OpenAPI schema ref property class name 29 | # @return [String] 30 | def ref_class 31 | ref.camelcase 32 | end 33 | 34 | # Whether property is ref or not 35 | # @return [Boolean] 36 | def ref? 37 | !@ref.nil? 38 | end 39 | 40 | # Whether property has ref array items 41 | # @return [Boolean] 42 | def ref_items? 43 | @type == 'array' && !@items['$ref'].nil? 44 | end 45 | 46 | # OpenAPI schema property types 47 | # @return [Array[Class]] 48 | def types 49 | return [Hash] if one_of? 50 | return [ref] if @type.nil? 51 | 52 | converted_types 53 | end 54 | 55 | def one_of? 56 | @type == 'oneOf' && !@one_of_refs.empty? 57 | end 58 | 59 | def one_of_refs 60 | @one_of_refs 61 | end 62 | 63 | private 64 | 65 | # OpenAPI schema property types in Ruby 66 | # @return [Array[Class]] 67 | def converted_types 68 | case @type 69 | when 'string', 'integer', 'array' 70 | [Object.const_get(@type.capitalize)] 71 | when 'number' 72 | [Float] 73 | when 'boolean' 74 | [TrueClass, FalseClass] 75 | when 'object' 76 | [Hash] 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/openapi2ruby/parser.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Openapi2ruby 4 | class Parser 5 | # Parse openapi.yaml 6 | # @param path [String] OpenAPI schema file path 7 | # @return [Openapi2ruby::Openapi] 8 | def self.parse(path) 9 | new(path).parse 10 | end 11 | 12 | def initialize(path) 13 | @path = path 14 | end 15 | 16 | # Parse openapi.yaml 17 | # @return [Openapi2ruby::Openapi] 18 | def parse 19 | Openapi.new(parse_file) 20 | end 21 | 22 | def parse_file 23 | file = File.read(@path) 24 | YAML.load(file) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/openapi2ruby/templates/serializer.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= @schema.name %>Serializer < ActiveModel::Serializer 2 | attributes <%= @schema.properties.map{ |p| ":#{p.name}" }.join(', ') -%> 3 | 4 | <%- @schema.properties.select{ |p| p.ref? || p.ref_items? }.each do |p| %> 5 | def <%= p.name -%> 6 | <%- if p.ref? %> 7 | <%= "#{p.ref_class}Serializer.new(object.#{p.ref})" %> 8 | <%- else %> 9 | ActiveModel::Serializer::CollectionSerializer.new( 10 | <%= "object.#{p.name}" -%>, 11 | serializer: <%= "#{p.ref_class}" -%>Serializer, 12 | ) 13 | <%- end -%> 14 | end 15 | <%- end -%> 16 | 17 | <%- @schema.properties.reject{ |p| p.ref? || p.ref_items? }.each do |p| %> 18 | def <%= p.name -%> 19 | <%- if @schema.required?(p) %> 20 | required_check(<%= ":#{p.name}" %>) 21 | <%- end %> 22 | <%- if p.one_of? -%> 23 | type_check(<%= ":#{p.name}"-%>, [ 24 | <%- p.one_of_refs.map do |ref| -%> 25 | <%= ref -%>, 26 | <%- end -%> 27 | ]) 28 | one_of_<%=- p.name %>(object.<%=- p.name %>) 29 | <%- else -%> 30 | type_check(<%= ":#{p.name}, #{p.types}" -%>) 31 | object.<%=- p.name %> 32 | <%- end -%> 33 | end 34 | <%- end %> 35 | private 36 | <%- unless @schema.requireds.nil? %> 37 | def required_check(name) 38 | raise "Required field is nil. #{name}" if object.send(name).nil? 39 | end 40 | <%- end %> 41 | def type_check(name, types) 42 | raise "Field type is invalid. #{name}" unless types.include?(object.send(name).class) 43 | end 44 | <%- @schema.properties.select{ |p| p.one_of? }.each do |p| %> 45 | def one_of_<%=- p.name %>(one_of_value) 46 | serializer = ActiveModelSerializers::SerializableResource.new(one_of_value).serializer 47 | raise "Undefined serializer one_of ref: #{one_of_value.class}" if serializer.nil? 48 | serializer.new(one_of_value) 49 | end 50 | <%- end -%> 51 | end 52 | -------------------------------------------------------------------------------- /lib/openapi2ruby/version.rb: -------------------------------------------------------------------------------- 1 | module Openapi2ruby 2 | VERSION = '0.2.0' 3 | end 4 | -------------------------------------------------------------------------------- /openapi2ruby.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "openapi2ruby/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "openapi2ruby" 8 | spec.version = Openapi2ruby::VERSION 9 | spec.authors = ["takanamito"] 10 | spec.email = ["takanamito0928@gmail.com"] 11 | 12 | spec.summary = %q{A simple ruby class generator from OpenAPI schema} 13 | spec.description = %q{A simple ruby class generator from OpenAPI schema} 14 | spec.homepage = "https://github.com/takanamito/openapi2ruby" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | spec.bindir = "exe" 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ["lib"] 23 | spec.required_ruby_version = '>= 2.3' 24 | 25 | spec.add_dependency 'activesupport' 26 | spec.add_dependency 'thor', '>= 0.20', '< 2.0' 27 | 28 | spec.add_development_dependency "bundler" 29 | spec.add_development_dependency "rake" 30 | spec.add_development_dependency "rspec" 31 | end 32 | -------------------------------------------------------------------------------- /spec/fixtures/files/link-example.yaml: -------------------------------------------------------------------------------- 1 | # This yaml referenced OpenAPI-Specification 2 | # https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/link-example.yaml 3 | openapi: 3.0.0 4 | info: 5 | title: Link Example 6 | version: 1.0.0 7 | paths: 8 | /2.0/users/{username}: 9 | get: 10 | operationId: getUserByName 11 | parameters: 12 | - name: username 13 | in: path 14 | required: true 15 | schema: 16 | type: string 17 | responses: 18 | '200': 19 | description: The User 20 | content: 21 | application/json: 22 | schema: 23 | $ref: '#/components/schemas/user' 24 | links: 25 | userRepositories: 26 | $ref: '#/components/links/UserRepositories' 27 | /2.0/repositories/{username}: 28 | get: 29 | operationId: getRepositoriesByOwner 30 | parameters: 31 | - name: username 32 | in: path 33 | required: true 34 | schema: 35 | type: string 36 | responses: 37 | '200': 38 | description: repositories owned by the supplied user 39 | content: 40 | application/json: 41 | schema: 42 | type: array 43 | items: 44 | $ref: '#/components/schemas/repository' 45 | links: 46 | userRepository: 47 | $ref: '#/components/links/UserRepository' 48 | /2.0/repositories/{username}/{slug}: 49 | get: 50 | operationId: getRepository 51 | parameters: 52 | - name: username 53 | in: path 54 | required: true 55 | schema: 56 | type: string 57 | - name: slug 58 | in: path 59 | required: true 60 | schema: 61 | type: string 62 | responses: 63 | '200': 64 | description: The repository 65 | content: 66 | application/json: 67 | schema: 68 | $ref: '#/components/schemas/repository' 69 | links: 70 | repositoryPullRequests: 71 | $ref: '#/components/links/RepositoryPullRequests' 72 | /2.0/repositories/{username}/{slug}/pullrequests: 73 | get: 74 | operationId: getPullRequestsByRepository 75 | parameters: 76 | - name: username 77 | in: path 78 | required: true 79 | schema: 80 | type: string 81 | - name: slug 82 | in: path 83 | required: true 84 | schema: 85 | type: string 86 | - name: state 87 | in: query 88 | schema: 89 | type: string 90 | enum: 91 | - open 92 | - merged 93 | - declined 94 | responses: 95 | '200': 96 | description: an array of pull request objects 97 | content: 98 | application/json: 99 | schema: 100 | type: array 101 | items: 102 | $ref: '#/components/schemas/pullrequest' 103 | /2.0/repositories/{username}/{slug}/pullrequests/{pid}: 104 | get: 105 | operationId: getPullRequestsById 106 | parameters: 107 | - name: username 108 | in: path 109 | required: true 110 | schema: 111 | type: string 112 | - name: slug 113 | in: path 114 | required: true 115 | schema: 116 | type: string 117 | - name: pid 118 | in: path 119 | required: true 120 | schema: 121 | type: string 122 | responses: 123 | '200': 124 | description: a pull request object 125 | content: 126 | application/json: 127 | schema: 128 | $ref: '#/components/schemas/pullrequest' 129 | links: 130 | pullRequestMerge: 131 | $ref: '#/components/links/PullRequestMerge' 132 | /2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge: 133 | post: 134 | operationId: mergePullRequest 135 | parameters: 136 | - name: username 137 | in: path 138 | required: true 139 | schema: 140 | type: string 141 | - name: slug 142 | in: path 143 | required: true 144 | schema: 145 | type: string 146 | - name: pid 147 | in: path 148 | required: true 149 | schema: 150 | type: string 151 | responses: 152 | '204': 153 | description: the PR was successfully merged 154 | components: 155 | links: 156 | UserRepositories: 157 | # returns array of '#/components/schemas/repository' 158 | operationId: getRepositoriesByOwner 159 | parameters: 160 | username: $response.body#/username 161 | UserRepository: 162 | # returns '#/components/schemas/repository' 163 | operationId: getRepository 164 | parameters: 165 | username: $response.body#/owner/username 166 | slug: $response.body#/slug 167 | RepositoryPullRequests: 168 | # returns '#/components/schemas/pullrequest' 169 | operationId: getPullRequestsByRepository 170 | parameters: 171 | username: $response.body#/owner/username 172 | slug: $response.body#/slug 173 | PullRequestMerge: 174 | # executes /2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge 175 | operationId: mergePullRequest 176 | parameters: 177 | username: $response.body#/author/username 178 | slug: $response.body#/repository/slug 179 | pid: $response.body#/id 180 | schemas: 181 | user: 182 | type: object 183 | properties: 184 | username: 185 | type: string 186 | uuid: 187 | type: string 188 | repository: 189 | type: object 190 | properties: 191 | slug: 192 | type: string 193 | owner: 194 | $ref: '#/components/schemas/user' 195 | pullrequest: 196 | type: object 197 | properties: 198 | id: 199 | type: integer 200 | title: 201 | type: string 202 | repository: 203 | $ref: '#/components/schemas/repository' 204 | author: 205 | $ref: '#/components/schemas/user' 206 | -------------------------------------------------------------------------------- /spec/fixtures/files/oneof.yaml: -------------------------------------------------------------------------------- 1 | # This yaml referenced OpenAPI-Specification 2 | # https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/ 3 | openapi: "3.0.0" 4 | info: 5 | version: 1.0.0 6 | title: oneof example 7 | license: 8 | name: MIT 9 | paths: 10 | /pets/{petId}: 11 | get: 12 | summary: Info for a specific pet 13 | operationId: showPetById 14 | tags: 15 | - pets 16 | parameters: 17 | - name: petId 18 | in: path 19 | required: true 20 | description: The id of the pet to retrieve 21 | schema: 22 | type: string 23 | responses: 24 | '200': 25 | description: Expected response to a valid request 26 | content: 27 | application/json: 28 | schema: 29 | $ref: "#/components/schemas/Pets" 30 | default: 31 | description: unexpected error 32 | content: 33 | application/json: 34 | schema: 35 | $ref: "#/components/schemas/Error" 36 | components: 37 | schemas: 38 | Pet: 39 | required: 40 | - id 41 | - name 42 | properties: 43 | id: 44 | type: integer 45 | format: int64 46 | name: 47 | type: string 48 | animal: 49 | oneOf: 50 | - $ref: '#/components/schemas/Cat' 51 | - $ref: '#/components/schemas/Dog' 52 | name: 53 | type: string 54 | tag: 55 | type: string 56 | Dog: 57 | type: object 58 | properties: 59 | bark: 60 | type: boolean 61 | breed: 62 | type: string 63 | enum: [Dingo, Husky, Retriever, Shepherd] 64 | Cat: 65 | type: object 66 | properties: 67 | hunts: 68 | type: boolean 69 | age: 70 | type: integer 71 | Error: 72 | required: 73 | - code 74 | - message 75 | properties: 76 | code: 77 | type: integer 78 | format: int32 79 | message: 80 | type: string 81 | -------------------------------------------------------------------------------- /spec/fixtures/files/pet_serializer.rb: -------------------------------------------------------------------------------- 1 | class PetSerializer < ActiveModel::Serializer 2 | attributes :id, :name, :tag 3 | 4 | 5 | def id 6 | required_check(:id) 7 | 8 | type_check(:id, [Integer]) 9 | object.id 10 | end 11 | 12 | def name 13 | required_check(:name) 14 | 15 | type_check(:name, [String]) 16 | object.name 17 | end 18 | 19 | def tag 20 | type_check(:tag, [String]) 21 | object.tag 22 | end 23 | 24 | private 25 | 26 | def required_check(name) 27 | raise "Required field is nil. #{name}" if object.send(name).nil? 28 | end 29 | 30 | def type_check(name, types) 31 | raise "Field type is invalid. #{name}" unless types.include?(object.send(name).class) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/fixtures/files/pet_serializer_oneof.rb: -------------------------------------------------------------------------------- 1 | class PetSerializer < ActiveModel::Serializer 2 | attributes :id, :name, :animal, :tag 3 | 4 | 5 | def id 6 | required_check(:id) 7 | 8 | type_check(:id, [Integer]) 9 | object.id 10 | end 11 | 12 | def name 13 | required_check(:name) 14 | 15 | type_check(:name, [String]) 16 | object.name 17 | end 18 | 19 | def animal 20 | type_check(:animal, [ 21 | Cat, 22 | Dog, 23 | ]) 24 | one_of_animal(object.animal) 25 | end 26 | 27 | def tag 28 | type_check(:tag, [String]) 29 | object.tag 30 | end 31 | 32 | private 33 | 34 | def required_check(name) 35 | raise "Required field is nil. #{name}" if object.send(name).nil? 36 | end 37 | 38 | def type_check(name, types) 39 | raise "Field type is invalid. #{name}" unless types.include?(object.send(name).class) 40 | end 41 | 42 | def one_of_animal(one_of_value) 43 | serializer = ActiveModelSerializers::SerializableResource.new(one_of_value).serializer 44 | raise "Undefined serializer one_of ref: #{one_of_value.class}" if serializer.nil? 45 | serializer.new(one_of_value) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/fixtures/files/petstore.yaml: -------------------------------------------------------------------------------- 1 | # This yaml referenced OpenAPI-Specification 2 | # https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml 3 | openapi: "3.0.0" 4 | info: 5 | version: 1.0.0 6 | title: Swagger Petstore 7 | license: 8 | name: MIT 9 | servers: 10 | - url: http://petstore.swagger.io/v1 11 | paths: 12 | /pets: 13 | get: 14 | summary: List all pets 15 | operationId: listPets 16 | tags: 17 | - pets 18 | parameters: 19 | - name: limit 20 | in: query 21 | description: How many items to return at one time (max 100) 22 | required: false 23 | schema: 24 | type: integer 25 | format: int32 26 | responses: 27 | '200': 28 | description: A paged array of pets 29 | headers: 30 | x-next: 31 | description: A link to the next page of responses 32 | schema: 33 | type: string 34 | content: 35 | application/json: 36 | schema: 37 | $ref: "#/components/schemas/Pets" 38 | default: 39 | description: unexpected error 40 | content: 41 | application/json: 42 | schema: 43 | $ref: "#/components/schemas/Error" 44 | post: 45 | summary: Create a pet 46 | operationId: createPets 47 | tags: 48 | - pets 49 | responses: 50 | '201': 51 | description: Null response 52 | default: 53 | description: unexpected error 54 | content: 55 | application/json: 56 | schema: 57 | $ref: "#/components/schemas/Error" 58 | /pets/{petId}: 59 | get: 60 | summary: Info for a specific pet 61 | operationId: showPetById 62 | tags: 63 | - pets 64 | parameters: 65 | - name: petId 66 | in: path 67 | required: true 68 | description: The id of the pet to retrieve 69 | schema: 70 | type: string 71 | responses: 72 | '200': 73 | description: Expected response to a valid request 74 | content: 75 | application/json: 76 | schema: 77 | $ref: "#/components/schemas/Pets" 78 | default: 79 | description: unexpected error 80 | content: 81 | application/json: 82 | schema: 83 | $ref: "#/components/schemas/Error" 84 | components: 85 | schemas: 86 | Pet: 87 | required: 88 | - id 89 | - name 90 | properties: 91 | id: 92 | type: integer 93 | format: int64 94 | name: 95 | type: string 96 | tag: 97 | type: string 98 | Pets: 99 | type: array 100 | items: 101 | $ref: "#/components/schemas/Pet" 102 | Error: 103 | required: 104 | - code 105 | - message 106 | properties: 107 | code: 108 | type: integer 109 | format: int32 110 | message: 111 | type: string 112 | -------------------------------------------------------------------------------- /spec/openapi2ruby/generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Openapi2ruby::Generator do 4 | let(:generator) { Openapi2ruby::Generator.new(schema) } 5 | let(:schema) { Openapi2ruby::Parser.parse(schema_path).schemas.first } 6 | let(:schema_path) { 'spec/fixtures/files/petstore.yaml' } 7 | let(:output_path) { 'spec/tmp' } 8 | 9 | describe '#generate' do 10 | let(:generated_file) { "#{output_path}/pet_serializer.rb" } 11 | let(:file_fixture) { 'spec/fixtures/files/pet_serializer.rb' } 12 | 13 | before { generator.generate(output_path, nil) } 14 | 15 | it 'generates serializer class' do 16 | expect(File.exist?(generated_file)).to be true 17 | expect(FileUtils.cmp(generated_file, file_fixture)).to be true 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/openapi2ruby/oneof_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'fileutils' 4 | 5 | RSpec.describe Openapi2ruby::Generator do 6 | let(:generator) { Openapi2ruby::Generator.new(schema) } 7 | let(:schema) { Openapi2ruby::Parser.parse(schema_path).schemas.first } 8 | let(:schema_path) { 'spec/fixtures/files/oneof.yaml' } 9 | let(:output_path) { 'spec/tmp' } 10 | 11 | describe '#generate' do 12 | let(:generated_file) { "#{output_path}/pet_serializer.rb" } 13 | let(:file_fixture) { 'spec/fixtures/files/pet_serializer_oneof.rb' } 14 | 15 | before { generator.generate(output_path, nil) } 16 | 17 | it 'generates serializer class' do 18 | expect(File.exist?(generated_file)).to be true 19 | expect(FileUtils.cmp(generated_file, file_fixture)).to be true 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/openapi2ruby/openapi_schema_property_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Openapi2ruby::Openapi::Schema::Property do 4 | let(:path) { 'spec/fixtures/files/link-example.yaml' } 5 | let(:content) { YAML.load(File.read(path)) } 6 | let(:openapi) { Openapi2ruby::Openapi.new(content) } 7 | let(:pull_req_schema) { openapi.schemas.last } 8 | let(:properties) { pull_req_schema.properties } 9 | 10 | describe '#ref' do 11 | subject { property.ref } 12 | 13 | let(:property) { properties.last } 14 | 15 | it { is_expected.to eq 'user' } 16 | end 17 | 18 | describe '#ref_class' do 19 | subject { property.ref_class } 20 | 21 | let(:property) { properties.last } 22 | 23 | it { is_expected.to eq 'User' } 24 | end 25 | 26 | describe '#ref?' do 27 | subject { property.ref? } 28 | 29 | 30 | context 'when property has ref type' do 31 | let(:property) { properties.last } 32 | 33 | it { is_expected.to be true } 34 | end 35 | 36 | context 'when property has primitive type' do 37 | let(:property) { properties.first } 38 | 39 | it { is_expected.to be false } 40 | end 41 | end 42 | 43 | describe '#ref_items?' do 44 | subject { property.ref_items? } 45 | 46 | let(:property) { properties.last } 47 | 48 | it { is_expected.to be false } 49 | end 50 | 51 | describe '#types' do 52 | subject { properties.first.types } 53 | 54 | it { is_expected.to eq [Integer] } 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/openapi2ruby/openapi_schema_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Openapi2ruby::Openapi::Schema do 4 | subject { openapi.schemas.first } 5 | 6 | let(:path) { 'spec/fixtures/files/petstore.yaml' } 7 | let(:content) { YAML.load(File.read(path)) } 8 | let(:openapi) { Openapi2ruby::Openapi.new(content) } 9 | 10 | describe '#name' do 11 | it 'returns camelcase schema name' do 12 | expect(subject.name).to eq 'Pet' 13 | end 14 | end 15 | 16 | describe '#requires' do 17 | it 'returns required fields' do 18 | expect(subject.requireds).to eq %w(id name) 19 | end 20 | end 21 | 22 | describe '#properties' do 23 | it 'returns Openapi::Schema::Property instances' do 24 | expect(subject.properties).to all be_an(Openapi2ruby::Openapi::Schema::Property) 25 | end 26 | end 27 | 28 | describe '#required?' do 29 | subject { schema.required?(property) } 30 | 31 | let(:schema) { openapi.schemas.first } 32 | 33 | context 'when required property' do 34 | let(:property) { schema.properties.first } 35 | 36 | it { is_expected.to be true } 37 | end 38 | 39 | context 'when not be required property' do 40 | let(:property) do 41 | Openapi2ruby::Openapi::Schema::Property.new( 42 | { name: 'hoge', definition: {} } 43 | ) 44 | end 45 | 46 | it { is_expected.to be false } 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/openapi2ruby/openapi_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Openapi2ruby::Openapi do 4 | subject { Openapi2ruby::Openapi.new(content) } 5 | 6 | let(:path) { 'spec/fixtures/files/petstore.yaml' } 7 | let(:content) { YAML.load(File.read(path)) } 8 | 9 | describe '#schemas' do 10 | it 'returns Openapi::Schema array' do 11 | expect(subject.schemas).to all be_an(Openapi2ruby::Openapi::Schema) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/openapi2ruby/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Openapi2ruby::Parser do 4 | subject { Openapi2ruby::Parser.new(path) } 5 | 6 | let(:path) { 'spec/fixtures/files/petstore.yaml' } 7 | 8 | describe '#parse_file' do 9 | it 'returns parsed from openapi.yaml hash' do 10 | expect(subject.parse_file['openapi']).to eq '3.0.0' 11 | end 12 | end 13 | 14 | describe '#parse' do 15 | it 'returns Openapi instance' do 16 | expect(subject.parse).to be_a Openapi2ruby::Openapi 17 | end 18 | end 19 | 20 | describe '.parse' do 21 | subject { Openapi2ruby::Parser.parse(path) } 22 | 23 | it { is_expected.to be_a Openapi2ruby::Openapi } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/openapi2ruby_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Openapi2ruby do 4 | it 'has a version number' do 5 | expect(Openapi2ruby::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "openapi2ruby" 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = ".rspec_status" 7 | 8 | # Disable RSpec exposing methods globally on `Module` and `main` 9 | config.disable_monkey_patching! 10 | 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takanamito/openapi2ruby/aea3091a2e15ecc9a08c96bf9cb6fd198422fd6b/spec/tmp/.gitkeep --------------------------------------------------------------------------------