diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b2fa470 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby ${{ matrix.ruby-version }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + + - name: Run tests + run: bundle exec rake spec + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + + - name: Run RuboCop + run: bundle exec rubocop diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..01ba47c --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,124 @@ +AllCops: + TargetRubyVersion: 3.0 + NewCops: enable + SuggestExtensions: false + Exclude: + - 'spec/fixtures/**/*' + - 'vendor/**/*' + - 'tmp/**/*' + +Style/Documentation: + Enabled: false + +Style/FrozenStringLiteralComment: + Enabled: true + EnforcedStyle: always + Exclude: + - 'spec/**/*' + - 'Rakefile' + - 'Gemfile' + - 'bin/*' + +Metrics/MethodLength: + Max: 400 + Exclude: + - 'spec/**/*' + - 'lib/open_api_import/open_api_import.rb' + +Metrics/AbcSize: + Max: 500 + Exclude: + - 'spec/**/*' + - 'lib/open_api_import/open_api_import.rb' + +Metrics/CyclomaticComplexity: + Max: 80 + Exclude: + - 'lib/open_api_import/open_api_import.rb' + +Metrics/PerceivedComplexity: + Max: 80 + Exclude: + - 'lib/open_api_import/open_api_import.rb' + +Metrics/BlockLength: + Exclude: + - 'spec/**/*' + - 'Rakefile' + - 'bin/open_api_import' + - 'lib/open_api_import/open_api_import.rb' + - 'lib/open_api_import/get_examples.rb' + +Metrics/ClassLength: + Max: 800 + +Layout/LineLength: + Max: 200 + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes + +Naming/MethodName: + Enabled: false + +Naming/MethodParameterName: + Enabled: false + +Style/GuardClause: + Enabled: false + +Style/Next: + Enabled: false + +Style/IfUnlessModifier: + Enabled: false + +Style/ConditionalAssignment: + Enabled: false + +Style/NegatedIf: + Enabled: false + +Style/CaseLikeIf: + Enabled: false + +Style/SymbolArray: + Enabled: false + +Lint/SuppressedException: + Enabled: false + +Style/RescueStandardError: + Enabled: false + +Style/OptionalBooleanParameter: + Enabled: false + +Metrics/BlockNesting: + Enabled: false + +Metrics/ParameterLists: + Enabled: false + +Lint/DuplicateBranch: + Enabled: false + +Lint/UnreachableLoop: + Enabled: false + +Lint/EmptyFile: + Exclude: + - 'spec/open_api_import/utils/open_api_import_utils_spec.rb' + +Gemspec/DevelopmentDependencies: + Enabled: false + +Gemspec/RequiredRubyVersion: + Enabled: false + +Security/Eval: + Exclude: + - 'spec/**/*' diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5e9b0c8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,48 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.12.0] - 2026-03-17 + +### Added +- OpenAPI 3.1 support: handles nullable type arrays (`type: ["string", "null"]`) and `examples` keyword (plural) +- `OpenApiImport::ParseError` exception class for parse failures (instead of `exit!`) +- `OpenApiImport::VERSION` constant +- `return_data` option to get generated code as a Hash without writing files +- `--version` / `-v` CLI flag +- `--dry_run` / `-d` CLI flag to preview output without writing files +- GitHub Actions CI workflow (`.github/workflows/ci.yml`) testing Ruby 3.0-3.4 +- RuboCop configuration (`.rubocop.yml`) +- Unit tests for all helper modules (`get_examples`, `get_patterns`, `filter`, `get_required_data`, `pretty_hash_symbolized`, `get_data_all_of_bodies`) +- Feature tests for `mock_response`, `silent`, error handling, `return_data`, and OAS 3.1 +- `after(:suite)` cleanup in spec_helper to remove generated test artifacts +- `CHANGELOG.md` + +### Changed +- **BREAKING**: `exit!` on parse failure replaced with `raise OpenApiImport::ParseError` -- callers should rescue this +- **BREAKING**: Minimum Ruby version raised from 2.7 to 3.0 +- `rescue Exception` replaced with `rescue StandardError` throughout (no longer swallows Ctrl-C/OOM) +- `eval()` calls removed -- replaced with safe hash construction and `load` for file validation +- `String` monkey-patching (`snake_case`/`camel_case`) replaced with Ruby refinements (`OpenApiImportStringExt`) +- `include LibOpenApiImport` moved from top-level (global namespace) into `OpenApiImport` class via `extend` +- Shell commands (`rufo`, `ruby -c`) now use `Shellwords.shellescape` for path safety +- `activesupport` constraint relaxed from `~> 6.1` to `>= 6.1, < 8.0` (supports Rails 7) +- `rufo` constraint relaxed from `~> 0.16.1` to `~> 0.16` +- Input data mutations reduced (non-destructive `gsub` instead of `gsub!`, local variables instead of modifying input hashes) +- Repeated `rufo` formatting + syntax check code extracted into `format_and_check_file` helper method +- `kind_of?` standardized to `is_a?`, `.keys.include?` to `.key?` +- Ruby version comparison uses `Gem::Version` instead of string comparison +- Output messages now display relative paths (as provided by the user) instead of expanded absolute paths + +### Fixed +- Bug in `get_examples.rb`: `val.include?("'")` was checking hash keys instead of string content +- Array modification during iteration in `get_required_data.rb` (now collects and concatenates after) +- `filter.rb`: nil guard added for nested key access (`result[key] ||= {}`) +- `get_patterns`: simple-type array items (e.g., `{type: "string"}`) now correctly produce `[:'string']` patterns without relying on mutation side-effects from `get_examples` +- `build_example_value`: array types now recurse into items schema instead of returning empty `[]`, restoring type-hinted examples like `["string"]` and `[{...}]` + +### Deprecated +- Travis CI configuration (`.travis.yml`) -- superseded by GitHub Actions diff --git a/Gemfile b/Gemfile index 759cfca..6abe0a7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,10 @@ -source 'https://rubygems.org' +source "https://rubygems.org" group :test do - gem 'rake' - gem 'rspec' - gem 'coveralls_reborn', '~> 0.27.0', require: false + gem "coveralls_reborn", "~> 0.27.0", require: false + gem "rake" + gem "rspec" end # Specify your gem's dependencies in mygem.gemspec -gemspec \ No newline at end of file +gemspec diff --git a/README.md b/README.md index f87d498..b6bec36 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # OpenApiImport [![Gem Version](https://badge.fury.io/rb/open_api_import.svg)](https://rubygems.org/gems/open_api_import) -[![Build Status](https://travis-ci.com/MarioRuiz/open_api_import.svg?branch=master)](https://github.com/MarioRuiz/open_api_import) +[![CI](https://github.com/MarioRuiz/open_api_import/actions/workflows/ci.yml/badge.svg)](https://github.com/MarioRuiz/open_api_import/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/MarioRuiz/open_api_import/badge.svg?branch=master)](https://coveralls.io/github/MarioRuiz/open_api_import?branch=master) -Import a Swagger or Open API file and create a Ruby Request Hash file including all requests and responses with all the examples. The file can be in JSON or YAML. +Import a Swagger or Open API file (including Open API 3.1) and create a Ruby Request Hash file including all requests and responses with all the examples. The file can be in JSON or YAML. The Request Hash will include also the pattern (regular expressions) of the fields, parameters, default values... @@ -68,6 +68,10 @@ This is the output of the previous run: - Helper: ./spec/helper.rb ``` +## Requirements + +- Ruby >= 3.0 + ## Installation Install it yourself as: @@ -102,6 +106,7 @@ More info: https://github.com/MarioRuiz/open_api_import In case no options supplied: * It will be used the value of operation_id on snake_case for the name of the methods * It will be used the first folder of the path to create the module name + -v, --version Display the version -n, --no_responses if you don't want to add the examples of responses in the resultant file. -m, --mock Add the first response on the request as mock_response -p, --path_method it will be used the path and http method to create the method names @@ -111,6 +116,7 @@ In case no options supplied: -F, --fixed_module all the requests will be under the module Requests -s, --silent It will display only errors -c, --create_constants For required arguments, it will create keyword arguments assigning by default a constant. + -d, --dry_run Preview the generated output without writing files ``` @@ -490,6 +496,23 @@ It will include this on the output file: ... ``` +### return_data + +Instead of writing files to disk, return a Hash of `{filename => content}`. This is useful for previewing the output or programmatically processing the generated code. + +Accepts true or false, by default is false. + +```ruby + require 'open_api_import' + + result = OpenApiImport.from "./spec/fixtures/v2.0/yaml/petstore-simple.yaml", return_data: true + + result.each do |filepath, content| + puts "--- #{filepath} ---" + puts content + end +``` + ### create_constants The methods will be generated using keyword arguments and for required arguments, it will create keyword arguments assigning by default a constant. diff --git a/Rakefile b/Rakefile index 3c1419a..d956bd7 100644 --- a/Rakefile +++ b/Rakefile @@ -4,4 +4,15 @@ RSpec::Core::RakeTask.new(:spec) do |t| t.pattern = Dir.glob("spec/**/*_spec.rb") t.rspec_opts = "--format documentation" end -task default: :spec \ No newline at end of file + +begin + require "rubocop/rake_task" + RuboCop::RakeTask.new(:rubocop) +rescue LoadError + desc "RuboCop not available" + task :rubocop do + puts "RuboCop is not installed. Run: gem install rubocop" + end +end + +task default: [:spec] diff --git a/bin/open_api_import b/bin/open_api_import index 5044bf2..56ec856 100755 --- a/bin/open_api_import +++ b/bin/open_api_import @@ -1,6 +1,6 @@ #!/usr/bin/env ruby -require 'optparse' -require 'open_api_import' +require "optparse" +require "open_api_import" options = { name_for_module: :path @@ -8,12 +8,17 @@ options = { optparse = OptionParser.new do |opts| opts.banner = "Usage: open_api_import [open_api_file] [options]\n" - opts.banner+= "Import a Swagger or Open API file and create a Ruby Request Hash file including all requests and responses.\n" - opts.banner+= "More info: https://github.com/MarioRuiz/open_api_import\n\n" - opts.banner+= "In case no options supplied: \n" - opts.banner+= " * It will be used the value of operation_id on snake_case for the name of the methods\n" - opts.banner+= " * It will be used the first folder of the path to create the module name\n" - + opts.banner += "Import a Swagger or Open API file and create a Ruby Request Hash file including all requests and responses.\n" + opts.banner += "More info: https://github.com/MarioRuiz/open_api_import\n\n" + opts.banner += "In case no options supplied: \n" + opts.banner += " * It will be used the value of operation_id on snake_case for the name of the methods\n" + opts.banner += " * It will be used the first folder of the path to create the module name\n" + + opts.on("-v", "--version", "Display the version") do + puts "open_api_import #{OpenApiImport::VERSION}" + exit + end + opts.on("-n", "--no_responses", "if you don't want to add the examples of responses in the resultant file.") do options[:include_responses] = false end @@ -50,7 +55,9 @@ optparse = OptionParser.new do |opts| options[:create_constants] = true end - + opts.on("-d", "--dry_run", "Preview the generated output without writing files") do + options[:return_data] = true + end end optparse.parse! @@ -65,9 +72,16 @@ if options.key?(:create_files) end filename = ARGV.pop -if filename.to_s=='' +if filename.to_s == "" puts optparse puts "** Need to specify at least a file to import." else - OpenApiImport.from filename, **options + result = OpenApiImport.from filename, **options + if options[:return_data] && result.is_a?(Hash) + result.each do |filepath, content| + puts "--- #{filepath} ---" + puts content + puts + end + end end diff --git a/lib/open_api_import.rb b/lib/open_api_import.rb index 3e49ab6..d880697 100644 --- a/lib/open_api_import.rb +++ b/lib/open_api_import.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "open_api_import/utils" require_relative "open_api_import/filter" require_relative "open_api_import/pretty_hash_symbolized" @@ -8,10 +10,8 @@ require_relative "open_api_import/get_examples" require_relative "open_api_import/open_api_import" -include LibOpenApiImport - require "oas_parser_reborn" require "rufo" require "nice_hash" require "logger" - +require "shellwords" diff --git a/lib/open_api_import/filter.rb b/lib/open_api_import/filter.rb index e96686b..90255f5 100644 --- a/lib/open_api_import/filter.rb +++ b/lib/open_api_import/filter.rb @@ -1,28 +1,32 @@ +# frozen_string_literal: true + module LibOpenApiImport - #filter hash + private + def filter(hash, keys, nested = false) result = {} keys = [keys] unless keys.is_a?(Array) if nested result = hash.nice_filter(keys) else - #to be backwards compatible keys.each do |k| - if k.is_a?(Symbol) and hash.key?(k) + if k.is_a?(Symbol) && hash.key?(k) if hash[k].is_a?(Hash) result[k] = {} else result[k] = hash[k] end - elsif k.is_a?(Symbol) and k.to_s.include?(".") and hash.key?((k.to_s.scan(/(\w+)\./).join).to_sym) #nested 'uno.dos.tres + elsif k.is_a?(Symbol) && k.to_s.include?(".") && hash.key?(k.to_s.scan(/(\w+)\./).join.to_sym) kn = k.to_s.split(".") vn = kn[1].to_sym + result[kn.first.to_sym] ||= {} result[kn.first.to_sym][vn] = filter(hash[kn.first.to_sym], vn).values[0] - elsif k.is_a?(Hash) and hash.key?(k.keys[0]) #nested {uno: {dos: :tres}} + elsif k.is_a?(Hash) && hash.key?(k.keys[0]) + result[k.keys[0]] ||= {} result[k.keys[0]][k.values[0]] = filter(hash[k.keys[0]], k.values[0]).values[0] end end end - return result + result end end diff --git a/lib/open_api_import/get_data_all_of_bodies.rb b/lib/open_api_import/get_data_all_of_bodies.rb index cbe0719..68000c0 100644 --- a/lib/open_api_import/get_data_all_of_bodies.rb +++ b/lib/open_api_import/get_data_all_of_bodies.rb @@ -1,22 +1,26 @@ +# frozen_string_literal: true + module LibOpenApiImport - private def get_data_all_of_bodies(p) + private + + def get_data_all_of_bodies(p) bodies = [] data_examples_all_of = false if p.is_a?(Array) q = p - elsif p.key?(:schema) and p[:schema].key?(:allOf) + elsif p.key?(:schema) && p[:schema].key?(:allOf) q = p[:schema][:allOf] else q = [p] end q.each do |pt| - if pt.is_a?(Hash) and pt.key?(:allOf) + if pt.is_a?(Hash) && pt.key?(:allOf) bodies += get_data_all_of_bodies(pt[:allOf])[1] data_examples_all_of = true else bodies << pt end end - return data_examples_all_of, bodies + [data_examples_all_of, bodies] end end diff --git a/lib/open_api_import/get_examples.rb b/lib/open_api_import/get_examples.rb index 50eb745..dcf78fd 100644 --- a/lib/open_api_import/get_examples.rb +++ b/lib/open_api_import/get_examples.rb @@ -1,34 +1,41 @@ +# frozen_string_literal: true + module LibOpenApiImport - # Retrieve the examples from the properties hash - private def get_examples(properties, type = :key_value, remove_readonly = false) - #todo: consider using this method also to get data examples + private + + def get_examples(properties, type = :key_value, remove_readonly = false) example = [] - example << "{" unless properties.empty? or type == :only_value + example << "{" unless properties.empty? || (type == :only_value) properties.each do |prop, val| - unless remove_readonly and val.key?(:readOnly) and val[:readOnly] == true - if val.key?(:properties) and !val.key?(:example) and !val.key?(:type) - val[:type] = "object" + unless remove_readonly && val.key?(:readOnly) && (val[:readOnly] == true) + effective_type = val[:type] + if val.key?(:properties) && !val.key?(:example) && !val.key?(:type) + effective_type = "object" end - if val.key?(:items) and !val.key?(:example) and !val.key?(:type) - val[:type] = "array" + if val.key?(:items) && !val.key?(:example) && !val.key?(:type) + effective_type = "array" end - if val.key?(:example) - if val[:example].is_a?(Array) and val.key?(:type) and val[:type] == "string" - example << " #{prop.to_sym}: \"#{val[:example][0]}\", " # only the first example + + effective_type = Array(effective_type).reject { |t| t == "null" }.first if effective_type.is_a?(Array) + + effective_example = val[:example] + effective_example ||= val[:examples]&.first if val.key?(:examples) && val[:examples].is_a?(Array) && !val[:examples].empty? + + if effective_example + if effective_example.is_a?(Array) && val.key?(:type) && (val[:type] == "string") + example << " #{prop.to_sym}: \"#{effective_example[0]}\", " + elsif effective_example.is_a?(String) + escaped = effective_example.include?("'") ? effective_example : effective_example.gsub('"', "'") + example << " #{prop.to_sym}: \"#{escaped}\", " + elsif effective_example.is_a?(Time) + example << " #{prop.to_sym}: \"#{effective_example}\", " else - if val[:example].is_a?(String) - val[:example].gsub!('"', "'") unless val.include?("'") - example << " #{prop.to_sym}: \"#{val[:example]}\", " - elsif val[:example].is_a?(Time) - example << " #{prop.to_sym}: \"#{val[:example]}\", " - else - example << " #{prop.to_sym}: #{val[:example]}, " - end + example << " #{prop.to_sym}: #{effective_example}, " end - elsif val.key?(:type) + elsif effective_type format = val[:format] - format = val[:type] if format.to_s == "" - case val[:type].downcase + format = effective_type if format.to_s == "" + case effective_type.downcase when "string" example << " #{prop.to_sym}: \"#{format}\", " when "integer" @@ -36,60 +43,56 @@ module LibOpenApiImport when "number" format_name = format.to_s.downcase number_value = if %w[float double decimal].include?(format_name) - "0.0" - else - "0" - end + "0.0" + else + "0" + end example << " #{prop.to_sym}: #{number_value}, " when "boolean" example << " #{prop.to_sym}: true, " when "array" - if val.key?(:items) and val[:items].is_a?(Hash) and val[:items].size == 1 and val[:items].key?(:type) - val[:items][:enum] = [val[:items][:type]] - end + items_enum = if val.key?(:items) && val[:items].is_a?(Hash) && (val[:items].size == 1) && val[:items].key?(:type) + [val[:items][:type]] + elsif val.key?(:items) && !val[:items].nil? && val[:items].key?(:enum) + val[:items][:enum] + end - if val.key?(:items) and !val[:items].nil? and val[:items].key?(:enum) - #before we were getting in all these cases a random value from the enum, now we are getting the first position by default - #the reason is to avoid confusion later in case we want to compare two swaggers and verify the changes + if items_enum if type == :only_value - if val[:items][:enum][0].is_a?(String) - example << " [\"" + val[:items][:enum][0] + "\"] " + if items_enum[0].is_a?(String) + example << " [\"#{items_enum[0]}\"] " else - example << " [" + val[:items][:enum][0] + "] " + example << " [#{items_enum[0]}] " end + elsif items_enum[0].is_a?(String) + example << " #{prop.to_sym}: [\"#{items_enum[0]}\"], " else - if val[:items][:enum][0].is_a?(String) - example << " #{prop.to_sym}: [\"" + val[:items][:enum][0] + "\"], " - else - example << " #{prop.to_sym}: [" + val[:items][:enum][0] + "], " - end + example << " #{prop.to_sym}: [#{items_enum[0]}], " end else - #todo: differ between response examples and data examples examplet = get_response_examples({ schema: val }, remove_readonly).join("\n") examplet = "[]" if examplet.empty? if type == :only_value example << examplet else - example << " #{prop.to_sym}: " + examplet + ", " + example << " #{prop.to_sym}: #{examplet}, " end end when "object" - #todo: differ between response examples and data examples res_ex = get_response_examples({ schema: val }, remove_readonly) - if res_ex.size == 0 + if res_ex.empty? res_ex = "{ }" else res_ex = res_ex.join("\n") end - example << " #{prop.to_sym}: " + res_ex + ", " + example << " #{prop.to_sym}: #{res_ex}, " else example << " #{prop.to_sym}: \"#{format}\", " end end end end - example << "}" unless properties.empty? or type == :only_value + example << "}" unless properties.empty? || (type == :only_value) example end end diff --git a/lib/open_api_import/get_patterns.rb b/lib/open_api_import/get_patterns.rb index ab679bc..ae9736d 100644 --- a/lib/open_api_import/get_patterns.rb +++ b/lib/open_api_import/get_patterns.rb @@ -1,69 +1,77 @@ +# frozen_string_literal: true + module LibOpenApiImport - # Get patterns - private def get_patterns(dpk, dpv) + private + + def get_patterns(dpk, dpv) data_pattern = [] + effective_type = dpv[:type] + effective_type = Array(effective_type).reject { |t| t == "null" }.first if effective_type.is_a?(Array) + if dpv.keys.include?(:pattern) - #todo: control better the cases with back slashes if dpv[:pattern].include?('\\\\/') - #for cases like this: ^[^\.\\/:*?"<>|][^\\/:*?"<>|]{0,13}[^\.\\/:*?"<>|]?$ data_pattern << "'#{dpk}': /#{dpv[:pattern].to_s.gsub('\/', "/")}/" - elsif dpv[:pattern].match?(/\\x[0-9ABCDEF][0-9ABCDEF]\-/) + elsif dpv[:pattern].match?(/\\x[0-9ABCDEF][0-9ABCDEF]-/) data_pattern << "'#{dpk}': /#{dpv[:pattern].to_s.gsub('\\x', '\\u00')}/" elsif dpv[:pattern].include?('\\x') data_pattern << "'#{dpk}': /#{dpv[:pattern].to_s.gsub('\\x', '\\u')}/" else - data_pattern << "'#{dpk}': /#{dpv[:pattern].to_s}/" + data_pattern << "'#{dpk}': /#{dpv[:pattern]}/" end - elsif dpv.key?(:minLength) and dpv.key?(:maxLength) + elsif dpv.key?(:minLength) && dpv.key?(:maxLength) data_pattern << "'#{dpk}': :'#{dpv[:minLength]}-#{dpv[:maxLength]}:LN$'" - elsif dpv.key?(:minLength) and !dpv.key?(:maxLength) + elsif dpv.key?(:minLength) && !dpv.key?(:maxLength) data_pattern << "'#{dpk}': :'#{dpv[:minLength]}:LN$'" - elsif !dpv.key?(:minLength) and dpv.key?(:maxLength) + elsif !dpv.key?(:minLength) && dpv.key?(:maxLength) data_pattern << "'#{dpk}': :'0-#{dpv[:maxLength]}:LN$'" - elsif dpv.key?(:minimum) and dpv.key?(:maximum) and dpv[:type] == "string" + elsif dpv.key?(:minimum) && dpv.key?(:maximum) && (effective_type == "string") data_pattern << "'#{dpk}': :'#{dpv[:minimum]}-#{dpv[:maximum]}:LN$'" - elsif dpv.key?(:minimum) and dpv.key?(:maximum) + elsif dpv.key?(:minimum) && dpv.key?(:maximum) data_pattern << "'#{dpk}': #{dpv[:minimum]}..#{dpv[:maximum]}" - elsif dpv.key?(:minimum) and !dpv.key?(:maximum) - if RUBY_VERSION >= "2.6.0" + elsif dpv.key?(:minimum) && !dpv.key?(:maximum) + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0") data_pattern << "'#{dpk}': #{dpv[:minimum]}.. " else data_pattern << "#'#{dpk}': #{dpv[:minimum]}.. # INFINITE only working on ruby>=2.6.0" end - elsif !dpv.key?(:minimum) and dpv.key?(:maximum) + elsif !dpv.key?(:minimum) && dpv.key?(:maximum) data_pattern << "'#{dpk}': 0..#{dpv[:maximum]}" elsif dpv[:format] == "date-time" data_pattern << "'#{dpk}': DateTime" - elsif dpv[:type] == "boolean" + elsif effective_type == "boolean" data_pattern << "'#{dpk}': Boolean" elsif dpv.key?(:enum) data_pattern << "'#{dpk}': :'#{dpv[:enum].join("|")}'" - elsif dpv[:type] == "array" and dpv.key?(:items) and dpv[:items].is_a?(Hash) and dpv[:items].key?(:enum) and dpv[:items][:enum].is_a?(Array) - #{:title=>"Balala", :type=>"array", :items=>{:type=>"string", :enum=>["uno","dos"], :example=>"uno"}} + elsif (effective_type == "array") && dpv.key?(:items) && dpv[:items].is_a?(Hash) && dpv[:items].key?(:enum) && dpv[:items][:enum].is_a?(Array) data_pattern << "'#{dpk}': [:'#{dpv[:items][:enum].join("|")}']" - elsif dpv[:type] == "array" and dpv.key?(:items) and dpv[:items].is_a?(Hash) and !dpv[:items].key?(:enum) and dpv[:items].key?(:properties) - #{:title=>"Balala", :type=>"array", :items=>{title: 'xxxx, properties: {server: {enum:['ibm','msa','pytan']}}} + elsif (effective_type == "array") && dpv.key?(:items) && dpv[:items].is_a?(Hash) && !dpv[:items].key?(:enum) && dpv[:items].key?(:properties) dpv[:items][:properties].each do |dpkk, dpvv| if dpk == "" - data_pattern += get_patterns("#{dpkk}", dpvv) + data_pattern += get_patterns(dpkk.to_s, dpvv) else data_pattern += get_patterns("#{dpk}.#{dpkk}", dpvv) end end - elsif dpv[:type] == "array" and dpv.key?(:items) and dpv[:items].is_a?(Hash) and - !dpv[:items].key?(:enum) and !dpv[:items].key?(:properties) and dpv[:items].key?(:type) - #{:title=>"labels", :description=>"Labels specified for the file system", :type=>"array", :items=>{:type=>"string", :enum=>["string"]}} - data_pattern << "'#{dpk}': [ #{get_patterns("", dpv[:items]).join[4..-1]} ]" - elsif dpv[:type] == "object" and dpv.key?(:properties) + elsif (effective_type == "array") && dpv.key?(:items) && dpv[:items].is_a?(Hash) && + !dpv[:items].key?(:enum) && !dpv[:items].key?(:properties) && dpv[:items].key?(:type) + result = get_patterns("", dpv[:items]) + if result.empty? + item_type = dpv[:items][:type] + item_type = Array(item_type).reject { |t| t == "null" }.first if item_type.is_a?(Array) + data_pattern << "'#{dpk}': [:'#{item_type}']" + else + data_pattern << "'#{dpk}': [ #{result.join[4..]} ]" + end + elsif (effective_type == "object") && dpv.key?(:properties) dpv[:properties].each do |dpkk, dpvv| if dpk == "" - data_pattern += get_patterns("#{dpkk}", dpvv) + data_pattern += get_patterns(dpkk.to_s, dpvv) else data_pattern += get_patterns("#{dpk}.#{dpkk}", dpvv) end end end data_pattern.uniq! - return data_pattern + data_pattern end end diff --git a/lib/open_api_import/get_required_data.rb b/lib/open_api_import/get_required_data.rb index b7d4fbf..eedd21a 100644 --- a/lib/open_api_import/get_required_data.rb +++ b/lib/open_api_import/get_required_data.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + module LibOpenApiImport - # Get required data - private def get_required_data(body) + private + + def get_required_data(body) data_required = [] - if body.keys.include?(:required) and body[:required].size > 0 + if body.key?(:required) && body[:required].size.positive? body[:required].each do |r| data_required << r.to_sym end @@ -16,15 +19,17 @@ module LibOpenApiImport end end end + nested_required = [] data_required.each do |key| - if body.key?(:properties) and body[:properties][key].is_a?(Hash) and - body[:properties][key].key?(:required) and body[:properties][key][:required].size > 0 + if body.key?(:properties) && body[:properties][key].is_a?(Hash) && + body[:properties][key].key?(:required) && body[:properties][key][:required].size.positive? dr = get_required_data(body[:properties][key]) dr.each do |k| - data_required.push("#{key}.#{k}".to_sym) + nested_required << :"#{key}.#{k}" end end end - return data_required + data_required.concat(nested_required) + data_required end end diff --git a/lib/open_api_import/get_response_examples.rb b/lib/open_api_import/get_response_examples.rb index cbf0ce2..4345a34 100644 --- a/lib/open_api_import/get_response_examples.rb +++ b/lib/open_api_import/get_response_examples.rb @@ -1,38 +1,42 @@ +# frozen_string_literal: true + module LibOpenApiImport + private + # Retrieve the response examples from the hash - private def get_response_examples(v, remove_readonly = false) + def get_response_examples(v, remove_readonly = false) # TODO: take in consideration the case allOf, oneOf... schema.items.allOf[0].properties schema.items.allOf[1].properties # example on https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v2.0/yaml/petstore-expanded.yaml v = v.dup - response_example = Array.new() + response_example = [] # for open api 3.0 with responses schema inside content - if v.key?(:content) && v[:content].is_a?(Hash) && v[:content].key?(:'application/json') && - v[:content][:'application/json'].key?(:schema) - v = v[:content][:'application/json'].dup + if v.key?(:content) && v[:content].is_a?(Hash) && v[:content].key?(:"application/json") && + v[:content][:"application/json"].key?(:schema) + v = v[:content][:"application/json"].dup end - if v.key?(:examples) && v[:examples].is_a?(Hash) && v[:examples].key?(:'application/json') - if v[:examples][:'application/json'].is_a?(String) - response_example << v[:examples][:'application/json'] - elsif v[:examples][:'application/json'].is_a?(Hash) - exs = v[:examples][:'application/json'].to_s + if v.key?(:examples) && v[:examples].is_a?(Hash) && v[:examples].key?(:"application/json") + if v[:examples][:"application/json"].is_a?(String) + response_example << v[:examples][:"application/json"] + elsif v[:examples][:"application/json"].is_a?(Hash) + exs = v[:examples][:"application/json"].to_s exs.gsub!(/:(\w+)=>/, "\n\\1: ") response_example << exs - elsif v[:examples][:'application/json'].is_a?(Array) + elsif v[:examples][:"application/json"].is_a?(Array) response_example << "[" - v[:examples][:'application/json'].each do |ex| + v[:examples][:"application/json"].each do |ex| exs = ex.to_s if ex.is_a?(Hash) exs.gsub!(/:(\w+)=>/, "\n\\1: ") end - response_example << (exs + ", ") + response_example << "#{exs}, " end response_example << "]" end # for open api 3.0. examples on reponses, for example: api-with-examples.yaml - elsif v.key?(:content) && v[:content].is_a?(Hash) && v[:content].key?(:'application/json') && - v[:content][:'application/json'].key?(:examples) - v[:content][:'application/json'][:examples].each do |tk, tv| - #todo: for the moment we only take in consideration the first example of response. + elsif v.key?(:content) && v[:content].is_a?(Hash) && v[:content].key?(:"application/json") && + v[:content][:"application/json"].key?(:examples) + v[:content][:"application/json"][:examples].each_value do |tv| + # TODO: for the moment we only take in consideration the first example of response. # we need to decide how to manage to do it correctly if tv.key?(:value) tresp = tv[:value] @@ -52,11 +56,11 @@ module LibOpenApiImport if ex.is_a?(Hash) exs.gsub!(/:(\w+)=>/, "\n\\1: ") end - response_example << (exs + ", ") + response_example << "#{exs}, " end response_example << "]" end - break #only the first one it is considered + break # only the first one it is considered end elsif v.key?(:schema) && v[:schema].is_a?(Hash) && (v[:schema].key?(:properties) || @@ -82,23 +86,19 @@ module LibOpenApiImport response_example += get_examples(properties, :key_value, remove_readonly) unless properties.empty? - unless response_example.empty? - if v[:schema].key?(:properties) || v[:schema].key?(:allOf) - # - else # array, items - response_example << "]" - end + if !response_example.empty? && !(v[:schema].key?(:properties) || v[:schema].key?(:allOf)) # array, items + response_example << "]" end - elsif v.key?(:schema) and v[:schema].key?(:items) and v[:schema][:items].is_a?(Hash) and v[:schema][:items].key?(:type) + elsif v.key?(:schema) && v[:schema].key?(:items) && v[:schema][:items].is_a?(Hash) && v[:schema][:items].key?(:type) # for the case only type supplied but nothing else for the array response_example << "[\"#{v[:schema][:items][:type]}\"]" end response_example.each do |rs| - #(@type Google) for the case in example the key is something like: @type: + # (@type Google) for the case in example the key is something like: @type: if rs.match?(/^\s*@\w+:/) rs.gsub!(/@(\w+):/, '\'@\1\':') end end - return response_example + response_example end end diff --git a/lib/open_api_import/open_api_import.rb b/lib/open_api_import/open_api_import.rb index 769c34a..5f39bba 100644 --- a/lib/open_api_import/open_api_import.rb +++ b/lib/open_api_import/open_api_import.rb @@ -1,4 +1,14 @@ +# frozen_string_literal: true + +using OpenApiImportStringExt + class OpenApiImport + class ParseError < StandardError; end + + VERSION = "0.12.0" + + extend LibOpenApiImport + ############################################################################################## # Import a Swagger or Open API file and create a Ruby Request Hash file including all requests and responses. # The http methods that will be treated are: 'get','post','put','delete', 'patch'. @@ -18,8 +28,10 @@ class OpenApiImport # tags: It will be used the tags key to create the module name, for example the tags: [users,list] will create the module UsersList and all the requests from all modules in the same file. # tags_file: It will be used the tags key to create the module name, for example the tags: [users,list] will create the module UsersList and and each module will be in a new requests file. # fixed: all the requests will be under the module Requests + # @param return_data [Boolean]. (default: false) Instead of writing files, return a Hash of {filename => content}. ############################################################################################## - def self.from(swagger_file, create_method_name: :operation_id, include_responses: true, mock_response: false, name_for_module: :path, silent: false, create_constants: false) + def self.from(swagger_file, create_method_name: :operation_id, include_responses: true, mock_response: false, + name_for_module: :path, silent: false, create_constants: false, return_data: false) begin f = File.new("#{swagger_file}_open_api_import.log", "w") f.sync = true @@ -35,59 +47,53 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses @logger.info "swagger_file: #{swagger_file}, include_responses: #{include_responses}, mock_response: #{mock_response}\n" @logger.info "create_method_name: #{create_method_name}, name_for_module: #{name_for_module}\n" - file_to_convert = if swagger_file["./"].nil? - swagger_file - else - Dir.pwd.to_s + "/" + swagger_file.gsub("./", "") - end + file_to_convert = File.expand_path(swagger_file) unless File.exist?(file_to_convert) raise "The file #{file_to_convert} doesn't exist" end - file_errors = file_to_convert + ".errors.log" - File.delete(file_errors) if File.exist?(file_errors) + file_errors = "#{file_to_convert}.errors.log" + FileUtils.rm_f(file_errors) import_errors = "" required_constants = [] begin definition = OasParser::Definition.resolve(swagger_file) - rescue Exception => stack + rescue StandardError => e message = "There was a problem parsing the Open Api document using the oas_parser_reborn gem. The execution was aborted.\n" message += "Visit the github for oas_parser_reborn gem for bugs and more info: https://github.com/MarioRuiz/oas_parser_reborn\n" - message += "Error: #{stack.message}" - puts message + message += "Error: #{e.message}" @logger.fatal message - @logger.fatal stack.backtrace.join("\n") - exit! + @logger.fatal e.backtrace.join("\n") + raise ParseError, message end raw = definition.raw.deep_symbolize_keys - if raw.key?(:openapi) && (raw[:openapi].to_f > 0) + if raw.key?(:openapi) && raw[:openapi].to_f.positive? raw[:swagger] = raw[:openapi] end if raw[:swagger].to_f < 2.0 raise "Unsupported Swagger version. Only versions >= 2.0 are valid." end - base_host = "" base_path = "" - base_host = raw[:host] if raw.key?(:host) + raw[:host] if raw.key?(:host) base_path = raw[:basePath] if raw.key?(:basePath) module_name = raw[:info][:title].camel_case module_version = "V#{raw[:info][:version].to_s.snake_case}" output = [] output_header = [] - output_header << "#" * 50 + output_header << ("#" * 50) output_header << "# #{raw[:info][:title]}" output_header << "# version: #{raw[:info][:version]}" output_header << "# description: " raw[:info][:description].to_s.split("\n").each do |d| output_header << "# #{d}" unless d == "" end - output_header << "#" * 50 + output_header << ("#" * 50) output_header << "module Swagger" output_header << "module #{module_name}" @@ -102,10 +108,9 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses raw = path.raw.deep_symbolize_keys if raw.key?(:parameters) - raw.each do |met, cont| + raw.each_key do |met| if met != :parameters if raw[met].key?(:parameters) - #in case parameters for all methods in path is present raw[met][:parameters] = raw[met][:parameters] + raw[:parameters] else raw[met][:parameters] = raw[:parameters] @@ -124,85 +129,76 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses params_data = [] description_parameters = [] data_form = [] + data_form_hash = {} data_required = [] - #todo: add nested one.true.three to data_read_only data_read_only = [] data_default = [] data_examples = [] + data_examples_hashes = [] data_pattern = [] responses = [] - # for the case operationId is missing cont[:operationId] = "undefined" unless cont.key?(:operationId) if create_method_name == :path - method_name = (met.to_s + "_" + path.path.to_s).snake_case + method_name = "#{met}_#{path.path}".snake_case method_name.chop! if method_name[-1] == "_" elsif create_method_name == :operation_id - if (name_for_module == :tags or name_for_module == :tags_file) and cont.key?(:tags) and cont[:tags].is_a?(Array) and cont[:tags].size > 0 + if ((name_for_module == :tags) || (name_for_module == :tags_file)) && cont.key?(:tags) && cont[:tags].is_a?(Array) && cont[:tags].size.positive? metnametmp = cont[:operationId].gsub(/^#{cont[:tags].join}[\s_]*/, "") - cont[:tags].join.split(" ").each do |tag| - metnametmp.gsub!(/^#{tag}[\s_]*/i, "") + cont[:tags].join.split.each do |tag| + metnametmp = metnametmp.gsub(/^#{tag}[\s_]*/i, "") end metnametmp = met if metnametmp == "" else metnametmp = cont[:operationId] end method_name = metnametmp.to_s.snake_case - else - if (name_for_module == :tags or name_for_module == :tags_file) and cont.key?(:tags) and cont[:tags].is_a?(Array) and cont[:tags].size > 0 - method_name = cont[:operationId].gsub(/^#{cont[:tags].join}[\s_]*/, "") - cont[:tags].join.split(" ").each do |tag| - method_name.gsub!(/^#{tag}[\s_]*/i, "") - end - method_name = met if method_name == "" - else - method_name = cont[:operationId] + elsif ((name_for_module == :tags) || (name_for_module == :tags_file)) && cont.key?(:tags) && cont[:tags].is_a?(Array) && cont[:tags].size.positive? + method_name = cont[:operationId].gsub(/^#{cont[:tags].join}[\s_]*/, "") + cont[:tags].join.split.each do |tag| + method_name = method_name.gsub(/^#{tag}[\s_]*/i, "") end + method_name = met if method_name == "" + else + method_name = cont[:operationId] end path_txt = path.path.dup.to_s if [:path, :path_file, :tags, :tags_file].include?(name_for_module) old_module_requests = module_requests if [:path, :path_file].include?(name_for_module) - # to remove version from path fex: /v1/Customer - path_requests = path_txt.gsub(/^\/v[\d\.]*\//i, "") - # to remove version from path fex: /1.0/Customer - path_requests = path_requests.gsub(/^\/[\d\.]*\//i, "") + path_requests = path_txt.gsub(%r{^/v[\d.]*/}i, "") + path_requests = path_requests.gsub(%r{^/[\d.]*/}i, "") if (path_requests == path_txt) && (path_txt.scan("/").size == 1) - # no folder in path module_requests = "Root" else res_path = path_requests.scan(/(\w+)/) module_requests = res_path[0][0].camel_case end + elsif cont.key?(:tags) && cont[:tags].is_a?(Array) && cont[:tags].size.positive? + module_requests = cont[:tags].join(" ").camel_case else - if cont.key?(:tags) and cont[:tags].is_a?(Array) and cont[:tags].size > 0 - module_requests = cont[:tags].join(" ").camel_case - else - module_requests = "Undefined" - end + module_requests = "Undefined" end - # to remove from method_name: v1_list_regions and add it to module if /^(?v\d+)/i =~ method_name - method_name.gsub!(/^#{vers}_?/, "") + method_name = method_name.gsub(/^#{vers}_?/, "") module_requests = (vers.capitalize + module_requests).camel_case unless module_requests.start_with?(vers) end if old_module_requests != module_requests - output << "end" unless old_module_requests == "" or name_for_module == :path_file or name_for_module == :tags_file - if name_for_module == :path or name_for_module == :tags - # to add the end for the previous module unless is the first one + output << "end" unless (old_module_requests == "") || (name_for_module == :path_file) || (name_for_module == :tags_file) + if (name_for_module == :path) || (name_for_module == :tags) output << "module #{module_requests}" - else #:path_file, :tags_file + else # :path_file, :tags_file if old_module_requests != "" unless files.key?(old_module_requests) - files[old_module_requests] = Array.new + files[old_module_requests] = [] end files[old_module_requests].concat(output) - output = Array.new + output = [] end - output << "module #{module_requests}" unless files.key?(module_requests) # don't add in case already existed + output << "module #{module_requests}" unless files.key?(module_requests) end end end @@ -210,60 +206,57 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses output << "" output << "# operationId: #{cont[:operationId]}, method: #{met}" output << "# summary: #{cont[:summary].split("\n").join("\n# ")}" if cont.key?(:summary) - if !cont[:description].to_s.split("\n").empty? + if cont[:description].to_s.split("\n").empty? + output << "# description: #{cont[:description]}" + else output << "# description: " cont[:description].to_s.split("\n").each do |d| output << "# #{d}" unless d == "" end - else - output << "# description: #{cont[:description]}" end mock_example = [] if include_responses && cont.key?(:responses) && cont[:responses].is_a?(Hash) cont[:responses].each do |k, v| - response_example = [] response_example = get_response_examples(v) data_pattern += get_patterns("", v[:schema]) if v.key?(:schema) data_pattern.uniq! - v[:description] = v[:description].to_s.gsub("'", %q(\\\')) - if !response_example.empty? + resp_description = v[:description].to_s.gsub("'", %q(\\\')) + if response_example.empty? + responses << "'#{k}': { message: '#{resp_description}'}, " + else responses << "'#{k}': { " - responses << "message: '#{v[:description]}', " + responses << "message: '#{resp_description}', " responses << "data: " responses << response_example responses << "}," - if mock_response and mock_example.size == 0 + if mock_response && mock_example.empty? mock_example << "code: '#{k}'," - mock_example << "message: '#{v[:description]}'," + mock_example << "message: '#{resp_description}'," mock_example << "data: " mock_example << response_example end - else - responses << "'#{k}': { message: '#{v[:description]}'}, " end end end - # todo: for open api 3.0 add the new Link feature: https://swagger.io/docs/specification/links/ - # todo: for open api 3.0 is not getting the required params in all cases - # for the case open api 3 with cont.requestBody.content.'applicatin/json'.schema - # example: petstore-expanded.yaml operationId=addPet - if cont.key?(:requestBody) and cont[:requestBody].key?(:content) and - cont[:requestBody][:content].key?(:'application/json') and cont[:requestBody][:content][:'application/json'].key?(:schema) + if cont.key?(:requestBody) && cont[:requestBody].key?(:content) && + cont[:requestBody][:content].key?(:"application/json") && cont[:requestBody][:content][:"application/json"].key?(:schema) cont[:parameters] = [] unless cont.key?(:parameters) - cont[:parameters] << { in: "body", schema: cont[:requestBody][:content][:'application/json'][:schema] } + cont[:parameters] << { in: "body", schema: cont[:requestBody][:content][:"application/json"][:schema] } end data_examples_all_of = false if cont.key?(:parameters) && cont[:parameters].is_a?(Array) cont[:parameters].each do |p| - if p.keys.include?(:schema) and p[:schema].include?(:type) + if p.keys.include?(:schema) && p[:schema].include?(:type) type = p[:schema][:type] + type = Array(type).reject { |t| t == "null" }.first if type.is_a?(Array) elsif p.keys.include?(:type) type = p[:type] + type = Array(type).reject { |t| t == "null" }.first if type.is_a?(Array) else type = "" end @@ -271,10 +264,10 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses if p[:in] == "path" if create_method_name == :operationId param_name = p[:name] - path_txt.gsub!("{#{param_name}}", "\#{#{param_name}}") + path_txt = path_txt.gsub("{#{param_name}}", "\#{#{param_name}}") else param_name = p[:name].to_s.snake_case - path_txt.gsub!("{#{p[:name]}}", "\#{#{param_name}}") + path_txt = path_txt.gsub("{#{p[:name]}}", "\#{#{param_name}}") end unless params_path.include?(param_name) if create_constants @@ -283,7 +276,6 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses else params_path << param_name end - #params_required << param_name if p[:required].to_s=="true" @logger.warn "Description key is missing for #{met} #{path.path} #{p[:name]}" if p[:description].nil? description_parameters << "# #{p[:name]}: (#{type}) #{"(required)" if p[:required].to_s == "true"} #{p[:description].to_s.split("\n").join("\n#\t\t\t")}" end @@ -292,22 +284,22 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses params_required << p[:name] if p[:required].to_s == "true" @logger.warn "Description key is missing for #{met} #{path.path} #{p[:name]}" if p[:description].nil? description_parameters << "# #{p[:name]}: (#{type}) #{"(required)" if p[:required].to_s == "true"} #{p[:description].to_s.split("\n").join("\n#\t\t\t")}" - elsif p[:in] == "formData" or p[:in] == "formdata" - #todo: take in consideration: default, required - #todo: see if we should add the required as params to the method and not required as options - #todo: set on data the required fields with the values from args - + elsif (p[:in] == "formData") || (p[:in] == "formdata") description_parameters << "# #{p[:name]}: (#{p[:type]}) #{p[:description].split("\n").join("\n#\t\t\t")}" case p[:type] when /^string$/i data_form << "#{p[:name]}: ''" + data_form_hash[p[:name].to_sym] = "" when /^boolean$/i data_form << "#{p[:name]}: true" + data_form_hash[p[:name].to_sym] = true when /^number$/i data_form << "#{p[:name]}: 0" + data_form_hash[p[:name].to_sym] = 0 when /^integer$/i data_form << "#{p[:name]}: 0" + data_form_hash[p[:name].to_sym] = 0 else puts "! on formData not supported type #{p[:type]}" end @@ -319,18 +311,19 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses bodies = p[:schema][:anyOf] elsif p[:schema].key?(:allOf) data_examples_all_of, bodies = get_data_all_of_bodies(p) - bodies.unshift(p[:schema]) if p[:schema].key?(:required) or p.key?(:required) #todo: check if this 'if' is necessary - data_examples_all_of = true # because we are on data and allOf already + bodies.unshift(p[:schema]) if p[:schema].key?(:required) || p.key?(:required) + data_examples_all_of = true else bodies = [p[:schema]] end params_data = [] + params_data_hash = {} bodies.each do |body| data_required += get_required_data(body) all_properties = [] - all_properties << body[:properties] if body.keys.include?(:properties) and body[:properties].size > 0 + all_properties << body[:properties] if body.keys.include?(:properties) && body[:properties].size.positive? if body.key?(:allOf) body[:allOf].each do |item| all_properties << item[:properties] if item.key?(:properties) @@ -338,30 +331,28 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses end all_properties.each do |props| - props.each { |dpk, dpv| + props.each do |dpk, dpv| if dpv.keys.include?(:example) - if dpv[:example].is_a?(Array) and dpv.type != "array" + if dpv[:example].is_a?(Array) && (dpv.type != "array") valv = dpv[:example][0] else valv = dpv[:example].to_s end - else - if dpv.type == "object" - if dpv.key?(:properties) - valv = get_examples(dpv[:properties], :key_value, true).join("\n") - else - valv = "{}" - end - elsif dpv.type == "array" - if dpv.key?(:items) - valv = get_examples({ dpk => dpv }, :only_value) - valv = valv.join("\n") - else - valv = "[]" - end + elsif dpv.type == "object" + if dpv.key?(:properties) + valv = get_examples(dpv[:properties], :key_value, true).join("\n") + else + valv = "{}" + end + elsif dpv.type == "array" + if dpv.key?(:items) + valv = get_examples({ dpk => dpv }, :only_value) + valv = valv.join("\n") else - valv = "" + valv = "[]" end + else + valv = "" end if dpv.keys.include?(:description) @@ -372,7 +363,7 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses data_pattern.uniq! dpkeys = [] data_pattern.reject! do |dp| - dpkey = dp.scan(/^'[\w\.]+'/) + dpkey = dp.scan(/^'[\w.]+'/) if dpkeys.include?(dpkey) true @@ -382,7 +373,7 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses end end - if dpv.keys.include?(:readOnly) and dpv[:readOnly] == true + if dpv.keys.include?(:readOnly) && (dpv[:readOnly] == true) data_read_only << dpk end if dpv.keys.include?(:default) @@ -395,28 +386,34 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses end end - #todo: consider check default and insert it - #todo: remove array from here and add the option to get_examples for the case thisisthekey: ['xxxx'] - if dpv.key?(:type) and dpv[:type] != "array" + params_data_hash[dpk] = build_example_value(dpv) + + if dpv.key?(:type) && (dpv[:type] != "array") params_data << get_examples({ dpk => dpv }, :only_value, true).join - params_data[-1].chop!.chop! if params_data[-1].to_s[-2..-1] == ", " + params_data[-1].chop!.chop! if params_data[-1].to_s[-2..] == ", " params_data.pop if params_data[-1].match?(/^\s*$/im) else if valv.to_s == "" valv = '""' elsif valv.include?('"') - valv.gsub!('"', "'") unless valv.include?("'") + valv = valv.gsub('"', "'") unless valv.include?("'") end params_data << "#{dpk}: #{valv}" end - } - if params_data.size > 0 - if data_examples_all_of == true and data_examples.size > 0 + end + if params_data.size.positive? + if (data_examples_all_of == true) && data_examples.size.positive? data_examples[0] += params_data else data_examples << params_data end + if (data_examples_all_of == true) && data_examples_hashes.size.positive? + data_examples_hashes[0].merge!(params_data_hash) + else + data_examples_hashes << params_data_hash.dup + end params_data = [] + params_data_hash = {} end end end @@ -427,7 +424,7 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses end end elsif p[:in] == "header" - #todo: see how we can treat those cases + # TODO: see how we can treat those cases else puts "! not imported data with :in:#{p[:in]} => #{p.inspect}" end @@ -450,15 +447,13 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses required_constants << pr.to_s.snake_case.upcase end end - else - if params_query.include?(pr) - if create_method_name == :operationId - path_txt += "#{pr}=\#{#{pr}}&" - params << "#{pr}" - else - path_txt += "#{pr}=\#{#{pr.to_s.snake_case}}&" - params << "#{pr.to_s.snake_case}" - end + elsif params_query.include?(pr) + if create_method_name == :operationId + path_txt += "#{pr}=\#{#{pr}}&" + params << pr.to_s + else + path_txt += "#{pr}=\#{#{pr.to_s.snake_case}}&" + params << pr.to_s.snake_case.to_s end end end @@ -476,23 +471,17 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses end end - if description_parameters.size > 0 + if description_parameters.size.positive? output << "# parameters description: " output << description_parameters.uniq end - #for the case we still have some parameters on path that were not in 'parameters' - if path_txt.scan(/[^#]{\w+}/).size > 0 + if path_txt.scan(/[^#]{\w+}/).size.positive? paramst = [] prms = path_txt.scan(/[^#]{(\w+)}/) prms.each do |p| - #if create_constants - # paramst<<"#{p[0].to_s.snake_case}: #{p[0].to_s.snake_case.upcase}" - # required_constants << p[0].to_s.snake_case.upcase - #else paramst << p[0].to_s.snake_case - #end - path_txt.gsub!("{#{p[0]}}", "\#{#{p[0].to_s.snake_case}}") + path_txt = path_txt.gsub("{#{p[0]}}", "\#{#{p[0].to_s.snake_case}}") end paramst.concat params params = paramst @@ -533,6 +522,7 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses unless data_form.empty? data_examples << data_form + data_examples_hashes << data_form_hash unless data_form_hash.empty? end unless data_examples.empty? @@ -540,18 +530,15 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses reqdata = [] begin data_examples[0].uniq! - data_ex = eval("{#{data_examples[0].join(", ")}}") - rescue SyntaxError - data_ex = {} - @logger.warn "Syntax error: #{met} for path: #{path.path} evaluating data_examples[0] => #{data_examples[0].inspect}" - rescue + data_ex = data_examples_hashes[0] || {} + rescue StandardError => e data_ex = {} - @logger.warn "Syntax error: #{met} for path: #{path.path} evaluating data_examples[0] => #{data_examples[0].inspect}" + @logger.warn "Error processing data examples: #{met} for path: #{path.path} => #{e.message}" end - if (data_required.grep(/\./)).empty? - reqdata = filter(data_ex, data_required) #not nested + if data_required.grep(/\./).empty? + reqdata = filter(data_ex, data_required) else - reqdata = filter(data_ex, data_required, true) #nested + reqdata = filter(data_ex, data_required, true) end unless reqdata.empty? reqdata.uniq! @@ -560,16 +547,15 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses output += phsd end end - unless data_read_only.empty? or !data_required.empty? + unless data_read_only.empty? || !data_required.empty? reqdata = [] - #remove read only fields from :data data_examples[0].each do |edata| read_only = false data_read_only.each do |rdata| - if edata.scan(/^#{rdata}:/).size > 0 + if edata.scan(/^#{rdata}:/).size.positive? read_only = true break - elsif edata.scan(/:/).size == 0 + elsif edata.scan(":").empty? break end end @@ -614,24 +600,29 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses end output_footer = [] - output_footer << "end" unless (module_requests == "") && ([:path, :path_file, :tags, :tags_file].include?(name_for_module)) + output_footer << "end" unless (module_requests == "") && [:path, :path_file, :tags, :tags_file].include?(name_for_module) output_footer << "end" << "end" << "end" - if files.size == 0 and !create_constants + + generated_files = {} + + if files.empty? && !create_constants output = output_header + output + output_footer output_txt = output.join("\n") - requests_file_path = file_to_convert + ".rb" - File.open(requests_file_path, "w") { |file| file.write(output_txt) } - res_rufo = `rufo #{requests_file_path}` - message = "** Requests file: #{swagger_file}.rb that contains the code of the requests after importing the Swagger file" - puts message unless silent - @logger.info message - @logger.error " Error formating with rufo" unless res_rufo.to_s.match?(/\AFormat:.+$\s*\z/) - @logger.error " Syntax Error: #{`ruby -c #{requests_file_path}`}" unless `ruby -c #{requests_file_path}`.include?("Syntax OK") + requests_file_path = "#{file_to_convert}.rb" + if return_data + generated_files[requests_file_path] = output_txt + else + File.write(requests_file_path, output_txt) + format_and_check_file(requests_file_path, @logger) + message = "** Requests file: #{swagger_file}.rb that contains the code of the requests after importing the Swagger file" + puts message unless silent + @logger.info message + end else unless files.key?(module_requests) - files[module_requests] = Array.new + files[module_requests] = [] end - files[module_requests].concat(output) #for the last one + files[module_requests].concat(output) requires_txt = "" message = "** Generated files that contain the code of the requests after importing the Swagger file: " @@ -640,19 +631,22 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses files.each do |mod, out_mod| output = output_header + out_mod + output_footer output_txt = output.join("\n") - requests_file_path = file_to_convert + "_" + mod + ".rb" + requests_file_path = "#{file_to_convert}_#{mod}.rb" requires_txt += "require_relative '#{File.basename(swagger_file)}_#{mod}'\n" - File.open(requests_file_path, "w") { |file| file.write(output_txt) } - res_rufo = `rufo #{requests_file_path}` - message = " - #{requests_file_path}" - puts message unless silent - @logger.info message - @logger.error " Error formating with rufo" unless res_rufo.to_s.match?(/\AFormat:.+$\s*\z/) - @logger.error " Syntax Error: #{`ruby -c #{requests_file_path}`}" unless `ruby -c #{requests_file_path}`.include?("Syntax OK") + if return_data + generated_files[requests_file_path] = output_txt + else + File.write(requests_file_path, output_txt) + format_and_check_file(requests_file_path, @logger) + display_path = "#{swagger_file}_#{mod}.rb" + message = " - #{display_path}" + puts message unless silent + @logger.info message + end end - requests_file_path = file_to_convert + ".rb" - if required_constants.size > 0 + requests_file_path = "#{file_to_convert}.rb" + if required_constants.size.positive? rconsts = "# Required constants\n" required_constants.uniq! required_constants.each do |rq| @@ -663,37 +657,88 @@ def self.from(swagger_file, create_method_name: :operation_id, include_responses rconsts = "" end - File.open(requests_file_path, "w") { |file| file.write(rconsts + requires_txt) } - res_rufo = `rufo #{requests_file_path}` - message = "** File that contains all the requires for all Request files: \n" - message += " - #{requests_file_path} " - puts message unless silent - @logger.info message - @logger.error " Error formating with rufo" unless res_rufo.to_s.match?(/\AFormat:.+$\s*\z/) - @logger.error " Syntax Error: #{`ruby -c #{requests_file_path}`}" unless `ruby -c #{requests_file_path}`.include?("Syntax OK") + if return_data + generated_files[requests_file_path] = rconsts + requires_txt + else + File.write(requests_file_path, rconsts + requires_txt) + format_and_check_file(requests_file_path, @logger) + message = "** File that contains all the requires for all Request files: \n" + message += " - #{swagger_file}.rb " + puts message unless silent + @logger.info message + end end + return generated_files if return_data + begin - res = eval(output_txt) - rescue Exception => stack - import_errors += "\n\nResult evaluating the ruby file generated: \n" + stack.to_s + load File.expand_path(requests_file_path) + rescue StandardError => e + import_errors += "\n\nResult evaluating the ruby file generated: \n#{e}" end - if import_errors.to_s != "" - File.open(file_errors, "w") { |file| file.write(import_errors) } - message = "* It seems there was a problem importing the Swagger file #{file_to_convert}\n" + if import_errors.to_s == "" + true + else + File.write(file_errors, import_errors) + message = "* It seems there was a problem importing the Swagger file #{swagger_file}\n" message += "* Take a look at the detected errors at #{file_errors}\n" warn message @logger.fatal message - return false - else - return true + false + end + rescue ParseError + raise + rescue StandardError => e + puts e.message + @logger.fatal e.message + @logger.fatal e.backtrace + puts e.backtrace + end + end + + private_class_method def self.format_and_check_file(file_path, logger) + escaped_path = Shellwords.shellescape(file_path) + res_rufo = `rufo #{escaped_path}` + logger.error " Error formatting with rufo" unless res_rufo.to_s.match?(/\AFormat:.+$\s*\z/) + syntax_result = `ruby -c #{escaped_path} 2>&1` + logger.error " Syntax Error: #{syntax_result}" unless syntax_result.include?("Syntax OK") + rescue Errno::ENOENT => e + logger.error " Could not run formatter/syntax checker: #{e.message}" + end + + private_class_method def self.build_example_value(dpv) + if dpv.key?(:example) + dpv[:example] + elsif dpv.key?(:examples) && dpv[:examples].is_a?(Array) && !dpv[:examples].empty? + dpv[:examples].first + elsif dpv.key?(:type) + effective_type = dpv[:type] + effective_type = Array(effective_type).reject { |t| t == "null" }.first if effective_type.is_a?(Array) + case effective_type.to_s.downcase + when "string" then dpv[:format] || "string" + when "integer" then 0 + when "number" + %w[float double decimal].include?(dpv[:format].to_s.downcase) ? 0.0 : 0 + when "boolean" then true + when "object" + if dpv.key?(:properties) + result = {} + dpv[:properties].each { |k, v| result[k] = build_example_value(v) } + result + else + {} + end + when "array" + if dpv.key?(:items) && dpv[:items].is_a?(Hash) + [build_example_value(dpv[:items])] + else + [] + end + else dpv[:format] || "" end - rescue StandardError => stack - puts stack.message - @logger.fatal stack.message - @logger.fatal stack.backtrace - puts stack.backtrace + else + "" end end end diff --git a/lib/open_api_import/pretty_hash_symbolized.rb b/lib/open_api_import/pretty_hash_symbolized.rb index c62c08b..4fc930f 100644 --- a/lib/open_api_import/pretty_hash_symbolized.rb +++ b/lib/open_api_import/pretty_hash_symbolized.rb @@ -1,10 +1,14 @@ +# frozen_string_literal: true + module LibOpenApiImport - #gen pretty hash symbolized - private def pretty_hash_symbolized(hash) + private + + # gen pretty hash symbolized + def pretty_hash_symbolized(hash) output = [] output << "{" hash.each do |kr, kv| - if kv.kind_of?(Hash) + if kv.is_a?(Hash) restv = pretty_hash_symbolized(kv) restv[0] = "#{kr}: {" output += restv @@ -13,6 +17,6 @@ module LibOpenApiImport end end output << "}," - return output + output end end diff --git a/lib/open_api_import/utils.rb b/lib/open_api_import/utils.rb index b3ed4a2..6d88816 100644 --- a/lib/open_api_import/utils.rb +++ b/lib/open_api_import/utils.rb @@ -1,22 +1,20 @@ -class String - ######################################################## - # Convert to snake_case a string - ######################################################## - def snake_case - gsub(/\W/, '_') - .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') - .gsub(/([a-z])([A-Z])/, '\1_\2') - .downcase - .gsub(/_+/, '_') - end +# frozen_string_literal: true + +module OpenApiImportStringExt + refine String do + def snake_case + gsub(/\W/, "_") + .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') + .gsub(/([a-z])([A-Z])/, '\1_\2') + .downcase + .gsub(/_+/, "_") + end - ######################################################## - # Convert to CamelCase a string - ######################################################## - def camel_case - return self if self !~ /_/ && self !~ /-/ && self !~ /\s/ && self =~ /^[A-Z]+.*/ + def camel_case + return self if self !~ /_/ && self !~ /-/ && self !~ /\s/ && self =~ /^[A-Z]+.*/ - gsub(/\W/, '_') - .split('_').map(&:capitalize).join + gsub(/\W/, "_") + .split("_").map(&:capitalize).join + end end -end \ No newline at end of file +end diff --git a/open_api_import.gemspec b/open_api_import.gemspec index b3fa730..1502e7e 100644 --- a/open_api_import.gemspec +++ b/open_api_import.gemspec @@ -1,22 +1,25 @@ -Gem::Specification.new do |s| - s.name = "open_api_import" - s.version = "0.11.6" - s.summary = "OpenApiImport -- Import a Swagger or Open API file and create a Ruby Request Hash file including all requests and responses with all the examples. The file can be in JSON or YAML" - s.description = "OpenApiImport -- Import a Swagger or Open API file and create a Ruby Request Hash file including all requests and responses with all the examples. The file can be in JSON or YAML" - s.authors = ["Mario Ruiz"] - s.email = "marioruizs@gmail.com" - s.files = ["lib/open_api_import.rb", "LICENSE", "README.md", ".yardopts"] + Dir["lib/open_api_import/*.rb"] - s.extra_rdoc_files = ["LICENSE", "README.md"] - s.homepage = "https://github.com/MarioRuiz/open_api_import" - s.license = "MIT" - s.add_runtime_dependency "oas_parser_reborn", "~> 0.25" - s.add_runtime_dependency "rufo", "~> 0.16.1" - s.add_runtime_dependency "nice_hash", "~> 1.18" - s.add_runtime_dependency "activesupport", "~> 6.1" #due this bug on activesupport https://github.com/Nexmo/oas_parser/issues/65 - s.add_development_dependency "rspec", "~> 3.8", ">= 3.8.0" - s.test_files = s.files.grep(%r{^(test|spec|features)/}) - s.require_paths = ["lib"] - s.executables << "open_api_import" - s.required_ruby_version = ">= 2.7" - s.post_install_message = "Thanks for installing! Visit us on https://github.com/MarioRuiz/open_api_import" -end +# frozen_string_literal: true + +Gem::Specification.new do |s| + s.name = "open_api_import" + s.version = "0.12.0" + s.summary = "OpenApiImport -- Import a Swagger or Open API file and create a Ruby Request Hash file including all requests and responses with all the examples. The file can be in JSON or YAML" + s.description = "OpenApiImport -- Import a Swagger or Open API file and create a Ruby Request Hash file including all requests and responses with all the examples. The file can be in JSON or YAML" + s.authors = ["Mario Ruiz"] + s.email = "marioruizs@gmail.com" + s.files = ["lib/open_api_import.rb", "LICENSE", "README.md", ".yardopts"] + Dir["lib/open_api_import/*.rb"] + s.extra_rdoc_files = ["LICENSE", "README.md"] + s.homepage = "https://github.com/MarioRuiz/open_api_import" + s.license = "MIT" + s.add_dependency "activesupport", ">= 6.1", "< 8.0" + s.add_dependency "nice_hash", "~> 1.19" + s.add_dependency "oas_parser_reborn", "~> 0.25" + s.add_dependency "rufo", "~> 0.16" + s.add_development_dependency "rspec", "~> 3.8", ">= 3.8.0" + s.add_development_dependency "rubocop", "~> 1.0" + s.require_paths = ["lib"] + s.executables << "open_api_import" + s.required_ruby_version = ">= 3.0" + s.post_install_message = "Thanks for installing! Visit us on https://github.com/MarioRuiz/open_api_import" + s.metadata["rubygems_mfa_required"] = "true" +end diff --git a/spec/fixtures/v2.0/yaml/formdata_api.yaml b/spec/fixtures/v2.0/yaml/formdata_api.yaml new file mode 100644 index 0000000..a2ed2db --- /dev/null +++ b/spec/fixtures/v2.0/yaml/formdata_api.yaml @@ -0,0 +1,53 @@ +swagger: "2.0" +info: + title: FormData API + version: "1.0.0" +basePath: /api +paths: + /upload: + post: + operationId: uploadFile + summary: Upload a file + consumes: + - multipart/form-data + parameters: + - name: filename + in: formData + type: string + description: Name of the file + - name: enabled + in: formData + type: boolean + description: Whether file is enabled + - name: size + in: formData + type: integer + description: File size + - name: ratio + in: formData + type: number + description: Compression ratio + responses: + '200': + description: File uploaded + /items: + get: + operationId: listItems + summary: List items + parameters: + - name: X-Request-Id + in: header + type: string + description: Request identifier + - name: page + in: query + type: integer + required: true + description: Page number + responses: + '200': + description: Items listed + schema: + type: array + items: + type: string diff --git a/spec/fixtures/v2.0/yaml/readonly_defaults.yaml b/spec/fixtures/v2.0/yaml/readonly_defaults.yaml new file mode 100644 index 0000000..31e6f88 --- /dev/null +++ b/spec/fixtures/v2.0/yaml/readonly_defaults.yaml @@ -0,0 +1,45 @@ +swagger: "2.0" +info: + title: ReadOnly and Defaults API + version: "1.0.0" +basePath: /api +paths: + /resources: + post: + operationId: createResource + summary: Create a resource + parameters: + - name: body + in: body + schema: + type: object + required: + - name + properties: + id: + type: integer + readOnly: true + name: + type: string + status: + type: string + default: "active" + count: + type: integer + default: 0 + enabled: + type: boolean + default: null + responses: + '201': + description: Resource created + schema: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string + status: + type: string diff --git a/spec/fixtures/v2.0/yaml/shared_params.yaml b/spec/fixtures/v2.0/yaml/shared_params.yaml new file mode 100644 index 0000000..daff48a --- /dev/null +++ b/spec/fixtures/v2.0/yaml/shared_params.yaml @@ -0,0 +1,30 @@ +swagger: "2.0" +info: + title: Shared Params API + version: "1.0.0" +basePath: /api +paths: + /items/{id}: + parameters: + - name: id + in: path + type: integer + required: true + description: Item ID + get: + operationId: getItem + summary: Get item by ID + parameters: + - name: fields + in: query + type: string + description: Fields to include + responses: + '200': + description: Item found + delete: + operationId: deleteItem + summary: Delete item by ID + responses: + '204': + description: Item deleted diff --git a/spec/fixtures/v3.0/oneof_api.yaml b/spec/fixtures/v3.0/oneof_api.yaml new file mode 100644 index 0000000..8b8f840 --- /dev/null +++ b/spec/fixtures/v3.0/oneof_api.yaml @@ -0,0 +1,53 @@ +openapi: "3.0.0" +info: + title: OneOf API + version: "1.0.0" +paths: + /pets: + post: + operationId: createPet + summary: Create a pet with oneOf body + requestBody: + content: + application/json: + schema: + oneOf: + - type: object + required: + - name + properties: + name: + type: string + bark: + type: boolean + - type: object + required: + - name + properties: + name: + type: string + purr: + type: boolean + responses: + '201': + description: Pet created + /animals: + post: + operationId: createAnimal + summary: Create animal with anyOf body + requestBody: + content: + application/json: + schema: + anyOf: + - type: object + properties: + species: + type: string + - type: object + properties: + breed: + type: string + responses: + '201': + description: Animal created diff --git a/spec/fixtures/v3.0/petstore_3_1.yaml b/spec/fixtures/v3.0/petstore_3_1.yaml new file mode 100644 index 0000000..1d61b5e --- /dev/null +++ b/spec/fixtures/v3.0/petstore_3_1.yaml @@ -0,0 +1,57 @@ +openapi: "3.1.0" +info: + version: 1.0.0 + title: Swagger Petstore 3.1 + license: + name: MIT +paths: + /pets: + get: + summary: List all pets + operationId: listPets + parameters: + - name: limit + in: query + required: false + schema: + type: integer + responses: + '200': + description: A list of pets + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + name: + type: string + tag: + type: + - string + - "null" + post: + summary: Create a pet + operationId: createPet + requestBody: + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + type: string + examples: + - Fido + tag: + type: + - string + - "null" + responses: + '201': + description: Pet created diff --git a/spec/open_api_import/features_spec.rb b/spec/open_api_import/features_spec.rb new file mode 100644 index 0000000..4bb0bc5 --- /dev/null +++ b/spec/open_api_import/features_spec.rb @@ -0,0 +1,276 @@ +require "open_api_import" + +RSpec.describe OpenApiImport do + describe ".from" do + describe "mock_response option" do + it "includes mock_response when mock_response is true" do + file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" + FileUtils.rm_f("#{file_name}.rb") + OpenApiImport.from file_name, create_method_name: :operation_id, mock_response: true + expect(File.exist?("#{file_name}.rb")).to eq true + content = File.read("#{file_name}.rb") + expect(content).to include("mock_response:") + end + + it "includes response code and message in mock_response" do + file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" + OpenApiImport.from file_name, create_method_name: :operation_id, mock_response: true + content = File.read("#{file_name}.rb") + expect(content).to include("mock_response:") + expect(content).to include("code:") + expect(content).to include("message:") + end + end + + describe "silent option" do + it "suppresses output when silent is true" do + file_name = "./spec/fixtures/v2.0/yaml/petstore-minimal.yaml" + expect do + OpenApiImport.from file_name, silent: true + end.not_to output(/Requests file/).to_stdout + end + + it "displays output when silent is false" do + file_name = "./spec/fixtures/v2.0/yaml/petstore-minimal.yaml" + expect do + OpenApiImport.from file_name, silent: false + end.to output(/Requests file/).to_stdout + end + + it "still creates log file when silent" do + file_name = "./spec/fixtures/v2.0/yaml/petstore-minimal.yaml" + OpenApiImport.from file_name, silent: true + expect(File.exist?("#{file_name}_open_api_import.log")).to eq true + end + end + + describe "error handling" do + it "raises ParseError for unparseable files" do + file_name = "./spec/fixtures/wrong/invalid_syntax.yaml" + File.write(file_name, "{{invalid yaml content!!") + expect do + OpenApiImport.from file_name + end.to raise_error(OpenApiImport::ParseError) + ensure + FileUtils.rm_f(file_name) + FileUtils.rm_f("#{file_name}_open_api_import.log") + end + + it "handles missing files by returning nil" do + result = OpenApiImport.from "./nonexistent_file.yaml" + expect(result).to be_nil + end + + it "logs unsupported swagger version" do + file_name = "./spec/fixtures/wrong/petstore-minimal.yaml" + OpenApiImport.from file_name + log = File.read("#{file_name}_open_api_import.log") + expect(log).to include("Unsupported Swagger version") + end + end + + describe "return_data option" do + it "returns hash of generated content without writing files" do + file_name = "./spec/fixtures/v2.0/yaml/petstore-minimal.yaml" + temp_rb = "#{file_name}.rb" + FileUtils.rm_f(temp_rb) + + result = OpenApiImport.from file_name, return_data: true, silent: true + expect(result).to be_a(Hash) + expect(result.keys.first).to end_with(".rb") + expect(result.values.first).to include("module Swagger") + end + + it "returns multiple files for path_file mode" do + file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" + result = OpenApiImport.from file_name, name_for_module: :path_file, return_data: true, silent: true + expect(result).to be_a(Hash) + expect(result.size).to be > 1 + end + end + + describe "formData parameters" do + it "generates data_examples with formData values" do + file_name = "./spec/fixtures/v2.0/yaml/formdata_api.yaml" + FileUtils.rm_f("#{file_name}.rb") + OpenApiImport.from file_name, create_method_name: :operation_id, silent: true + expect(File.exist?("#{file_name}.rb")).to eq true + content = File.read("#{file_name}.rb") + expect(content).to include("data_examples:") + expect(content).to include("filename:") + expect(content).to include("enabled:") + expect(content).to include("size:") + expect(content).to include("ratio:") + end + end + + describe "header parameters" do + it "does not crash on header parameters" do + file_name = "./spec/fixtures/v2.0/yaml/formdata_api.yaml" + result = OpenApiImport.from file_name, create_method_name: :operation_id, silent: true + expect(result).to eq true + end + end + + describe "shared path-level parameters" do + it "distributes shared parameters to all methods on the path" do + file_name = "./spec/fixtures/v2.0/yaml/shared_params.yaml" + FileUtils.rm_f("#{file_name}.rb") + OpenApiImport.from file_name, create_method_name: :operation_id, silent: true + content = File.read("#{file_name}.rb") + expect(content).to include("def self.get_item") + expect(content).to include("def self.delete_item") + expect(content).to match(/get_item.*id/m) + expect(content).to match(/delete_item.*id/m) + end + end + + describe "readOnly and default values" do + it "generates data_read_only key" do + file_name = "./spec/fixtures/v2.0/yaml/readonly_defaults.yaml" + FileUtils.rm_f("#{file_name}.rb") + OpenApiImport.from file_name, create_method_name: :operation_id, silent: true + content = File.read("#{file_name}.rb") + expect(content).to include("data_read_only:") + end + + it "generates data_default key" do + file_name = "./spec/fixtures/v2.0/yaml/readonly_defaults.yaml" + OpenApiImport.from file_name, create_method_name: :operation_id, silent: true + content = File.read("#{file_name}.rb") + expect(content).to include("data_default:") + expect(content).to include("active") + end + end + + describe "oneOf body schemas" do + it "generates data_examples from oneOf schemas" do + file_name = "./spec/fixtures/v3.0/oneof_api.yaml" + FileUtils.rm_f("#{file_name}.rb") + OpenApiImport.from file_name, create_method_name: :operation_id, silent: true + expect(File.exist?("#{file_name}.rb")).to eq true + content = File.read("#{file_name}.rb") + expect(content).to include("def self.create_pet") + expect(content).to include("data_examples:") + end + end + + describe "anyOf body schemas" do + it "generates data_examples from anyOf schemas" do + file_name = "./spec/fixtures/v3.0/oneof_api.yaml" + OpenApiImport.from file_name, create_method_name: :operation_id, silent: true + content = File.read("#{file_name}.rb") + expect(content).to include("def self.create_animal") + expect(content).to include("data_examples:") + end + end + + describe "OpenAPI 3.0 support" do + it "handles v3.0 specs with requestBody" do + file_name = "./spec/fixtures/v3.0/petstore.yaml" + FileUtils.rm_f("#{file_name}.rb") + result = OpenApiImport.from file_name, silent: true + expect(result).to eq true + expect(File.exist?("#{file_name}.rb")).to eq true + end + + it "handles v3.0 api-with-examples (content -> examples)" do + file_name = "./spec/fixtures/v3.0/api-with-examples.yaml" + FileUtils.rm_f("#{file_name}.rb") + result = OpenApiImport.from file_name, create_method_name: :operation_id, silent: true + expect(result).to eq true + content = File.read("#{file_name}.rb") + expect(content).to include("responses:") + end + + it "handles v3.0 expanded petstore with allOf in requestBody" do + file_name = "./spec/fixtures/v3.0/petstore-expanded.yaml" + FileUtils.rm_f("#{file_name}.rb") + result = OpenApiImport.from file_name, create_method_name: :operation_id, silent: true + expect(result).to eq true + content = File.read("#{file_name}.rb") + expect(content).to include("data_examples:") + end + + it "handles v2.0 api-with-examples (examples -> application/json)" do + file_name = "./spec/fixtures/v2.0/yaml/api-with-examples.yaml" + FileUtils.rm_f("#{file_name}.rb") + result = OpenApiImport.from file_name, create_method_name: :operation_id, silent: true + expect(result).to eq true + content = File.read("#{file_name}.rb") + expect(content).to include("responses:") + end + end + + describe "OpenAPI 3.1 support" do + it "handles v3.1 specs with nullable type arrays" do + file_name = "./spec/fixtures/v3.0/petstore_3_1.yaml" + FileUtils.rm_f("#{file_name}.rb") + result = OpenApiImport.from file_name, silent: true + expect(result).to eq true + expect(File.exist?("#{file_name}.rb")).to eq true + content = File.read("#{file_name}.rb") + expect(content).to include("def self.list_pets") + expect(content).to include("def self.create_pet") + end + end + + describe "response with array of items (type only)" do + it "generates response with array type items" do + file_name = "./spec/fixtures/v2.0/yaml/formdata_api.yaml" + OpenApiImport.from file_name, create_method_name: :operation_id, silent: true + content = File.read("#{file_name}.rb") + expect(content).to include("responses:") + end + end + + describe "version constant" do + it "has a VERSION constant matching semver" do + expect(OpenApiImport::VERSION).to match(/\d+\.\d+\.\d+/) + end + end + + describe "ParseError class" do + it "is a subclass of StandardError" do + expect(OpenApiImport::ParseError.ancestors).to include(StandardError) + end + + it "can be instantiated with a message" do + error = OpenApiImport::ParseError.new("test error") + expect(error.message).to eq("test error") + end + end + + describe "JSON format support" do + it "handles JSON swagger files" do + file_name = "./spec/fixtures/v2.0/json/petstore-simple.json" + FileUtils.rm_f("#{file_name}.rb") + result = OpenApiImport.from file_name, create_method_name: :operation_id, silent: true + expect(result).to eq true + content = File.read("#{file_name}.rb") + expect(content).to include("def self.find_pets") + end + end + + describe "uber API (complex multi-tag)" do + it "creates all tag-based modules with :tags" do + file_name = "./spec/fixtures/v2.0/yaml/uber.yaml" + OpenApiImport.from file_name, name_for_module: :tags, silent: true + content = File.read("#{file_name}.rb") + expect(content).to include("module Products") + expect(content).to include("module Estimates") + expect(content).to include("module User") + end + end + + describe "create_constants with query params" do + it "generates constants with operationId method naming" do + file_name = "./spec/fixtures/v2.0/yaml/uber.yaml" + FileUtils.rm_f("#{file_name}.rb") + OpenApiImport.from file_name, create_method_name: :operationId, create_constants: true, silent: true + content = File.read("#{file_name}.rb") + expect(content).to include("LATITUDE") + end + end + end +end diff --git a/spec/open_api_import/helpers_spec.rb b/spec/open_api_import/helpers_spec.rb new file mode 100644 index 0000000..9f317dc --- /dev/null +++ b/spec/open_api_import/helpers_spec.rb @@ -0,0 +1,891 @@ +require "open_api_import" + +RSpec.describe LibOpenApiImport do + let(:helper) do + obj = Object.new + obj.extend(LibOpenApiImport) + obj + end + + describe "#get_examples" do + it "handles string type properties" do + properties = { name: { type: "string" } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("name:") + expect(result.join).to include('"string"') + end + + it "handles integer type properties" do + properties = { age: { type: "integer" } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("age: 0") + end + + it "handles number type with float format" do + properties = { price: { type: "number", format: "float" } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("price: 0.0") + end + + it "handles number type with double format" do + properties = { amount: { type: "number", format: "double" } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("amount: 0.0") + end + + it "handles number type with decimal format" do + properties = { rate: { type: "number", format: "decimal" } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("rate: 0.0") + end + + it "handles number type without float/double format" do + properties = { count: { type: "number" } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("count: 0") + end + + it "handles boolean type properties" do + properties = { active: { type: "boolean" } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("active: true") + end + + it "uses example values when provided" do + properties = { name: { type: "string", example: "John" } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("John") + end + + it "uses numeric example values directly" do + properties = { age: { type: "integer", example: 42 } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("42") + end + + it "uses examples (plural, OAS 3.1) when example is absent" do + properties = { name: { type: "string", examples: %w[Fido Rex] } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("Fido") + end + + it "prefers example over examples (plural)" do + properties = { name: { type: "string", example: "Buddy", examples: ["Fido"] } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("Buddy") + expect(result.join).not_to include("Fido") + end + + it "handles string example containing single quotes" do + properties = { desc: { type: "string", example: "it's a test" } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("it's a test") + end + + it "replaces double quotes with single quotes in string examples" do + properties = { desc: { type: "string", example: 'say "hello"' } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("say 'hello'") + end + + it "handles Time example values" do + time_val = Time.new(2024, 1, 15) + properties = { created: { type: "string", example: time_val } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("2024") + end + + it "handles array example that is Array but type is string (takes first)" do + properties = { tag: { type: "string", example: %w[alpha beta] } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("alpha") + end + + it "skips readOnly properties when remove_readonly is true" do + properties = { + id: { type: "integer", readOnly: true }, + name: { type: "string" } + } + result = helper.send(:get_examples, properties, :key_value, true) + expect(result.join).not_to include("id:") + expect(result.join).to include("name:") + end + + it "includes readOnly properties when remove_readonly is false" do + properties = { + id: { type: "integer", readOnly: true }, + name: { type: "string" } + } + result = helper.send(:get_examples, properties, :key_value, false) + expect(result.join).to include("id:") + expect(result.join).to include("name:") + end + + it "returns only values when type is :only_value" do + properties = { name: { type: "string" } } + result = helper.send(:get_examples, properties, :only_value) + expect(result.join).not_to include("{") + expect(result.join).not_to include("}") + end + + it "wraps in braces for :key_value type" do + properties = { name: { type: "string" } } + result = helper.send(:get_examples, properties, :key_value) + expect(result.first).to eq("{") + expect(result.last).to eq("}") + end + + it "handles array type with enum items" do + properties = { + status: { + type: "array", + items: { type: "string", enum: %w[active inactive] } + } + } + result = helper.send(:get_examples, properties) + expect(result.join).to include("active") + end + + it "handles array with single-type items (no enum)" do + properties = { + tags: { + type: "array", + items: { type: "string" } + } + } + result = helper.send(:get_examples, properties) + expect(result.join).to include("tags:") + expect(result.join).to include("string") + end + + it "handles array enum items with :only_value type" do + properties = { + status: { + type: "array", + items: { type: "string", enum: %w[active inactive] } + } + } + result = helper.send(:get_examples, properties, :only_value) + expect(result.join).to include("active") + expect(result.join).not_to include("status:") + end + + it "infers object type from :properties key when no explicit type" do + properties = { + address: { + properties: { city: { type: "string" } } + } + } + result = helper.send(:get_examples, properties) + expect(result.join).to include("address:") + end + + it "infers array type from :items key when no explicit type" do + properties = { + tags: { + items: { type: "string", enum: %w[a b] } + } + } + result = helper.send(:get_examples, properties) + expect(result.join).to include("tags:") + end + + it "handles nullable type arrays (OAS 3.1)" do + properties = { name: { type: %w[string null] } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("name:") + expect(result.join).to include('"string"') + end + + it "returns empty array for empty properties" do + result = helper.send(:get_examples, {}) + expect(result).to eq([]) + end + + it "handles object type with no properties" do + properties = { meta: { type: "object" } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("meta:") + expect(result.join).to include("{ }") + end + + it "handles unknown type by using format as value" do + properties = { field: { type: "custom", format: "special" } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("special") + end + + it "handles string type with format (uses format as placeholder)" do + properties = { email: { type: "string", format: "email" } } + result = helper.send(:get_examples, properties) + expect(result.join).to include("email") + end + end + + describe "#get_response_examples" do + it "handles v2.0 string examples (application/json)" do + v = { + examples: { + "application/json": '{"name": "test"}' + } + } + result = helper.send(:get_response_examples, v) + expect(result).not_to be_empty + expect(result.first).to include("test") + end + + it "handles v2.0 hash examples (application/json)" do + v = { + examples: { + "application/json": { name: "test", id: 1 } + } + } + result = helper.send(:get_response_examples, v) + expect(result).not_to be_empty + expect(result.join).to include("name") + end + + it "handles v2.0 array examples (application/json)" do + v = { + examples: { + "application/json": [{ name: "a" }, { name: "b" }] + } + } + result = helper.send(:get_response_examples, v) + expect(result).not_to be_empty + expect(result.first).to eq("[") + expect(result.last).to eq("]") + end + + it "handles v3.0 content -> application/json -> schema with properties" do + v = { + content: { + "application/json": { + schema: { + properties: { + name: { type: "string" }, + age: { type: "integer" } + } + } + } + } + } + result = helper.send(:get_response_examples, v) + expect(result).not_to be_empty + expect(result.join).to include("name:") + expect(result.join).to include("age:") + end + + it "handles v3.0 content -> examples (hash value)" do + v = { + content: { + "application/json": { + examples: { + example1: { + value: { name: "test", id: 1 } + } + } + } + } + } + result = helper.send(:get_response_examples, v) + expect(result).not_to be_empty + expect(result.join).to include("name") + end + + it "handles v3.0 content -> examples (string value)" do + v = { + content: { + "application/json": { + examples: { + example1: { + value: "simple string response" + } + } + } + } + } + result = helper.send(:get_response_examples, v) + expect(result).to eq(["simple string response"]) + end + + it "handles v3.0 content -> examples (array value)" do + v = { + content: { + "application/json": { + examples: { + example1: { + value: [{ id: 1 }, { id: 2 }] + } + } + } + } + } + result = helper.send(:get_response_examples, v) + expect(result.first).to eq("[") + expect(result.last).to eq("]") + end + + it "handles v3.0 content -> examples without :value key (returns empty string)" do + v = { + content: { + "application/json": { + examples: { + example1: { summary: "just a summary" } + } + } + } + } + result = helper.send(:get_response_examples, v) + expect(result).to eq([""]) + end + + it "handles schema with allOf" do + v = { + schema: { + allOf: [ + { properties: { name: { type: "string" } } }, + { properties: { id: { type: "integer" } } } + ] + } + } + result = helper.send(:get_response_examples, v) + expect(result).not_to be_empty + expect(result.join).to include("name:") + expect(result.join).to include("id:") + end + + it "handles schema with items.properties (array response)" do + v = { + schema: { + type: "array", + items: { + properties: { + name: { type: "string" } + } + } + } + } + result = helper.send(:get_response_examples, v) + expect(result).not_to be_empty + expect(result.first).to eq("[") + expect(result.last).to eq("]") + expect(result.join).to include("name:") + end + + it "handles schema with items.allOf (array of allOf)" do + v = { + schema: { + type: "array", + items: { + allOf: [ + { properties: { name: { type: "string" } } }, + { properties: { age: { type: "integer" } } } + ] + } + } + } + result = helper.send(:get_response_examples, v) + expect(result).not_to be_empty + expect(result.first).to eq("[") + expect(result.join).to include("name:") + expect(result.join).to include("age:") + end + + it "handles schema with items having only type (no properties)" do + v = { + schema: { + type: "array", + items: { type: "string" } + } + } + result = helper.send(:get_response_examples, v) + expect(result).to eq(["[\"string\"]"]) + end + + it "handles @type key replacement" do + v = { + schema: { + properties: { + "@type": { type: "string", example: "Person" } + } + } + } + result = helper.send(:get_response_examples, v) + joined = result.join + expect(joined).to include("@type") + end + + it "returns empty array when no schema or examples" do + v = { description: "No content" } + result = helper.send(:get_response_examples, v) + expect(result).to be_empty + end + + it "does not mutate the input hash" do + v = { + schema: { properties: { name: { type: "string" } } } + } + original = v.dup + helper.send(:get_response_examples, v) + expect(v.keys).to eq(original.keys) + end + + it "handles remove_readonly=true by excluding readOnly fields" do + v = { + schema: { + properties: { + id: { type: "integer", readOnly: true }, + name: { type: "string" } + } + } + } + result = helper.send(:get_response_examples, v, true) + expect(result.join).not_to include("id:") + expect(result.join).to include("name:") + end + end + + describe "#get_patterns" do + it "extracts regex patterns" do + result = helper.send(:get_patterns, "email", { pattern: "^[a-z]+@[a-z]+$" }) + expect(result).not_to be_empty + expect(result.first).to include("email") + expect(result.first).to include("/") + end + + it "extracts minLength/maxLength patterns" do + result = helper.send(:get_patterns, "name", { minLength: 1, maxLength: 50 }) + expect(result.first).to include("1-50") + expect(result.first).to include("LN$") + end + + it "extracts minLength only" do + result = helper.send(:get_patterns, "name", { minLength: 3 }) + expect(result.first).to include("3") + expect(result.first).to include("LN$") + end + + it "extracts maxLength only" do + result = helper.send(:get_patterns, "name", { maxLength: 100 }) + expect(result.first).to include("0-100") + expect(result.first).to include("LN$") + end + + it "extracts minimum/maximum range patterns for integers" do + result = helper.send(:get_patterns, "age", { type: "integer", minimum: 0, maximum: 150 }) + expect(result.first).to include("0..150") + end + + it "extracts minimum/maximum for string type (produces LN pattern)" do + result = helper.send(:get_patterns, "code", { type: "string", minimum: 1, maximum: 10 }) + expect(result.first).to include("1-10") + expect(result.first).to include("LN$") + end + + it "extracts minimum only (infinite range)" do + result = helper.send(:get_patterns, "age", { type: "integer", minimum: 18 }) + expect(result.first).to include("18..") + end + + it "extracts maximum only" do + result = helper.send(:get_patterns, "count", { type: "integer", maximum: 100 }) + expect(result.first).to include("0..100") + end + + it "extracts enum patterns" do + result = helper.send(:get_patterns, "status", { enum: %w[active inactive] }) + expect(result.first).to include("active|inactive") + end + + it "extracts boolean patterns" do + result = helper.send(:get_patterns, "flag", { type: "boolean" }) + expect(result.first).to include("Boolean") + end + + it "extracts datetime patterns" do + result = helper.send(:get_patterns, "created", { format: "date-time" }) + expect(result.first).to include("DateTime") + end + + it "handles \\x to \\u conversion in patterns" do + result = helper.send(:get_patterns, "name", { pattern: "^[\\x41-\\x5A]+$" }) + expect(result).not_to be_empty + end + + it "handles \\x to \\u00 conversion for hex ranges" do + result = helper.send(:get_patterns, "name", { pattern: "^[\\xDF-\\xFF]+$" }) + expect(result).not_to be_empty + expect(result.first).to include("\\u00") + end + + it "handles pattern with escaped forward slashes" do + result = helper.send(:get_patterns, "path", { pattern: "^[^\\.\\\\/:*?\"<>|]+$" }) + expect(result).not_to be_empty + end + + it "handles array type with items.enum" do + dpv = { type: "array", items: { type: "string", enum: %w[a b c] } } + result = helper.send(:get_patterns, "tags", dpv) + expect(result.first).to include("a|b|c") + end + + it "handles array type with items.properties (nested patterns)" do + dpv = { + type: "array", + items: { + properties: { + status: { enum: %w[on off] } + } + } + } + result = helper.send(:get_patterns, "items", dpv) + expect(result).not_to be_empty + expect(result.first).to include("items.status") + end + + it "handles array with items.type only (no enum, no properties)" do + dpv = { type: "array", items: { type: "boolean" } } + result = helper.send(:get_patterns, "flags", dpv) + expect(result).not_to be_empty + end + + it "handles object type with nested properties" do + dpv = { + type: "object", + properties: { + city: { minLength: 1, maxLength: 100 } + } + } + result = helper.send(:get_patterns, "address", dpv) + expect(result).not_to be_empty + expect(result.first).to include("address.city") + end + + it "handles object type with empty root key" do + dpv = { + type: "object", + properties: { + city: { enum: %w[NYC LA] } + } + } + result = helper.send(:get_patterns, "", dpv) + expect(result).not_to be_empty + expect(result.first).to include("'city'") + end + + it "handles nullable type arrays (OAS 3.1)" do + result = helper.send(:get_patterns, "flag", { type: %w[boolean null] }) + expect(result.first).to include("Boolean") + end + + it "returns empty array when no patterns match" do + result = helper.send(:get_patterns, "name", { type: "string" }) + expect(result).to be_empty + end + + it "deduplicates patterns" do + dpv = { type: "boolean" } + result = helper.send(:get_patterns, "flag", dpv) + expect(result.size).to eq(result.uniq.size) + end + end + + describe "#get_required_data" do + it "extracts required field names" do + body = { required: %w[name email], properties: {} } + result = helper.send(:get_required_data, body) + expect(result).to include(:name) + expect(result).to include(:email) + end + + it "extracts required fields from allOf" do + body = { + allOf: [ + { required: ["id"], properties: {} }, + { required: ["name"], properties: {} } + ], + properties: {} + } + result = helper.send(:get_required_data, body) + expect(result).to include(:id) + expect(result).to include(:name) + end + + it "returns empty array when no required fields" do + body = { properties: { name: { type: "string" } } } + result = helper.send(:get_required_data, body) + expect(result).to be_empty + end + + it "handles nested required fields" do + body = { + required: ["address"], + properties: { + address: { + type: "object", + required: ["city"], + properties: { + city: { type: "string" } + } + } + } + } + result = helper.send(:get_required_data, body) + expect(result).to include(:address) + expect(result).to include(:"address.city") + end + + it "handles empty required array" do + body = { required: [], properties: {} } + result = helper.send(:get_required_data, body) + expect(result).to be_empty + end + + it "handles allOf items without required key" do + body = { + allOf: [ + { properties: { name: { type: "string" } } } + ], + properties: {} + } + result = helper.send(:get_required_data, body) + expect(result).to be_empty + end + + it "handles body without properties key" do + body = { required: ["name"] } + result = helper.send(:get_required_data, body) + expect(result).to eq([:name]) + end + end + + describe "#filter" do + it "filters hash by specified keys" do + hash = { name: "John", age: 30, email: "john@example.com" } + result = helper.send(:filter, hash, [:name, :email]) + expect(result).to eq({ name: "John", email: "john@example.com" }) + end + + it "returns empty hash for missing keys" do + hash = { name: "John" } + result = helper.send(:filter, hash, [:missing]) + expect(result).to be_empty + end + + it "returns empty hash values for hash-type values" do + hash = { address: { city: "NYC" } } + result = helper.send(:filter, hash, [:address]) + expect(result).to eq({ address: {} }) + end + + it "accepts a single symbol key (auto-wraps in array)" do + hash = { name: "John", age: 30 } + result = helper.send(:filter, hash, :name) + expect(result).to eq({ name: "John" }) + end + + it "delegates to nice_filter when nested=true" do + hash = { name: "John", age: 30 } + result = helper.send(:filter, hash, [:name], true) + expect(result).to eq({ name: "John" }) + end + + it "handles dot-notation symbol keys for nested access" do + hash = { address: { city: "NYC", zip: "10001" } } + result = helper.send(:filter, hash, [:"address.city"]) + expect(result).to have_key(:address) + expect(result[:address]).to have_key(:city) + end + end + + describe "#pretty_hash_symbolized" do + it "formats a flat hash" do + hash = { name: "John", age: 30 } + result = helper.send(:pretty_hash_symbolized, hash) + expect(result.join("\n")).to include("name:") + expect(result.join("\n")).to include("age:") + end + + it "handles nested hashes" do + hash = { address: { city: "NYC" } } + result = helper.send(:pretty_hash_symbolized, hash) + joined = result.join("\n") + expect(joined).to include("address:") + expect(joined).to include("city:") + end + + it "handles empty hash" do + result = helper.send(:pretty_hash_symbolized, {}) + expect(result).to eq(["{", "},"]) + end + + it "handles deeply nested hashes" do + hash = { a: { b: { c: "deep" } } } + result = helper.send(:pretty_hash_symbolized, hash) + joined = result.join("\n") + expect(joined).to include("a:") + expect(joined).to include("b:") + expect(joined).to include("c:") + end + + it "handles nil values" do + hash = { key: nil } + result = helper.send(:pretty_hash_symbolized, hash) + expect(result.join("\n")).to include("nil") + end + + it "handles array values" do + hash = { items: [1, 2, 3] } + result = helper.send(:pretty_hash_symbolized, hash) + expect(result.join("\n")).to include("items:") + end + + it "handles symbol values" do + hash = { status: :active } + result = helper.send(:pretty_hash_symbolized, hash) + expect(result.join("\n")).to include(":active") + end + end + + describe "#get_data_all_of_bodies" do + it "flattens allOf schemas" do + param = { + schema: { + allOf: [ + { properties: { name: { type: "string" } } }, + { properties: { age: { type: "integer" } } } + ] + } + } + _, bodies = helper.send(:get_data_all_of_bodies, param) + expect(bodies.size).to eq 2 + end + + it "handles non-allOf schemas" do + param = { + schema: { + properties: { name: { type: "string" } } + } + } + _data_examples_all_of, bodies = helper.send(:get_data_all_of_bodies, param) + expect(bodies.size).to eq 1 + end + + it "handles array input (recursive case)" do + arr = [ + { properties: { name: { type: "string" } } }, + { properties: { age: { type: "integer" } } } + ] + _data_examples_all_of, bodies = helper.send(:get_data_all_of_bodies, arr) + expect(bodies.size).to eq 2 + end + + it "handles nested allOf within allOf" do + param = { + schema: { + allOf: [ + { + allOf: [ + { properties: { inner1: { type: "string" } } }, + { properties: { inner2: { type: "integer" } } } + ] + }, + { properties: { outer: { type: "boolean" } } } + ] + } + } + data_examples_all_of, bodies = helper.send(:get_data_all_of_bodies, param) + expect(data_examples_all_of).to eq true + expect(bodies.size).to eq 3 + end + + it "handles mixed items with and without allOf" do + param = { + schema: { + allOf: [ + { properties: { simple: { type: "string" } } }, + { + allOf: [ + { properties: { nested: { type: "integer" } } } + ] + } + ] + } + } + data_examples_all_of, bodies = helper.send(:get_data_all_of_bodies, param) + expect(data_examples_all_of).to eq true + expect(bodies.size).to eq 2 + end + end + + describe "#build_example_value (via OpenApiImport)" do + it "returns example value directly" do + result = OpenApiImport.send(:build_example_value, { example: "hello" }) + expect(result).to eq("hello") + end + + it "returns first from examples (plural)" do + result = OpenApiImport.send(:build_example_value, { examples: %w[a b] }) + expect(result).to eq("a") + end + + it "returns 'string' for string type" do + result = OpenApiImport.send(:build_example_value, { type: "string" }) + expect(result).to eq("string") + end + + it "returns format for string type with format" do + result = OpenApiImport.send(:build_example_value, { type: "string", format: "email" }) + expect(result).to eq("email") + end + + it "returns 0 for integer type" do + result = OpenApiImport.send(:build_example_value, { type: "integer" }) + expect(result).to eq(0) + end + + it "returns 0.0 for number type with float format" do + result = OpenApiImport.send(:build_example_value, { type: "number", format: "float" }) + expect(result).to eq(0.0) + end + + it "returns 0 for number type without float format" do + result = OpenApiImport.send(:build_example_value, { type: "number" }) + expect(result).to eq(0) + end + + it "returns true for boolean type" do + result = OpenApiImport.send(:build_example_value, { type: "boolean" }) + expect(result).to eq(true) + end + + it "returns empty hash for object type without properties" do + result = OpenApiImport.send(:build_example_value, { type: "object" }) + expect(result).to eq({}) + end + + it "returns populated hash for object type with properties" do + result = OpenApiImport.send(:build_example_value, { + type: "object", + properties: { name: { type: "string" }, age: { type: "integer" } } + }) + expect(result).to eq({ name: "string", age: 0 }) + end + + it "returns empty array for array type" do + result = OpenApiImport.send(:build_example_value, { type: "array" }) + expect(result).to eq([]) + end + + it "handles nullable type arrays (OAS 3.1)" do + result = OpenApiImport.send(:build_example_value, { type: %w[string null] }) + expect(result).to eq("string") + end + + it "returns empty string when no type" do + result = OpenApiImport.send(:build_example_value, {}) + expect(result).to eq("") + end + end +end diff --git a/spec/open_api_import/open_api_import_data_examples_spec.rb b/spec/open_api_import/open_api_import_data_examples_spec.rb index 78ad379..340aa78 100644 --- a/spec/open_api_import/open_api_import_data_examples_spec.rb +++ b/spec/open_api_import/open_api_import_data_examples_spec.rb @@ -1,21 +1,19 @@ -require 'open_api_import' +require "open_api_import" -#todo: add test for data_examples to include values according to type in case no examples supplied, fex: doo: 0 instead of doo: "" if type is intenger +# TODO: add test for data_examples to include values according to type in case no examples supplied, fex: doo: 0 instead of doo: "" if type is intenger RSpec.describe OpenApiImport do - - describe '#from' do - - it 'adds the data examples specified' do - file_name = './spec/fixtures/v2.0/yaml/petstore-simple.yaml' - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") - OpenApiImport.from file_name, create_method_name: :operation_id - expect(File.exist?("#{file_name}.rb")).to eq true - content = File.read("#{file_name}.rb") - eval(content) - req = Swagger::SwaggerPetstore::V1_0_0::Root.add_pet() - expect(req.key?(:data_examples)).to eq true - expect(req[:data_examples].class).to eq Array - expect(req[:data_examples]).to eq ([{name: "string", tag: "string"}]) - end + describe "#from" do + it "adds the data examples specified" do + file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" + FileUtils.rm_f("#{file_name}.rb") + OpenApiImport.from file_name, create_method_name: :operation_id + expect(File.exist?("#{file_name}.rb")).to eq true + content = File.read("#{file_name}.rb") + eval(content) + req = Swagger::SwaggerPetstore::V1_0_0::Root.add_pet + expect(req.key?(:data_examples)).to eq true + expect(req[:data_examples].class).to eq Array + expect(req[:data_examples]).to eq([{ name: "string", tag: "string" }]) end + end end diff --git a/spec/open_api_import/open_api_import_data_required_spec.rb b/spec/open_api_import/open_api_import_data_required_spec.rb index 326f75d..4eb8926 100644 --- a/spec/open_api_import/open_api_import_data_required_spec.rb +++ b/spec/open_api_import/open_api_import_data_required_spec.rb @@ -1,42 +1,38 @@ -require 'open_api_import' +require "open_api_import" RSpec.describe OpenApiImport do + describe "#from" do + it "adds the required parameters on data body to data_required key" do + file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" + FileUtils.rm_f("#{file_name}.rb") + OpenApiImport.from file_name, create_method_name: :operation_id + expect(File.exist?("#{file_name}.rb")).to eq true + content = File.read("#{file_name}.rb") + eval(content) + req = Swagger::SwaggerPetstore::V1_0_0::Root.add_pet + expect(req.key?(:data_required)).to eq true + expect(req[:data_required].class).to eq Array + expect(req[:data_required]).to eq([:name]) + end - describe '#from' do - - it 'adds the required parameters on data body to data_required key' do - file_name = './spec/fixtures/v2.0/yaml/petstore-simple.yaml' - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") - OpenApiImport.from file_name, create_method_name: :operation_id - expect(File.exist?("#{file_name}.rb")).to eq true - content = File.read("#{file_name}.rb") - eval(content) - req = Swagger::SwaggerPetstore::V1_0_0::Root.add_pet() - expect(req.key?(:data_required)).to eq true - expect(req[:data_required].class).to eq Array - expect(req[:data_required]).to eq ([:name]) - end - - it 'adds query parameters to path and as non required params on the method when no required' do - file_name = './spec/fixtures/v2.0/yaml/petstore-simple.yaml' - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") - OpenApiImport.from file_name, create_method_name: :operation_id - expect(File.exist?("#{file_name}.rb")).to eq true - content = File.read("#{file_name}.rb") - expect(content).to include 'def self.find_pets(tags: "", limit: "")' - expect(content).to include 'path: "/api/pets?tags=#{tags}&limit=#{limit}&"' - end - - it 'adds query parameters to path and as required params on the method when required' do - file_name = './spec/fixtures/v2.0/yaml/petstore-simple.yaml' - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") - OpenApiImport.from file_name, create_method_name: :operation_id - expect(File.exist?("#{file_name}.rb")).to eq true - content = File.read("#{file_name}.rb") - expect(content).to include 'def self.find_pet_by_id(id)' - expect(content).to include 'path: "/api/pets/#{id}",' - end - + it "adds query parameters to path and as non required params on the method when no required" do + file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" + FileUtils.rm_f("#{file_name}.rb") + OpenApiImport.from file_name, create_method_name: :operation_id + expect(File.exist?("#{file_name}.rb")).to eq true + content = File.read("#{file_name}.rb") + expect(content).to include 'def self.find_pets(tags: "", limit: "")' + expect(content).to include "path: \"/api/pets?tags=\#{tags}&limit=\#{limit}&\"" + end + it "adds query parameters to path and as required params on the method when required" do + file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" + FileUtils.rm_f("#{file_name}.rb") + OpenApiImport.from file_name, create_method_name: :operation_id + expect(File.exist?("#{file_name}.rb")).to eq true + content = File.read("#{file_name}.rb") + expect(content).to include "def self.find_pet_by_id(id)" + expect(content).to include "path: \"/api/pets/\#{id}\"," end + end end diff --git a/spec/open_api_import/open_api_import_responses_spec.rb b/spec/open_api_import/open_api_import_responses_spec.rb index 2d3caf5..68a8ed6 100644 --- a/spec/open_api_import/open_api_import_responses_spec.rb +++ b/spec/open_api_import/open_api_import_responses_spec.rb @@ -4,7 +4,7 @@ describe "#from" do it "adds response codes as keys on responses array" do file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true content = File.read("#{file_name}.rb") @@ -12,50 +12,50 @@ req = Swagger::SwaggerPetstore::V1_0_0::Root.find_pets expect(req.key?(:responses)).to eq true expect(req[:responses].class).to eq Hash - expect(req[:responses].keys).to eq [:'200', :'default'] + expect(req[:responses].keys).to eq [:"200", :default] end it "adds the message for the response on responses array" do file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true content = File.read("#{file_name}.rb") eval(content) req = Swagger::SwaggerPetstore::V1_0_0::Root.find_pets - expect(req[:responses][:'200'].key?(:message)).to eq true - expect(req[:responses][:'200'][:message]).to eq "pet response" + expect(req[:responses][:"200"].key?(:message)).to eq true + expect(req[:responses][:"200"][:message]).to eq "pet response" end it "adds the data body for the response on responses array" do file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true content = File.read("#{file_name}.rb") eval(content) req = Swagger::SwaggerPetstore::V1_0_0::Root.find_pets - expect(req[:responses][:'200'].key?(:data)).to eq true + expect(req[:responses][:"200"].key?(:data)).to eq true data = [{ name: "string", tag: "string", - id: 0, + id: 0 }] - expect(req[:responses][:'200'][:data]).to eq data + expect(req[:responses][:"200"][:data]).to eq data end it "uses 0.0 as default example for float numbers without explicit example" do file_name = "./spec/fixtures/v2.0/yaml/float_response.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true content = File.read("#{file_name}.rb") eval(content) req = Swagger::FloatApi::V1_0_0::Root.get_resource - expect(req[:responses][:'200'][:data]).to eq({ - floatValue: 0.0, - integerValue: 0, - }) + expect(req[:responses][:"200"][:data]).to eq({ + floatValue: 0.0, + integerValue: 0 + }) end end end diff --git a/spec/open_api_import/open_api_import_spec.rb b/spec/open_api_import/open_api_import_spec.rb index ef4d84e..b5f7179 100644 --- a/spec/open_api_import/open_api_import_spec.rb +++ b/spec/open_api_import/open_api_import_spec.rb @@ -4,13 +4,13 @@ describe "#from" do it "creates a log file if swagger_file is a valid string for a file name" do file_name = "example.yaml" - File.delete("#{file_name}_open_api_import.log") if File.exist?("#{file_name}_open_api_import.log") + FileUtils.rm_f("#{file_name}_open_api_import.log") OpenApiImport.from file_name expect(File.exist?("#{file_name}_open_api_import.log")).to eq true - expect(File.read("#{file_name}_open_api_import.log")).to match /swagger_file:\s#{file_name}/ + expect(File.read("#{file_name}_open_api_import.log")).to match(/swagger_file:\s#{file_name}/) end - it 'doesn\'t create a log file if swagger_file is not a valid string for a file name' do + it "doesn't create a log file if swagger_file is not a valid string for a file name" do file_name = "exa%$#@{}//&&[]`mple.yaml" OpenApiImport.from file_name expect(File.exist?("#{file_name}_open_api_import.log")).to eq false @@ -18,15 +18,15 @@ it "logs error when swagger version file is lower than supported" do file_name = "./spec/fixtures/wrong/petstore-minimal.yaml" - File.delete("#{file_name}_open_api_import.log") if File.exist?("#{file_name}_open_api_import.log") + FileUtils.rm_f("#{file_name}_open_api_import.log") OpenApiImport.from file_name expect(File.exist?("#{file_name}_open_api_import.log")).to eq true - expect(File.read("#{file_name}_open_api_import.log")).to match /Unsupported Swagger version/ + expect(File.read("#{file_name}_open_api_import.log")).to match(/Unsupported Swagger version/) end it "creates a requests file" do file_name = "./spec/fixtures/v2.0/yaml/petstore-minimal.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name expect(File.exist?("#{file_name}.rb")).to eq true end @@ -34,19 +34,19 @@ it "creates the module names correctly from the yaml swagger file" do file_name = "./spec/fixtures/v2.0/yaml/petstore-minimal.yaml" OpenApiImport.from file_name - expect(File.read("#{file_name}.rb")).to match /module\sSwagger\s+module\sSwaggerPetstore\s+module\sV1_0_0/ + expect(File.read("#{file_name}.rb")).to match(/module\sSwagger\s+module\sSwaggerPetstore\s+module\sV1_0_0/) end it "creates the module names correctly from the json swagger file" do file_name = "./spec/fixtures/v2.0/json/petstore-minimal.json" OpenApiImport.from file_name - expect(File.read("#{file_name}.rb")).to match /module\sSwagger\s+module\sSwaggerPetstore\s+module\sV1_0_0/ + expect(File.read("#{file_name}.rb")).to match(/module\sSwagger\s+module\sSwaggerPetstore\s+module\sV1_0_0/) end it "creates the module names correctly when name_for_module is :fixed" do file_name = "./spec/fixtures/v2.0/yaml/petstore-minimal.yaml" OpenApiImport.from file_name, name_for_module: :fixed - expect(File.read("#{file_name}.rb")).to match /module\sSwagger\s+module\sSwaggerPetstore\s+module\sV1_0_0\s+module\sRequests/ + expect(File.read("#{file_name}.rb")).to match(/module\sSwagger\s+module\sSwaggerPetstore\s+module\sV1_0_0\s+module\sRequests/) end it "creates the module names correctly when name_for_module is :path" do @@ -82,16 +82,16 @@ it "creates the module names correctly when name_for_module is :path_file" do file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" OpenApiImport.from file_name, name_for_module: :path_file - expect(File.read("#{file_name}_Pets.rb")).to match /module\sPets$/ - expect(File.read("#{file_name}_Root.rb")).to match /module\sRoot$/ + expect(File.read("#{file_name}_Pets.rb")).to match(/module\sPets$/) + expect(File.read("#{file_name}_Root.rb")).to match(/module\sRoot$/) end it "creates the module names correctly when name_for_module is :tags_file" do file_name = "./spec/fixtures/v2.0/yaml/uber.yaml" OpenApiImport.from file_name, name_for_module: :tags_file - expect(File.read("#{file_name}_Products.rb")).to match /module\sProducts$/ - expect(File.read("#{file_name}_Estimates.rb")).to match /module\sEstimates$/ - expect(File.read("#{file_name}_User.rb")).to match /module\sUser$/ + expect(File.read("#{file_name}_Products.rb")).to match(/module\sProducts$/) + expect(File.read("#{file_name}_Estimates.rb")).to match(/module\sEstimates$/) + expect(File.read("#{file_name}_User.rb")).to match(/module\sUser$/) end it "creates a file that requires all request files when name_for_module is :path_file" do @@ -115,12 +115,12 @@ file_name = "./spec/fixtures/wrong/petstore-minimal_not_supported_method.yaml" OpenApiImport.from file_name expect(File.exist?("#{file_name}_open_api_import.log")).to eq true - expect(File.read("#{file_name}_open_api_import.log")).to match /Not imported method: head for path: / + expect(File.read("#{file_name}_open_api_import.log")).to match(/Not imported method: head for path: /) end it "creates the name of the method using the http method and the path when create_method_name is :path" do file_name = "./spec/fixtures/v2.0/yaml/petstore-minimal.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :path expect(File.exist?("#{file_name}.rb")).to eq true expect(File.read("#{file_name}.rb")).to include("def self.get_pets(") @@ -128,7 +128,7 @@ it "creates the name of the method using the operationId in snake_case when create_method_name is :operation_id" do file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true expect(File.read("#{file_name}.rb")).to include("def self.find_pets(") @@ -136,7 +136,7 @@ it "creates the name of the method using the operationId like it is when create_method_name is :operationId" do file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operationId expect(File.exist?("#{file_name}.rb")).to eq true expect(File.read("#{file_name}.rb")).to include("def self.findPets(") @@ -144,7 +144,7 @@ it 'creates the name of the method using the default "undefined" when no operationId supplied' do file_name = "./spec/fixtures/v2.0/yaml/petstore-minimal.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true expect(File.read("#{file_name}.rb")).to include("def self.undefined(") @@ -152,7 +152,7 @@ it "creates all end points and http methods" do file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true content = File.read("#{file_name}.rb") @@ -164,7 +164,7 @@ it "creates module Root when no folder in path" do file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true content = File.read("#{file_name}.rb") @@ -173,7 +173,7 @@ it "creates module with name of folder in path" do file_name = "./spec/fixtures/v2.0/yaml/uber.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true content = File.read("#{file_name}.rb") @@ -182,7 +182,7 @@ it "adds info in comments: operationId, method, summary, description and parameters" do file_name = "./spec/fixtures/v2.0/yaml/uber.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true content = File.read("#{file_name}.rb") @@ -194,7 +194,7 @@ it "adds method key on request hash" do file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true content = File.read("#{file_name}.rb") @@ -206,7 +206,7 @@ it "adds name key on request hash" do file_name = "./spec/fixtures/v2.0/yaml/petstore-simple.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true content = File.read("#{file_name}.rb") @@ -219,19 +219,19 @@ it "detects version on method_name and add it to module" do file_name = "./spec/fixtures/v3.0/petstore_with_version.yaml" OpenApiImport.from file_name - expect(File.read("#{file_name}.rb")).to match /module\sV1Pets/ + expect(File.read("#{file_name}.rb")).to match(/module\sV1Pets/) end it "detects version on method_name and remove it" do file_name = "./spec/fixtures/v3.0/petstore_with_version.yaml" OpenApiImport.from file_name - expect(File.read("#{file_name}.rb")).to match /def\sself\.list_pets/ + expect(File.read("#{file_name}.rb")).to match(/def\sself\.list_pets/) end it "adds constants if create_constants" do file_name = "./spec/fixtures/v2.0/yaml/uber.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") - File.delete("#{file_name}_Root.rb") if File.exist?("#{file_name}_Root.rb") + FileUtils.rm_f("#{file_name}.rb") + FileUtils.rm_f("#{file_name}_Root.rb") OpenApiImport.from file_name, create_method_name: :path, create_constants: true expect(File.exist?("#{file_name}.rb")).to eq true content = File.read("#{file_name}.rb") @@ -242,7 +242,7 @@ it 'converts patterns from \\x to \\u' do file_name = "./spec/fixtures/v2.0/yaml/petstore.yaml" - File.delete("#{file_name}.rb") if File.exist?("#{file_name}.rb") + FileUtils.rm_f("#{file_name}.rb") OpenApiImport.from file_name, create_method_name: :operation_id expect(File.exist?("#{file_name}.rb")).to eq true content = File.read("#{file_name}.rb") diff --git a/spec/open_api_import/utils_spec.rb b/spec/open_api_import/utils_spec.rb new file mode 100644 index 0000000..efffc58 --- /dev/null +++ b/spec/open_api_import/utils_spec.rb @@ -0,0 +1,97 @@ +require "open_api_import" + +using OpenApiImportStringExt + +RSpec.describe OpenApiImportStringExt do + describe "#snake_case" do + it "converts CamelCase to snake_case" do + expect("CamelCase".snake_case).to eq "camel_case" + end + + it "converts PascalCase to snake_case" do + expect("PascalCaseString".snake_case).to eq "pascal_case_string" + end + + it "handles consecutive uppercase letters" do + expect("HTMLParser".snake_case).to eq "html_parser" + end + + it "replaces non-word characters with underscores" do + expect("hello-world".snake_case).to eq "hello_world" + end + + it "collapses multiple underscores" do + expect("hello__world".snake_case).to eq "hello_world" + end + + it "handles already snake_case strings" do + expect("already_snake".snake_case).to eq "already_snake" + end + + it "converts spaces to underscores" do + expect("hello world".snake_case).to eq "hello_world" + end + + it "handles path-like strings" do + expect("listUsers".snake_case).to eq "list_users" + end + + it "handles empty strings" do + expect("".snake_case).to eq "" + end + + it "handles all-uppercase strings" do + expect("API".snake_case).to eq "api" + end + + it "handles strings with numbers" do + expect("v2GetUser".snake_case).to eq "v2get_user" + end + + it "handles special characters" do + expect("hello/world".snake_case).to eq "hello_world" + end + + it "handles single character" do + expect("A".snake_case).to eq "a" + end + end + + describe "#camel_case" do + it "converts snake_case to CamelCase" do + expect("hello_world".camel_case).to eq "HelloWorld" + end + + it "converts hyphenated strings" do + expect("hello-world".camel_case).to eq "HelloWorld" + end + + it "returns already CamelCase strings unchanged" do + expect("HelloWorld".camel_case).to eq "HelloWorld" + end + + it "converts space-separated strings" do + expect("hello world".camel_case).to eq "HelloWorld" + end + + it "handles single word lowercase" do + expect("hello".camel_case).to eq "Hello" + end + + it "handles empty strings" do + expect("".camel_case).to eq "" + end + + it "returns all-caps strings unchanged" do + expect("API".camel_case).to eq "API" + end + + it "handles strings starting with uppercase but containing separators" do + expect("Hello_World".camel_case).to eq "HelloWorld" + end + + it "handles multiple underscores" do + expect("one__two___three".camel_case).to eq "OneTwoThree" + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index aa8b523..103edef 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,104 +1,26 @@ - -require 'coveralls' +require "coveralls" Coveralls.wear! -# This file was generated by the `rspec --init` command. Conventionally, all -# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause -# this file to always be loaded, without a need to explicitly require it in any -# files. -# -# Given that it is always loaded, you are encouraged to keep this file as -# light-weight as possible. Requiring heavyweight dependencies from this file -# will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, consider making -# a separate helper file that requires the additional dependencies and performs -# the additional setup, and require it from the spec files that actually need -# it. -# -# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +require "open_api_import" + RSpec.configure do |config| - # rspec-expectations config goes here. You can use an alternate - # assertion/expectation library such as wrong or the stdlib/minitest - # assertions if you prefer. config.expect_with :rspec do |expectations| - # This option will default to `true` in RSpec 4. It makes the `description` - # and `failure_message` of custom matchers include text for helper methods - # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" - # ...rather than: - # # => "be bigger than 2" expectations.include_chain_clauses_in_custom_matcher_descriptions = true end - # rspec-mocks config goes here. You can use an alternate test double - # library (such as bogus or mocha) by changing the `mock_with` option here. config.mock_with :rspec do |mocks| - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended, and will default to - # `true` in RSpec 4. mocks.verify_partial_doubles = true end - # This option will default to `:apply_to_host_groups` in RSpec 4 (and will - # have no way to turn it off -- the option exists only for backwards - # compatibility in RSpec 3). It causes shared context metadata to be - # inherited by the metadata hash of host groups and examples, rather than - # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin - # This allows you to limit a spec run to individual examples or groups - # you care about by tagging them with `:focus` metadata. When nothing - # is tagged with `:focus`, all examples get run. RSpec also provides - # aliases for `it`, `describe`, and `context` that include `:focus` - # metadata: `fit`, `fdescribe` and `fcontext`, respectively. config.filter_run_when_matching :focus - # Allows RSpec to persist some state between runs in order to support - # the `--only-failures` and `--next-failure` CLI options. We recommend - # you configure your source control system to ignore this file. - config.example_status_persistence_file_path = "spec/examples.txt" - - # Limits the available syntax to the non-monkey patched syntax that is - # recommended. For more details, see: - # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ - # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode - config.disable_monkey_patching! - - # This setting enables warnings. It's recommended, but in some cases may - # be too noisy due to issues in dependencies. - config.warnings = true + config.order = :defined - # Many RSpec users commonly either run the entire suite or an individual - # file, and it's useful to allow more verbose output when running an - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = "doc" + config.after(:suite) do + Dir.glob("spec/fixtures/**/*.rb").each { |f| File.delete(f) } + Dir.glob("spec/fixtures/**/*.log").each { |f| File.delete(f) } + Dir.glob("*.log").each { |f| File.delete(f) } end - - # Print the 10 slowest examples and example groups at the - # end of the spec run, to help surface which specs are running - # particularly slow. - config.profile_examples = 10 - - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed -=end end