diff --git a/gen-client.sh b/gen-client.sh index 8c51982..97f5e5b 100755 --- a/gen-client.sh +++ b/gen-client.sh @@ -34,7 +34,10 @@ if language.lower() == "python": else: print("v4.3.1") elif language.lower() == "ruby": - print("v4.3.1") + if core_version >= Version("3.70.dev"): + print("v7.10.0") + else: + print("v4.3.1") elif language.lower() == "typescript": print("v5.2.1") else: diff --git a/templates/diff.sh b/templates/diff.sh new file mode 100755 index 0000000..b6b44cb --- /dev/null +++ b/templates/diff.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -eu + +if [ $# != 3 ] +then + echo "Usage: ./diff.sh " + echo "Example: ./diff.sh ruby 7.10.0 gemspec.mustache" + echo "Available langauges: python, ruby, typescript" + echo "" + echo "Reference: https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources" + exit 1 +fi + +PROJECT_ROOT="$(git rev-parse --show-toplevel)" +LANG=$1 +VERSION=$2 +FILENAME=$3 + +CLIENT_NAME=$1 +if [ $LANG = "ruby" ] +then + CLIENT_NAME="ruby-client" +fi + +LOCAL_FILE="$PROJECT_ROOT/templates/$LANG/v$VERSION/$FILENAME" +REMOTE_FILE_URL="https://raw.githubusercontent.com/OpenAPITools/openapi-generator/refs/tags/v$VERSION/modules/openapi-generator/src/main/resources/$CLIENT_NAME/$FILENAME" + +git diff "$LOCAL_FILE" <(curl -s "$REMOTE_FILE_URL") diff --git a/templates/ruby/v7.10.0/api_client.mustache b/templates/ruby/v7.10.0/api_client.mustache new file mode 100644 index 0000000..ac21747 --- /dev/null +++ b/templates/ruby/v7.10.0/api_client.mustache @@ -0,0 +1,247 @@ +=begin +{{> api_info}} +=end + +require 'date' +require 'json' +require 'logger' +require 'tempfile' +require 'time' +{{#isTyphoeus}} +require 'typhoeus' +{{/isTyphoeus}} +{{#isFaraday}} +require 'faraday' +require 'faraday/multipart' if Gem::Version.new(Faraday::VERSION) >= Gem::Version.new('2.0') +require 'marcel' +{{/isFaraday}} +{{#isHttpx}} +require 'httpx' +require 'net/http/status' +{{/isHttpx}} +require 'pathname' + + +module {{moduleName}} + class ApiClient + # The Configuration object holding settings to be used in the API client. + attr_accessor :config + + # Defines the headers to be used in HTTP requests of all API calls by default. + # + # @return [Hash] + attr_accessor :default_headers + + # Initializes the ApiClient + # @option config [Configuration] Configuration for initializing the object, default to Configuration.default + def initialize(config = Configuration.default) + @config = config + @user_agent = "{{{httpUserAgent}}}{{^httpUserAgent}}OpenAPI-Generator/#{VERSION}/ruby{{/httpUserAgent}}" + @default_headers = { + 'Content-Type' => 'application/json', + 'User-Agent' => @user_agent + } + end + + def self.default + @@default ||= ApiClient.new + end + +{{#isTyphoeus}} +{{> api_client_typhoeus_partial}} +{{/isTyphoeus}} +{{#isFaraday}} +{{> api_client_faraday_partial}} +{{/isFaraday}} +{{#isHttpx}} +{{> api_client_httpx_partial}} +{{/isHttpx}} + # Check if the given MIME is a JSON MIME. + # JSON MIME examples: + # application/json + # application/json; charset=UTF8 + # APPLICATION/JSON + # */* + # @param [String] mime MIME + # @return [Boolean] True if the MIME is application/json + def json_mime?(mime) + (mime == '*/*') || !(mime =~ /^Application\/.*json(?!p)(;.*)?/i).nil? + end + + # Deserialize the response to the given return type. + # + # @param [Response] response HTTP response + # @param [String] return_type some examples: "User", "Array", "Hash" + def deserialize(response, return_type) + body = response.body + return nil if body.nil? || body.empty? + + # return response body directly for String return type + return body.to_s if return_type == 'String' + + # ensuring a default content type + content_type = response.headers['Content-Type'] || 'application/json' + + fail "Content-Type is not supported: #{content_type}" unless json_mime?(content_type) + + begin + data = JSON.parse("[#{body}]", :symbolize_names => true)[0] + rescue JSON::ParserError => e + if %w(String Date Time).include?(return_type) + data = body + else + raise e + end + end + + convert_to_type data, return_type + end + + # Convert data to the given return type. + # @param [Object] data Data to be converted + # @param [String] return_type Return type + # @return [Mixed] Data in a particular type + def convert_to_type(data, return_type) + return nil if data.nil? + case return_type + when 'String' + data.to_s + when 'Integer' + data.to_i + when 'Float' + data.to_f + when 'Boolean' + data == true + when 'Time' + # parse date time (expecting ISO 8601 format) + Time.parse data + when 'Date' + # parse date time (expecting ISO 8601 format) + Date.parse data + when 'Object' + # generic object (usually a Hash), return directly + data + when /\AArray<(.+)>\z/ + # e.g. Array + sub_type = $1 + data.map { |item| convert_to_type(item, sub_type) } + when /\AHash\\z/ + # e.g. Hash + sub_type = $1 + {}.tap do |hash| + data.each { |k, v| hash[k] = convert_to_type(v, sub_type) } + end + else + # models (e.g. Pet) or oneOf + klass = {{moduleName}}.const_get(return_type) + klass.respond_to?(:openapi_one_of) ? klass.build(data) : klass.build_from_hash(data) + end + end + + # Sanitize filename by removing path. + # e.g. ../../sun.gif becomes sun.gif + # + # @param [String] filename the filename to be sanitized + # @return [String] the sanitized filename + def sanitize_filename(filename) + filename.split(/[\/\\]/).last + end + + def build_request_url(path, opts = {}) + # Add leading and trailing slashes to path + path = "/#{path}".gsub(/\/+/, '/') + @config.base_url(opts[:operation]) + path + end + + # Update header and query params based on authentication settings. + # + # @param [Hash] header_params Header parameters + # @param [Hash] query_params Query parameters + # @param [String] auth_names Authentication scheme name + def update_params_for_auth!(header_params, query_params, auth_names) + Array(auth_names).each do |auth_name| + auth_setting = @config.auth_settings[auth_name] + next unless auth_setting + case auth_setting[:in] + when 'header' then header_params[auth_setting[:key]] = auth_setting[:value] + when 'query' then query_params[auth_setting[:key]] = auth_setting[:value] + else fail ArgumentError, 'Authentication token must be in `query` or `header`' + end + end + end + + # Sets user agent in HTTP header + # + # @param [String] user_agent User agent (e.g. openapi-generator/ruby/1.0.0) + def user_agent=(user_agent) + @user_agent = user_agent + @default_headers['User-Agent'] = @user_agent + end + + # Return Accept header based on an array of accepts provided. + # @param [Array] accepts array for Accept + # @return [String] the Accept header (e.g. application/json) + def select_header_accept(accepts) + return nil if accepts.nil? || accepts.empty? + # use JSON when present, otherwise use all of the provided + json_accept = accepts.find { |s| json_mime?(s) } + json_accept || accepts.join(',') + end + + # Return Content-Type header based on an array of content types provided. + # @param [Array] content_types array for Content-Type + # @return [String] the Content-Type header (e.g. application/json) + def select_header_content_type(content_types) + # return nil by default + return if content_types.nil? || content_types.empty? + # use JSON when present, otherwise use the first one + json_content_type = content_types.find { |s| json_mime?(s) } + json_content_type || content_types.first + end + + # Convert object (array, hash, object, etc) to JSON string. + # @param [Object] model object to be converted into JSON string + # @return [String] JSON string representation of the object + def object_to_http_body(model) + return model if model.nil? || model.is_a?(String) + local_body = nil + if model.is_a?(Array) + local_body = model.map { |m| object_to_hash(m) } + else + local_body = object_to_hash(model) + end + local_body.to_json + end + + # Convert object(non-array) to hash. + # @param [Object] obj object to be converted into JSON string + # @return [String] JSON string representation of the object + def object_to_hash(obj) + if obj.respond_to?(:to_hash) + obj.to_hash + else + obj + end + end + + # Build parameter value according to the given collection format. + # @param [String] collection_format one of :csv, :ssv, :tsv, :pipes and :multi + def build_collection_param(param, collection_format) + case collection_format + when :csv + param.join(',') + when :ssv + param.join(' ') + when :tsv + param.join("\t") + when :pipes + param.join('|') + when :multi + # return the array directly as typhoeus will handle it as expected + param + else + fail "unknown collection format: #{collection_format.inspect}" + end + end + end +end diff --git a/templates/ruby/v7.10.0/gemspec.mustache b/templates/ruby/v7.10.0/gemspec.mustache new file mode 100644 index 0000000..3503583 --- /dev/null +++ b/templates/ruby/v7.10.0/gemspec.mustache @@ -0,0 +1,42 @@ +# -*- encoding: utf-8 -*- + +=begin +{{> api_info}} +=end + +$:.push File.expand_path("../lib", __FILE__) +require "{{gemName}}/version" + +Gem::Specification.new do |s| + s.name = "{{gemName}}{{^gemName}}{{{appName}}}{{/gemName}}" + s.version = {{moduleName}}::VERSION + s.platform = Gem::Platform::RUBY + s.authors = ["{{gemAuthor}}{{^gemAuthor}}OpenAPI-Generator{{/gemAuthor}}"] + s.email = ["{{gemAuthorEmail}}{{^gemAuthorEmail}}{{infoEmail}}{{/gemAuthorEmail}}"] + s.homepage = "{{gemHomepage}}{{^gemHomepage}}https://openapi-generator.tech{{/gemHomepage}}" + s.summary = "{{gemSummary}}{{^gemSummary}}{{{appName}}} Ruby Gem{{/gemSummary}}" + s.description = "{{gemDescription}}{{^gemDescription}}{{{appDescription}}}{{^appDescription}}{{{appName}}} Ruby Gem{{/appDescription}}{{/gemDescription}}" + s.license = "{{{gemLicense}}}{{^gemLicense}}Unlicense{{/gemLicense}}" + s.required_ruby_version = "{{{gemRequiredRubyVersion}}}{{^gemRequiredRubyVersion}}>= 2.7{{/gemRequiredRubyVersion}}" + s.metadata = {{{gemMetadata}}}{{^gemMetadata}}{}{{/gemMetadata}} + + {{#isFaraday}} + s.add_runtime_dependency 'faraday-net_http', '>= 2.0', '< 3.1' + s.add_runtime_dependency 'faraday', '>= 1.0.1', '< 2.9' + s.add_runtime_dependency 'faraday-multipart' + s.add_runtime_dependency 'marcel' + {{/isFaraday}} + {{#isTyphoeus}} + s.add_runtime_dependency 'typhoeus', '~> 1.0', '>= 1.0.1' + {{/isTyphoeus}} + {{#isHttpx}} + s.add_runtime_dependency 'httpx', '~> 1.0', '>= 1.0.0' + {{/isHttpx}} + + s.add_development_dependency 'rspec', '~> 3.6', '>= 3.6.0' + + s.files = `find *`.split("\n").uniq.sort.select { |f| !f.empty? } + s.test_files = `find spec/*`.split("\n") + s.executables = [] + s.require_paths = ["lib"] +end diff --git a/tests/ruby_workflow.rb b/tests/ruby_workflow.rb new file mode 100644 index 0000000..3f7fb94 --- /dev/null +++ b/tests/ruby_workflow.rb @@ -0,0 +1,148 @@ +require 'pulpcore_client' +require 'pulp_file_client' +require 'tempfile' +require 'digest' + + +PulpcoreClient.configure do |config| + config.host= "http://localhost:5001" + config.username= 'admin' + config.password= 'password' + config.debugging=true +end + +PulpFileClient.configure do |config| + config.host= "http://localhost:5001" + config.username= 'admin' + config.password= 'password' + config.debugging=true +end + + +@artifacts_api = PulpcoreClient::ArtifactsApi.new +@filerepositories_api = PulpFileClient::RepositoriesFileApi.new +@repoversions_api = PulpFileClient::RepositoriesFileVersionsApi.new +@filecontent_api = PulpFileClient::ContentFilesApi.new +@filedistributions_api = PulpFileClient::DistributionsFileApi.new +@filepublications_api = PulpFileClient::PublicationsFileApi.new +@fileremotes_api = PulpFileClient::RemotesFileApi.new +@tasks_api = PulpcoreClient::TasksApi.new +@uploads_api = PulpcoreClient::UploadsApi.new + + +def monitor_task(task_href) + # Polls the Task API until the task is in a completed state. + # + # Prints the task details and a success or failure message. Exits on failure. + # + # Args: + # task_href(str): The href of the task to monitor + # + # Returns: + # list[str]: List of hrefs that identify resource created by the task + task = @tasks_api.read(task_href) + until ["completed", "failed", "canceled"].include? task.state + sleep(2) + task = @tasks_api.read(task_href) + end + if task.state == 'completed' + task.created_resources + else + print("Task failed. Exiting.\n") + exit(2) + end +end + +def content_range(start, finish, total) + finish = finish > total ? total : finish + "bytes #{start}-#{finish}/#{total}" +end + +def upload_file_in_chunks(file_path) + # Uploads a file using the Uploads API + # + # The file located at 'file_path' is uploaded in chunks of 200kb. + # + # Args: + # file_path (str): path to the file being uploaded to Pulp + # + # Returns: + # upload object + response = "" + File.open(file_path, "rb") do |file| + total_size = File.size(file) + upload_data = PulpcoreClient::Upload.new({size: total_size}) + response = @uploads_api.create(upload_data) + upload_href = response.pulp_href + chunksize = 200000 + offset = 0 + sha256 = Digest::SHA256.new + until file.eof? + chunk = file.read(chunksize) + sha256.update(chunk) + begin + filechunk = Tempfile.new('fred') + filechunk.write(chunk) + filechunk.flush() + actual_chunk_size = File.size(filechunk) + response = @uploads_api.update(content_range(offset, offset + actual_chunk_size - 1, total_size), upload_href, filechunk) + offset += actual_chunk_size - 1 + ensure + filechunk.close + filechunk.unlink + end + end + upload_commit_response = @uploads_api.commit(upload_href, {sha256: sha256.hexdigest}) + created_resources = monitor_task(upload_commit_response.task) + @artifacts_api.read(created_resources[0]) + end +end + + +artifact = upload_file_in_chunks(File.new(File.expand_path(__FILE__))) + +# Create a File Remote +remote_url = 'https://fixtures.pulpproject.org/file/PULP_MANIFEST' +remote_data = PulpFileClient::FileFileRemote.new({name: 'bar38', url: remote_url}) +file_remote = @fileremotes_api.create(remote_data) + +# Create a Repository +repository_data = PulpFileClient::FileFileRepository.new({name: 'foo38'}) +file_repository = @filerepositories_api.create(repository_data) + +# Sync a Repository +repository_sync_data = PulpFileClient::RepositorySyncURL.new({remote: file_remote.pulp_href}) +sync_response = @filerepositories_api.sync(file_repository.pulp_href, repository_sync_data) + +# Monitor the sync task +created_resources = monitor_task(sync_response.task) + +repository_version_1 = @repoversions_api.read(created_resources[0]) + +# Create a FileContent from a local file +filecontent_response = @filecontent_api.create('foo.tar.gz', {file: File.new(File.expand_path(__FILE__))}) + +created_resources = monitor_task(filecontent_response.task) + +# Add the new FileContent to a repository version +repo_version_data = {add_content_units: [created_resources[0]]} +repo_version_response = @filerepositories_api.modify(file_repository.pulp_href, repo_version_data) + +# Monitor the repo version creation task +created_resources = monitor_task(repo_version_response.task) + +repository_version_2 = @repoversions_api.read(created_resources[0]) + +# List all the repository versions +@repoversions_api.list(file_repository.pulp_href) + +# Create a publication from the latest version of the repository +publish_data = PulpFileClient::FileFilePublication.new({repository: file_repository.pulp_href}) +publish_response = @filepublications_api.create(publish_data) + +# Monitor the publish task +created_resources = monitor_task(publish_response.task) +publication_href = created_resources[0] + +distribution_data = PulpFileClient::FileFileDistribution.new({name: 'baz38', base_path: 'foo38', publication: publication_href}) +distribution = @filedistributions_api.create(distribution_data) diff --git a/tests/test_ruby.sh b/tests/test_ruby.sh new file mode 100755 index 0000000..acd94cc --- /dev/null +++ b/tests/test_ruby.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Script to test ruby bindings locally. +# Recommended to have rbenv installed and set to the appropirate ruby version. + +set -mveuo pipefail + +# Configure environment +GIT_PROJECT_ROOT="$(git rev-parse --show-toplevel)" +cd "$GIT_PROJECT_ROOT" +export PULP_URL="http://localhost:5001" + +# Configure "isolated" ruby on host machine +# https://stackoverflow.com/a/17413767 +TMPDIR="/tmp/ruby-venv" +rm -rf $TMPDIR +mkdir -p $TMPDIR/local/gems +export GEM_HOME=$TMPDIR/local/gems + +# Generate clients for pulpcore and pulp_file +rm -rf ./pulpcore-client +./generate.sh pulpcore ruby +pushd pulpcore-client + gem build pulpcore_client + gem install --both ./pulpcore_client-*.gem +popd + +rm -rf ./pulp_file-client +./generate.sh pulp_file ruby +pushd pulp_file-client +gem build pulp_file_client +gem install --both ./pulp_file_client-*.gem +popd + +# Run tests +ruby tests/ruby_workflow.rb