Skip to content

Conversation

dhamo-ror
Copy link

Hi @agrare, could you kindly review this PR and let me know your suggestions.

@dhamo-ror dhamo-ror requested a review from bdunne as a code owner September 13, 2025 05:46
Comment on lines 29 to 43
def get_model_class(klass)
case klass
when String
begin
klass.constantize
rescue NameError => e
puts "Warning: Could not constantize #{klass}: #{e.message}"
nil
end
when Class
klass
else
nil
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it could be just klass = klass.safe_constantize if klass.kind_of?(String)
If we don't need to handle Class or String and only String then we could use safe_constantize directly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE I threw a breakpoint in else and Class and no instances of that so I think we can do collection.klass&.safe_constantize below

"properties" => {
"name" => {"type" => "string", "example" => collection_name},
"count" => {"type" => "integer", "description" => "Total number of resources"},
"subcount" => {"type" => "integer", "description" => "Number of resources returned"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor

Suggested change
"subcount" => {"type" => "integer", "description" => "Number of resources returned"},
"subcount" => {"type" => "integer", "description" => "Number of resources returned after filters applied"},

"type" => "object",
"properties" => {
"name" => {"type" => "string", "example" => collection_name},
"count" => {"type" => "integer", "description" => "Total number of resources"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"count" => {"type" => "integer", "description" => "Total number of resources"},
"count" => {"type" => "integer", "description" => "Total number of resources without filters applied"},

Comment on lines 194 to 196
"404" => {"description" => "Resource not found"},
"401" => {"description" => "Unauthorized"},
"403" => {"description" => "Forbidden"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor, but let's sort these.

Suggested change
"404" => {"description" => "Resource not found"},
"401" => {"description" => "Unauthorized"},
"403" => {"description" => "Forbidden"}
"401" => {"description" => "Unauthorized"},
"403" => {"description" => "Forbidden"},
"404" => {"description" => "Resource not found"}

Comment on lines 408 to 409
collection.respond_to?(:collection_actions) &&
collection.collection_actions&.key?(action)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
collection.respond_to?(:collection_actions) &&
collection.collection_actions&.key?(action)
collection.respond_to?(:collection_actions) &&
collection.collection_actions&.key?(action)

Comment on lines 413 to 414
collection.respond_to?(:resource_actions) &&
collection.resource_actions&.key?(action)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
collection.respond_to?(:resource_actions) &&
collection.resource_actions&.key?(action)
collection.respond_to?(:resource_actions) &&
collection.resource_actions&.key?(action)

def build_update_schema(model_class, schema_name)
properties = build_writable_properties(model_class)
# Make all properties optional for PATCH
properties.each { |_, prop| prop.delete("required") if prop.is_a?(Hash) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
properties.each { |_, prop| prop.delete("required") if prop.is_a?(Hash) }
properties.each_value { |prop| prop.delete("required") if prop.is_a?(Hash) }

Comment on lines 418 to 422
if collection.respond_to?(:resource_actions) && collection.resource_actions
collection.resource_actions.keys
else
[]
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if collection.respond_to?(:resource_actions) && collection.resource_actions
collection.resource_actions.keys
else
[]
end
collection.try(:resource_actions)&.keys || []

Comment on lines 65 to 69
next unless collection.klass

# Skip if we can't get a valid model class
model_class = get_model_class(collection.klass)
next unless model_class
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is always a string we could do

model_class = collection.klass&.safe_constantize
next if model_class.nil?

Comment on lines 470 to 471
"subcoll_id" => {
"name" => "subcoll_id",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be spelled out here.

Suggested change
"subcoll_id" => {
"name" => "subcoll_id",
"subcollection_id" => {
"name" => "subcollection_id",

# POST collection - if creation is supported
if supports_collection_action?(collection, :post)
operations["post"] = {
"summary" => "Create #{collection_name.to_s.singularize}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to check the collection_actions here because not every POST on the collection creates the resource.

Example: VMs has a lot of collection_actions but none of them are :name: create

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @agrare, I request your input on this to proceed further.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

@dhamo-ror dhamo-ror Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like this @agrare?,

# POST collection - only if creation is supported
  if collection.collection_actions&.key?(:create)
    operations["post"] = {
      "summary" => "Create #{collection_name.to_s.singularize}",
      "description" => "Create a new #{collection_name.to_s.singularize}",
      "requestBody" => {
        "required" => true,
        "content" => {
          "application/json" => {
            "schema" => build_create_schema(model_class, schema_name)
          }
        }
      },
      "responses" => {
        "201" => {
          "description" => "Resource created successfully",
          "content" => {
            "application/json" => {
              "schema" => {"$ref" => "##{SCHEMAS_PATH}/#{schema_name}"}
            }
          }
        },
        "400" => {"description" => "Bad Request - Invalid input"},
        "401" => {"description" => "Unauthorized"},
        "403" => {"description" => "Forbidden"},
        "422" => {"description" => "Unprocessable Entity - Validation errors"}
      },
      "tags" => [collection_name.to_s.titleize]
    }
  end

  # Document other collection_actions (excluding :create) as POSTs
  collection.collection_actions&.each do |action, config|
    next if action == :create
    operations["post_#{action}"] = {
      "summary" => "Invoke #{action.to_s.titleize} on #{collection_name}",
      "description" => "Perform the #{action} action on the #{collection_name} collection.",
      "requestBody" => {
        "required" => true,
        "content" => {
          "application/json" => {
            "schema" => {
              "type" => "object",
              "required" => ["action"],
              "properties" => {
                "action" => {
                  "type" => "string",
                  "enum" => [action.to_s],
                  "description" => "Name of the collection action"
                },
                # Optionally add custom params from config if needed
              }
            }
          }
        }
      },
      "responses" => {
        "200" => {"description" => "Action performed successfully"},
        "400" => {"description" => "Bad Request - Invalid action or parameters"},
        "401" => {"description" => "Unauthorized"},
        "403" => {"description" => "Forbidden"},
        "422" => {"description" => "Action cannot be performed"}
      },
      "tags" => [collection_name.to_s.titleize]
    }
  end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to special case create there or can we treat it like all of the other collection actions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes @agrare, We can handle create the same way as the other collection actions

@Fryguy
Copy link
Member

Fryguy commented Sep 15, 2025

This is an awesome PR @dhamo-ror. Thank you!

Can you add the generated openapi.yml to this PR as well, which is the result of running these updates? That way we can also see the final result.

Also, not necessarily for this PR, but in the future we also need OPTIONS support for the endpoints, and we need to also add the /api endpoint itself, which is details about the server and the logged in user itself.

@dhamo-ror
Copy link
Author

Thanks @Fryguy for your review. I’ll address the comments, update the PR and share the YAML file as well. I also plan to include all actions and endpoints in this, so please share your inputs if I’ve missed anything.

@dhamo-ror
Copy link
Author

Hi @Fryguy, I’m attaching the JSON file generated for the API. Please take a look and share your feedback.

openapi.json

@Fryguy
Copy link
Member

Fryguy commented Sep 18, 2025

@dhamo-ror It should just be a part of this PR is what I mean. We have the results of running the generator in https://github.com/ManageIQ/manageiq-api/blob/master/config/openapi.json

@dhamo-ror
Copy link
Author

Understood @Fryguy, Once we finalize the generator.rb file I'll run that and commit that in this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants