Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ updates:
open-pull-requests-limit: 10
cooldown:
default-days: 7
- package-ecosystem: bundler
directory: /components/serialization/multipart
Comment thread
Copilot marked this conversation as resolved.
schedule:
interval: daily
open-pull-requests-limit: 10
cooldown:
default-days: 7
- package-ecosystem: bundler
directory: /components/http
schedule:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
- { name: serialization-json, dir: components/serialization/json }
- { name: serialization-form, dir: components/serialization/form }
- { name: serialization-text, dir: components/serialization/text }
- { name: serialization-multipart, dir: components/serialization/multipart }
- { name: http, dir: components/http }
- { name: authentication-oauth, dir: components/authentication/oauth }
steps:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
- { name: serialization-json, dir: components/serialization/json }
- { name: serialization-form, dir: components/serialization/form }
- { name: serialization-text, dir: components/serialization/text }
- { name: serialization-multipart, dir: components/serialization/multipart }
- { name: http, dir: components/http }
- { name: authentication-oauth, dir: components/authentication/oauth }
runs-on: ${{ matrix.os }}
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ gem 'microsoft_kiota_authentication_oauth', path: 'components/authentication/oau
gem 'microsoft_kiota_faraday', path: 'components/http'
gem 'microsoft_kiota_serialization_form', path: 'components/serialization/form'
gem 'microsoft_kiota_serialization_json', path: 'components/serialization/json'
gem 'microsoft_kiota_serialization_multipart', path: 'components/serialization/multipart'
gem 'microsoft_kiota_serialization_text', path: 'components/serialization/text'

