diff --git a/lib/swagger_yard/model.rb b/lib/swagger_yard/model.rb index 9aaff06..ec2b647 100644 --- a/lib/swagger_yard/model.rb +++ b/lib/swagger_yard/model.rb @@ -3,82 +3,117 @@ module SwaggerYard # Carries id (the class name) and properties for a referenced # complex model object as defined by swagger schema # - class Model - include Example - attr_reader :id, :discriminator, :inherits, - :description, :properties, :additional_properties + Model = Struct.new(:id, :discriminator, :inherits, :description, :properties, :additional_properties, :example, keyword_init: true) do + def property(key) + properties.detect { |prop| prop.name == key } + end + end + + class ModelParser + def model + return unless id + + Model.new( + id: id, + discriminator: discriminator, + inherits: inherits, + description: @yard_object.docstring, + properties: properties, + example: example, + additional_properties: additional_properties, + ) + end def self.from_yard_object(yard_object) - new.tap do |model| - model.add_info(yard_object) - model.parse_tags(yard_object.tags) - yard_object.children.each do |child| - next unless child.is_a?(YARD::CodeObjects::MethodObject) - prop = Property.from_method(child) - model.properties << prop if prop - end - end + new(yard_object).model end def self.mangle(name) name.gsub(/[^[:alnum:]_]+/, '_') end - def initialize - @properties = [] - @inherits = [] + def initialize(yard_object) + @yard_object = yard_object end - def valid? - !id.nil? && @has_model_tag + def id + return unless tag('model') + + name = tag('model').text.presence || @yard_object.path + + self.class.mangle(name) end - def add_info(yard_object) - @description = yard_object.docstring - @id = Model.mangle(yard_object.path) + def inherits + tags('inherits').map(&:text) end - def property(key) - properties.detect {|prop| prop.name == key } + def discriminator + return unless tag('discriminator') + + Property.from_tag(tag('discriminator')).name end - TAG_ORDER = %w(model inherits discriminator property example additional_properties) - - def parse_tags(tags) - sorted_tags = tags.each_with_index.sort_by { |t,i| - [TAG_ORDER.index(t.tag_name), i] }.map(&:first) - sorted_tags.each do |tag| - case tag.tag_name - when "model" - @has_model_tag = true - @id = Model.mangle(tag.text) unless tag.text.empty? - when "property" - prop = Property.from_tag(tag) - @properties << prop if prop - when "discriminator" - prop = Property.from_tag(tag) - if prop - @properties << prop - @discriminator ||= prop.name - end - when "inherits" - @inherits << tag.text - when "example" - if tag.name && !tag.name.empty? - if (prop = property(tag.name)) - prop.example = tag.text - else - SwaggerYard.log.warn("no property '#{tag.name}' defined yet to which to attach example: #{tag.text.inspect}") - end - else - self.example = tag.text - end - when "additional_properties" - @additional_properties = Type.new(tag.text).schema + def properties + return @properties if @properties + + @properties = [] + + # Properties from the direct tags + tags('property').each do |property_tag| + property = Property.from_tag(property_tag) + @properties.push property if property + end + + # Property from discriminator tag + @properties.push Property.from_tag(tag('discriminator')) if tag('discriminator') + + # Properties from nested method definition + @yard_object.children.each do |child| + next unless child.is_a?(YARD::CodeObjects::MethodObject) + property = Property.from_method(child) + @properties.push property if property + end + + # Search examples + tags('example').each do |example_tag| + next if example_tag.name.blank? + + property = @properties.find { |prop| prop.name == example_tag.name } + if property + property.example = example_tag.text + else + SwaggerYard.log.warn <<~MESSAGE + no property '#{example_tag.name}' defined yet to which to attach example: + + #{example_tag.text.inspect} + + MESSAGE end end - self + @properties + end + + def tag(key) + @yard_object.tags.find { |tag| tag.tag_name == key } + end + + def tags(key) + @yard_object.tags.select { |tag| tag.tag_name == key } + end + + def additional_properties + return unless tag('additional_properties') + + Type.new(tag('additional_properties').text).schema + end + + def example + tag = tags('example').find { |tag| tag.name.blank? } + return unless tag + + JSON.parse(tag.text) rescue tag.text end end end diff --git a/lib/swagger_yard/specification.rb b/lib/swagger_yard/specification.rb index 7e9e21b..9b80087 100644 --- a/lib/swagger_yard/specification.rb +++ b/lib/swagger_yard/specification.rb @@ -44,10 +44,10 @@ def parse_models @model_paths.map do |model_path| Dir[model_path.to_s].map do |file_path| SwaggerYard.yard_class_objects_from_file(file_path).map do |obj| - Model.from_yard_object(obj) + ModelParser.from_yard_object(obj) end end - end.flatten.compact.select(&:valid?) + end.flatten.compact end def parse_controllers diff --git a/lib/swagger_yard/type_parser.rb b/lib/swagger_yard/type_parser.rb index aec0e8e..a387210 100644 --- a/lib/swagger_yard/type_parser.rb +++ b/lib/swagger_yard/type_parser.rb @@ -78,7 +78,7 @@ class Transform < Parslet::Transform when "date-time", "date", "time", "uuid" { 'type' => 'string', 'format' => v } else - name = Model.mangle(v) + name = ModelParser.mangle(v) if /[[:upper:]]/.match(name) { '$ref' => "#{model_path}#{name}" } else @@ -96,7 +96,7 @@ class Transform < Parslet::Transform rule(external_identifier: { namespace: simple(:namespace), identifier: simple(:identifier) }) do prefix, name = namespace.to_s, identifier.to_s url, fragment = resolve_uri.call(name, prefix) - { '$ref' => "#{url}#{fragment}#{Model.mangle(name)}" } + { '$ref' => "#{url}#{fragment}#{ModelParser.mangle(name)}" } end rule(formatted: { name: simple(:name), format: simple(:format) }) do diff --git a/spec/fixtures/models/person.rb b/spec/fixtures/models/person.rb index 2cda35f..b17a0e0 100644 --- a/spec/fixtures/models/person.rb +++ b/spec/fixtures/models/person.rb @@ -1,5 +1,6 @@ # @model Person # @property [Person] parent +# @discriminator myType(required) [string] class Person extend Forwardable diff --git a/spec/lib/swagger_yard/model_spec.rb b/spec/lib/swagger_yard/model_spec.rb index 63568c7..edc98b2 100644 --- a/spec/lib/swagger_yard/model_spec.rb +++ b/spec/lib/swagger_yard/model_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -RSpec.describe SwaggerYard::Model do +RSpec.describe SwaggerYard::ModelParser do let(:content) do [ "@model MyModel", "@discriminator myType(required) [string]" ].join("\n") @@ -51,7 +51,7 @@ context "with no @model tag" do let(:content) { "Some description without a SwaggerYard model tag" } - it { is_expected.to_not be_valid } + it { is_expected.to be_nil } end context "with an @example" do @@ -100,6 +100,7 @@ its('properties') { is_expected.to include(a_property_named('address'), a_property_named('parent'), + a_property_named('myType'), a_property_named('age')) } its('properties') { is_expected.to_not include(a_property_named('some_non_model_method'), diff --git a/spec/lib/swagger_yard/swagger_spec.rb b/spec/lib/swagger_yard/swagger_spec.rb index 8324b5d..779574c 100644 --- a/spec/lib/swagger_yard/swagger_spec.rb +++ b/spec/lib/swagger_yard/swagger_spec.rb @@ -136,7 +136,7 @@ end context "models" do - let(:model) { SwaggerYard::Model.from_yard_object(yard_class('MyModel', content)) } + let(:model) { SwaggerYard::ModelParser.from_yard_object(yard_class('MyModel', content)) } let(:spec) { stub(path_objects: SwaggerYard::Paths.new([]), tag_objects: [], security_objects: [], model_objects: { model.id => model }) } diff --git a/spec/support/shared_examples.rb b/spec/support/shared_examples.rb index 8167f55..0eead8c 100644 --- a/spec/support/shared_examples.rb +++ b/spec/support/shared_examples.rb @@ -11,5 +11,5 @@ SwaggerYard.yard_class_objects_from_file((FIXTURE_PATH + 'models' + 'person.rb').to_s) end - let(:model) { SwaggerYard::Model.from_yard_object(objects.first) } + let(:model) { SwaggerYard::ModelParser.from_yard_object(objects.first) } end