gem 'rake', '~> 13.0'
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Ruby 3.3 or newer is required.
| [microsoft_kiota_serialization_json](components/serialization/json/) | JSON serialization implementation | [![Gem Version](https://badge.fury.io/rb/microsoft_kiota_serialization_json.svg)](https://rubygems.org/gems/microsoft_kiota_serialization_json) |
| [microsoft_kiota_serialization_form](components/serialization/form/) | Form serialization implementation | [![Gem Version](https://badge.fury.io/rb/microsoft_kiota_serialization_form.svg)](https://rubygems.org/gems/microsoft_kiota_serialization_form) |
| [microsoft_kiota_serialization_text](components/serialization/text/) | Text serialization implementation | [![Gem Version](https://badge.fury.io/rb/microsoft_kiota_serialization_text.svg)](https://rubygems.org/gems/microsoft_kiota_serialization_text) |
| [microsoft_kiota_serialization_multipart](components/serialization/multipart/) | Multipart serialization implementation | [![Gem Version](https://badge.fury.io/rb/microsoft_kiota_serialization_multipart.svg)](https://rubygems.org/gems/microsoft_kiota_serialization_multipart) |
| [microsoft_kiota_faraday](components/http/) | HTTP client implementation with Faraday | [![Gem Version](https://badge.fury.io/rb/microsoft_kiota_faraday.svg)](https://rubygems.org/gems/microsoft_kiota_faraday) |
| [microsoft_kiota_authentication_oauth](components/authentication/oauth/) | OAuth authentication provider | [![Gem Version](https://badge.fury.io/rb/microsoft_kiota_authentication_oauth.svg)](https://rubygems.org/gems/microsoft_kiota_authentication_oauth) |

Expand All @@ -28,6 +29,7 @@ gem 'microsoft_kiota_abstractions'
gem 'microsoft_kiota_serialization_json'
gem 'microsoft_kiota_serialization_form'
gem 'microsoft_kiota_serialization_text'
gem 'microsoft_kiota_serialization_multipart'
gem 'microsoft_kiota_faraday'
gem 'microsoft_kiota_authentication_oauth'
```
Expand All @@ -51,6 +53,7 @@ rake abstractions:spec
rake serialization-json:spec
rake serialization-form:spec
rake serialization-text:spec
rake serialization-multipart:spec
rake http:spec
rake authentication-oauth:spec
```
Expand Down
1 change: 1 addition & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ COMPONENTS = {
'serialization-json' => 'components/serialization/json',
'serialization-form' => 'components/serialization/form',
'serialization-text' => 'components/serialization/text',
'serialization-multipart' => 'components/serialization/multipart',
'http' => 'components/http',
'authentication-oauth' => 'components/authentication/oauth'
}.freeze
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
require_relative 'microsoft_kiota_abstractions/request_option'
require_relative 'microsoft_kiota_abstractions/request_headers'
require_relative 'microsoft_kiota_abstractions/request_information'
require_relative 'microsoft_kiota_abstractions/multipart_body'
require_relative 'microsoft_kiota_abstractions/version'
require_relative 'microsoft_kiota_abstractions/serialization/parsable'
require_relative 'microsoft_kiota_abstractions/serialization/iso_duration'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# frozen_string_literal: true

require 'securerandom'
require_relative 'serialization/parsable'

module MicrosoftKiotaAbstractions
class MultipartBody
include Parsable

Part = Struct.new(:name, :content_type, :value, :filename)
private_constant :Part

attr_reader :boundary
attr_accessor :request_adapter

def initialize
@boundary = SecureRandom.uuid
@parts = {}
end

def add_or_replace_part(part_name, content_type, part_value, filename = nil)
raise ArgumentError, 'Part name cannot be null' if blank?(part_name)
raise ArgumentError, 'Content type cannot be null' if blank?(content_type)
raise ArgumentError, 'Content type cannot contain line breaks' if content_type.to_s.match?(/[\r\n]/)
raise ArgumentError, 'Part value cannot be null' if part_value.nil?

@parts[normalize(part_name)] = Part.new(part_name, content_type, part_value, filename)
end

def get_part_value(part_name)
raise ArgumentError, 'Part name cannot be null' if blank?(part_name)

@parts[normalize(part_name)]&.value
end

def remove_part(part_name) # rubocop:disable Naming/PredicateMethod
raise ArgumentError, 'Part name cannot be null' if blank?(part_name)

!@parts.delete(normalize(part_name)).nil?
end

def serialize(writer)
raise ArgumentError, 'Serialization writer cannot be null' if writer.nil?
raise ArgumentError, 'Request adapter or serialization writer factory cannot be null' if request_adapter&.get_serialization_writer_factory.nil?
raise ArgumentError, 'No parts to serialize' if @parts.empty?

@parts.each_value.with_index do |part, index|
add_new_line(writer) unless index.zero?
writer.write_string_value('', "--#{boundary}")
writer.write_string_value('Content-Type', part.content_type)
writer.write_string_value('Content-Disposition', content_disposition(part))
add_new_line(writer)
write_part_value(writer, part)
end
add_new_line(writer)
writer.write_string_value('', "--#{boundary}--")
end

private

def blank?(value)
value.nil? || (value.respond_to?(:empty?) && value.empty?)
end

def normalize(part_name)
part_name.downcase
end

def add_new_line(writer)
writer.write_string_value('', '')
end

def content_disposition(part)
disposition = %(form-data; name="#{escape(part.name)}")
disposition += %(; filename="#{escape(part.filename)}") unless part.filename.nil?
disposition
end

# as browsers escape names and file names in multipart/form-data
def escape(value)
value.to_s.gsub(/["\r\n]/, '"' => '%22', "\r" => '%0D', "\n" => '%0A')
end

def write_part_value(writer, part)
value = part.value
if value.is_a?(Parsable)
writer.write_byte_array_value('', serialize_parsable(part))
elsif value.is_a?(String)
writer.write_byte_array_value('', value)
elsif value.respond_to?(:read)
rewind(value)
writer.write_byte_array_value('', value.read)
Comment thread
baywet marked this conversation as resolved.
else
raise ArgumentError, "Unsupported type #{value.class} for part #{part.name}"
end
end

# read from the start each time the body is written, as .NET does for a seekable stream
def rewind(stream)
stream.rewind if stream.respond_to?(:rewind)
rescue SystemCallError
nil
end

def serialize_parsable(part)
part_writer = request_adapter.get_serialization_writer_factory.get_serialization_writer(part.content_type)
part_writer.write_object_value(nil, part.value)
content = part_writer.get_serialized_content
Comment thread
baywet marked this conversation as resolved.
return content unless content.respond_to?(:read)

content.rewind if content.respond_to?(:rewind)
content.read
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require 'uri'
require 'stduritemplate'
require_relative 'http_method'
require_relative 'multipart_body'
require_relative 'request_headers'

module MicrosoftKiotaAbstractions
Expand Down Expand Up @@ -114,6 +115,11 @@ def set_content_from_scalar(request_adapter, content_type, values)

def set_content_from_parsable(request_adapter, content_type, values)
writer = request_adapter.get_serialization_writer_factory.get_serialization_writer(content_type)
if values.is_a?(MultipartBody)
Comment thread
Copilot marked this conversation as resolved.
content_type = "#{content_type.gsub(/;\s*boundary=[^;]*/i, '')}; boundary=#{values.boundary}"
values.request_adapter = request_adapter
@headers.remove(@@content_type_header)
end
@headers.try_add(@@content_type_header, content_type)
if !values.nil? && values.is_a?(Array)
writer.write_collection_of_object_values(nil, values)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ def get_duration_value
raise NotImplementedError
end

def get_byte_array_value
raise NotImplementedError
end

def get_collection_of_primitive_values
raise NotImplementedError
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ def write_duration_value(_key, _value)
raise NotImplementedError
end

def write_byte_array_value(_key, _value)
raise NotImplementedError
end

def write_collection_of_primitive_values(_key, _value)
raise NotImplementedError
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ def initialize
end

def before(value)
# an exception already being handled around the write is not a failure of the write
@outer_error = $ERROR_INFO if @depth.zero?
if @depth.zero?
# an exception already being handled around the write is not a failure of the write
@outer_error = $ERROR_INFO
open_documents.push(self)
end
@depth += 1
store = store_of(value)
return unless store
Expand All @@ -61,19 +64,32 @@ def start(value, writer)
# runs even when writing the model raised: the store reads every value again, and keeps its changes
def after(value)
@depth -= 1
open_documents.pop if @depth.zero? && open_documents.last.equal?(self)
@failed ||= !$ERROR_INFO.nil? && !$ERROR_INFO.equal?(@outer_error)
store = store_of(value)
store.return_only_changed_values = @filtering.delete(store) if store && @filtering.key?(store)
end

# a document produced while another is written, such as a multipart part, leaves the models to that one
def finish
@written.each_key { |store| store.initialization_completed = true } unless @failed
unless @failed
enclosing = open_documents.last
enclosing ? enclosing.adopt(@written) : @written.each_key { |store| store.initialization_completed = true }
end
@written.clear
@failed = false
end

def adopt(written)
@written.merge!(written)
end

private

def open_documents
Thread.current[:microsoft_kiota_open_documents] ||= []
end

def store_of(value)
value.backing_store if value.is_a?(BackedModel)
end
Expand Down
Loading
Loading