From 6e0a18b4b547d2f12a936a2b9a8d1f8727c8133a Mon Sep 17 00:00:00 2001 From: andreatp Date: Mon, 12 Oct 2026 11:31:42 +0100 Subject: [PATCH 1/6] feat: add text serialization --- components/serialization/text/Gemfile | 11 + components/serialization/text/Rakefile | 8 + .../lib/microsoft_kiota_serialization_text.rb | 10 + .../encoding.rb | 8 + .../text_parse_node.rb | 109 +++++++++ .../text_parse_node_factory.rb | 22 ++ .../text_serialization_writer.rb | 126 +++++++++++ .../text_serialization_writer_factory.rb | 20 ++ .../version.rb | 5 + ...microsoft_kiota_serialization_text.gemspec | 32 +++ .../serialization/text/spec/files/files.rb | 8 + .../text/spec/files/office_location.rb | 8 + .../serialization/text/spec/files/user.rb | 11 + .../serialization/text/spec/spec_helper.rb | 17 ++ .../text/spec/text_parse_node_factory_spec.rb | 51 +++++ .../text/spec/text_parse_node_spec.rb | 149 +++++++++++++ .../text_serialization_writer_factory_spec.rb | 29 +++ .../spec/text_serialization_writer_spec.rb | 209 ++++++++++++++++++ 18 files changed, 833 insertions(+) create mode 100644 components/serialization/text/Gemfile create mode 100644 components/serialization/text/Rakefile create mode 100644 components/serialization/text/lib/microsoft_kiota_serialization_text.rb create mode 100644 components/serialization/text/lib/microsoft_kiota_serialization_text/encoding.rb create mode 100644 components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node.rb create mode 100644 components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node_factory.rb create mode 100644 components/serialization/text/lib/microsoft_kiota_serialization_text/text_serialization_writer.rb create mode 100644 components/serialization/text/lib/microsoft_kiota_serialization_text/text_serialization_writer_factory.rb create mode 100644 components/serialization/text/lib/microsoft_kiota_serialization_text/version.rb create mode 100644 components/serialization/text/microsoft_kiota_serialization_text.gemspec create mode 100644 components/serialization/text/spec/files/files.rb create mode 100644 components/serialization/text/spec/files/office_location.rb create mode 100644 components/serialization/text/spec/files/user.rb create mode 100644 components/serialization/text/spec/spec_helper.rb create mode 100644 components/serialization/text/spec/text_parse_node_factory_spec.rb create mode 100644 components/serialization/text/spec/text_parse_node_spec.rb create mode 100644 components/serialization/text/spec/text_serialization_writer_factory_spec.rb create mode 100644 components/serialization/text/spec/text_serialization_writer_spec.rb diff --git a/components/serialization/text/Gemfile b/components/serialization/text/Gemfile new file mode 100644 index 0000000..3dea809 --- /dev/null +++ b/components/serialization/text/Gemfile @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +ruby '>= 3.3.0' + +gem 'microsoft_kiota_abstractions', path: '../../abstractions' +gem 'rake', '~> 13.0' +gem 'rspec', '~> 3.0' + +gemspec diff --git a/components/serialization/text/Rakefile b/components/serialization/text/Rakefile new file mode 100644 index 0000000..82bb534 --- /dev/null +++ b/components/serialization/text/Rakefile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' + +RSpec::Core::RakeTask.new(:spec) + +task default: :spec diff --git a/components/serialization/text/lib/microsoft_kiota_serialization_text.rb b/components/serialization/text/lib/microsoft_kiota_serialization_text.rb new file mode 100644 index 0000000..eee1fc8 --- /dev/null +++ b/components/serialization/text/lib/microsoft_kiota_serialization_text.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative 'microsoft_kiota_serialization_text/text_parse_node' +require_relative 'microsoft_kiota_serialization_text/text_parse_node_factory' +require_relative 'microsoft_kiota_serialization_text/text_serialization_writer' +require_relative 'microsoft_kiota_serialization_text/text_serialization_writer_factory' +require_relative 'microsoft_kiota_serialization_text/version' + +module MicrosoftKiotaSerializationText +end diff --git a/components/serialization/text/lib/microsoft_kiota_serialization_text/encoding.rb b/components/serialization/text/lib/microsoft_kiota_serialization_text/encoding.rb new file mode 100644 index 0000000..438724c --- /dev/null +++ b/components/serialization/text/lib/microsoft_kiota_serialization_text/encoding.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module MicrosoftKiotaSerializationText + # bytes are read as UTF-8; a string Faraday tagged with its charset is converted + def self.to_utf8(text) + text.encoding == Encoding::BINARY ? text.dup.force_encoding(Encoding::UTF_8) : text.encode(Encoding::UTF_8) + end +end diff --git a/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node.rb b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node.rb new file mode 100644 index 0000000..e152f13 --- /dev/null +++ b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'time' +require 'date' +require 'uuidtools' +require 'microsoft_kiota_abstractions' + +module MicrosoftKiotaSerializationText + class TextParseNode + include MicrosoftKiotaAbstractions::ParseNode + + NO_STRUCTURED_DATA_MESSAGE = 'Text does not support structured data' + + def initialize(text) + @text = text + end + + def get_string_value + @text unless @text.nil? || @text.empty? + end + + def get_boolean_value + case scalar_text&.downcase + when 'true' then true + when 'false' then false + end + end + + def get_number_value + text = scalar_text + Integer(text, 10, exception: false) if text + end + + def get_float_value + text = scalar_text + Float(text, exception: false) if text + end + + def get_guid_value + text = scalar_text + UUIDTools::UUID.parse(text) if text + end + + def get_date_value + text = scalar_text + Date.parse(text) if text + end + + def get_time_value + text = scalar_text + Time.parse(text) if text + end + + def get_date_time_value + text = scalar_text + DateTime.parse(text) if text + end + + def get_duration_value + text = scalar_text + MicrosoftKiotaAbstractions::ISODuration.new(text) if text + end + + def get_byte_array_value + scalar_text&.unpack1('m') + end + + def get_enum_value(type) + text = scalar_text + resolve_enum_member(type, text) if text + end + + def get_child_node(_name) + raise StandardError, NO_STRUCTURED_DATA_MESSAGE + end + + def get_collection_of_primitive_values(_type) + raise StandardError, NO_STRUCTURED_DATA_MESSAGE + end + + def get_collection_of_object_values(_factory) + raise StandardError, NO_STRUCTURED_DATA_MESSAGE + end + + def get_collection_of_enum_values(_type) + raise StandardError, NO_STRUCTURED_DATA_MESSAGE + end + + def get_object_value(_factory) + raise StandardError, NO_STRUCTURED_DATA_MESSAGE + end + + private + + # typed values ignore surrounding whitespace, as .NET's TryParse does + def scalar_text + text = get_string_value&.strip + text unless text.nil? || text.empty? + end + + # same resolution as JsonParseNode: the wire value first, then the member name + def resolve_enum_member(type, raw) + return raw.to_sym unless type.is_a?(Hash) + + type.each_value { |value| return value if value.to_s == raw } + type[raw.to_sym] || type[(raw[0].to_s.upcase + raw[1..].to_s).to_sym] + end + end +end diff --git a/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node_factory.rb b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node_factory.rb new file mode 100644 index 0000000..a853e0a --- /dev/null +++ b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node_factory.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'microsoft_kiota_abstractions' +require_relative 'encoding' + +module MicrosoftKiotaSerializationText + class TextParseNodeFactory + include MicrosoftKiotaAbstractions::ParseNodeFactory + + def get_valid_content_type + 'text/plain' + end + + def get_parse_node(content_type, content) + raise ArgumentError, 'content type cannot be undefined or empty' if content_type.nil? || content_type.empty? + raise ArgumentError, "expected a #{get_valid_content_type} content type" unless get_valid_content_type.casecmp?(content_type) + raise ArgumentError, 'content cannot be undefined or empty' if content.nil? || content.empty? + + TextParseNode.new(MicrosoftKiotaSerializationText.to_utf8(content).delete_prefix("\uFEFF")) + end + end +end diff --git a/components/serialization/text/lib/microsoft_kiota_serialization_text/text_serialization_writer.rb b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_serialization_writer.rb new file mode 100644 index 0000000..a8b5e75 --- /dev/null +++ b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_serialization_writer.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'time' +require 'date' +require 'uuidtools' +require 'microsoft_kiota_abstractions' +require_relative 'encoding' +require_relative 'text_parse_node' + +module MicrosoftKiotaSerializationText + class TextSerializationWriter + include MicrosoftKiotaAbstractions::SerializationWriter + + NO_STRUCTURED_DATA_MESSAGE = TextParseNode::NO_STRUCTURED_DATA_MESSAGE + VALUE_ALREADY_WRITTEN_MESSAGE = + 'a value was already written for this serialization writer, text content only supports a single value' + + def initialize + @content = nil + end + + def write_string_value(key, value) + return if value.nil? + raise StandardError, NO_STRUCTURED_DATA_MESSAGE unless key.nil? || key.empty? + return if value.empty? + raise StandardError, VALUE_ALREADY_WRITTEN_MESSAGE unless @content.nil? + + @content = value + end + + def write_boolean_value(key, value) + write_string_value(key, value&.to_s) + end + + def write_number_value(key, value) + write_string_value(key, value&.to_s) + end + + def write_float_value(key, value) + write_string_value(key, value&.to_s) + end + + def write_guid_value(key, value) + write_string_value(key, value&.to_s) + end + + def write_date_value(key, value) + write_string_value(key, value&.strftime('%Y-%m-%d')) + end + + def write_time_value(key, value) + return if value.nil? + + digits = fraction_digits(value) + write_string_value(key, value.strftime(digits.zero? ? '%H:%M:%S' : "%H:%M:%S.%#{digits}N")) + end + + def write_date_time_value(key, value) + write_string_value(key, value&.iso8601(fraction_digits(value))) + end + + def write_duration_value(key, value) + write_string_value(key, value&.string) + end + + def write_enum_value(key, value) + write_string_value(key, value&.to_s) + end + + def write_byte_array_value(key, value) + write_string_value(key, value && [value].pack('m0')) + end + + def write_null_value(key) + write_string_value(key, 'null') + end + + def write_any_value(key, value) + case value + when nil then nil + when String then write_string_value(key, value) + when true, false then write_boolean_value(key, value) + when Integer then write_number_value(key, value) + when Float then write_float_value(key, value) + when DateTime then write_date_time_value(key, value) + when Date then write_date_value(key, value) + when Time then write_time_value(key, value) + when MicrosoftKiotaAbstractions::ISODuration then write_duration_value(key, value) + when UUIDTools::UUID then write_guid_value(key, value) + when Symbol then write_enum_value(key, value) + when Array, Hash, MicrosoftKiotaAbstractions::Parsable then raise StandardError, NO_STRUCTURED_DATA_MESSAGE + else raise StandardError, "encountered unknown value type during serialization #{value}" + end + end + + def write_collection_of_primitive_values(_key, _values) + raise StandardError, NO_STRUCTURED_DATA_MESSAGE + end + + def write_collection_of_object_values(_key, _values) + raise StandardError, NO_STRUCTURED_DATA_MESSAGE + end + + def write_collection_of_enum_values(_key, _values) + raise StandardError, NO_STRUCTURED_DATA_MESSAGE + end + + def write_object_value(_key, _value, *_additional_values_to_merge) + raise StandardError, NO_STRUCTURED_DATA_MESSAGE + end + + def write_additional_data(_value) + raise StandardError, NO_STRUCTURED_DATA_MESSAGE + end + + def get_serialized_content + @content.nil? ? '' : MicrosoftKiotaSerializationText.to_utf8(@content) + end + + private + + def fraction_digits(value) + value.strftime('%N').sub(/0+\z/, '').length + end + end +end diff --git a/components/serialization/text/lib/microsoft_kiota_serialization_text/text_serialization_writer_factory.rb b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_serialization_writer_factory.rb new file mode 100644 index 0000000..25bb690 --- /dev/null +++ b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_serialization_writer_factory.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'microsoft_kiota_abstractions' + +module MicrosoftKiotaSerializationText + class TextSerializationWriterFactory + include MicrosoftKiotaAbstractions::SerializationWriterFactory + + def get_valid_content_type + 'text/plain' + end + + def get_serialization_writer(content_type) + raise ArgumentError, 'content type cannot be undefined or empty' if content_type.nil? || content_type.empty? + raise ArgumentError, "expected a #{get_valid_content_type} content type" unless get_valid_content_type.casecmp?(content_type) + + TextSerializationWriter.new + end + end +end diff --git a/components/serialization/text/lib/microsoft_kiota_serialization_text/version.rb b/components/serialization/text/lib/microsoft_kiota_serialization_text/version.rb new file mode 100644 index 0000000..e3badb9 --- /dev/null +++ b/components/serialization/text/lib/microsoft_kiota_serialization_text/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module MicrosoftKiotaSerializationText + VERSION = '0.24.1' # x-release-please-version +end diff --git a/components/serialization/text/microsoft_kiota_serialization_text.gemspec b/components/serialization/text/microsoft_kiota_serialization_text.gemspec new file mode 100644 index 0000000..9c0f43e --- /dev/null +++ b/components/serialization/text/microsoft_kiota_serialization_text.gemspec @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative 'lib/microsoft_kiota_serialization_text/version' + +Gem::Specification.new do |spec| + spec.name = 'microsoft_kiota_serialization_text' + spec.version = MicrosoftKiotaSerializationText::VERSION + spec.authors = 'Microsoft Corporation' + spec.email = 'graphsdkpub+ruby@microsoft.com' + spec.description = 'Implementation of Kiota Serialization interfaces for text' + spec.summary = 'Microsoft Kiota Serialization - Ruby serialization for building library agnostic http client' + spec.homepage = 'https://learn.microsoft.com/openapi/kiota/' + spec.license = 'MIT' + spec.metadata = { + 'bug_tracker_uri' => 'https://github.com/microsoft/kiota-ruby/issues', + 'changelog_uri' => 'https://github.com/microsoft/kiota-ruby/blob/main/CHANGELOG.md', + 'homepage_uri' => spec.homepage, + 'source_code_uri' => 'https://github.com/microsoft/kiota-ruby/tree/main/components/serialization/text', + 'github_repo' => 'ssh://github.com/microsoft/kiota-ruby', + 'rubygems_mfa_required' => 'true' + } + spec.required_ruby_version = '>= 3.3.0' + + spec.files = Dir.chdir(File.expand_path(__dir__)) do + `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } + end + spec.bindir = 'bin' + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ['lib'] + spec.add_dependency 'microsoft_kiota_abstractions', MicrosoftKiotaSerializationText::VERSION + spec.add_dependency 'uuidtools', '>= 2.2', '< 3.1' +end diff --git a/components/serialization/text/spec/files/files.rb b/components/serialization/text/spec/files/files.rb new file mode 100644 index 0000000..d6fe8c8 --- /dev/null +++ b/components/serialization/text/spec/files/files.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'microsoft_kiota_abstractions' +require_relative 'office_location' +require_relative 'user' + +module Files +end diff --git a/components/serialization/text/spec/files/office_location.rb b/components/serialization/text/spec/files/office_location.rb new file mode 100644 index 0000000..035006d --- /dev/null +++ b/components/serialization/text/spec/files/office_location.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Files + OFFICE_LOCATION = { + Dunhill: 'dunhill', + Oval: 'oval' + }.freeze +end diff --git a/components/serialization/text/spec/files/user.rb b/components/serialization/text/spec/files/user.rb new file mode 100644 index 0000000..3152b73 --- /dev/null +++ b/components/serialization/text/spec/files/user.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'microsoft_kiota_abstractions' + +module Files + class User + include MicrosoftKiotaAbstractions::Parsable + + def self.create_from_discriminator_value(_parse_node) = User.new + end +end diff --git a/components/serialization/text/spec/spec_helper.rb b/components/serialization/text/spec/spec_helper.rb new file mode 100644 index 0000000..5e1b016 --- /dev/null +++ b/components/serialization/text/spec/spec_helper.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'microsoft_kiota_abstractions' + +Dir["#{File.dirname(__FILE__)}/./../lib/*.rb"].each { |f| load(f) } + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = '.rspec_status' + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end diff --git a/components/serialization/text/spec/text_parse_node_factory_spec.rb b/components/serialization/text/spec/text_parse_node_factory_spec.rb new file mode 100644 index 0000000..ac11af0 --- /dev/null +++ b/components/serialization/text/spec/text_parse_node_factory_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' + +RSpec.describe MicrosoftKiotaSerializationText::TextParseNodeFactory do + subject(:factory) { described_class.new } + + let(:sample_text_string) { 'Microsoft Graph'.encode(Encoding::UTF_8).b } + + it 'gets a root parse node' do + expect(factory.get_parse_node('text/plain', sample_text_string)).to be_a(MicrosoftKiotaSerializationText::TextParseNode) + end + + it 'raises without a content type' do + expect { factory.get_parse_node('', sample_text_string) }.to raise_error(ArgumentError, 'content type cannot be undefined or empty') + end + + it 'raises for an unsupported content type' do + expect { factory.get_parse_node('application/xml', sample_text_string) }.to raise_error( + ArgumentError, 'expected a text/plain content type' + ) + end + + it 'raises for empty content' do + expect { factory.get_parse_node('text/plain', ''.b) }.to raise_error(ArgumentError, 'content cannot be undefined or empty') + end + + it 'matches the content type ignoring case' do + expect(factory.get_parse_node('Text/Plain', sample_text_string).get_string_value).to eq('Microsoft Graph') + end + + it 'reads a body still in bytes as UTF-8' do + node = factory.get_parse_node('text/plain', 'café'.b) + expect(node.get_string_value).to eq('café') + end + + it 'converts a body tagged with another charset, as Faraday tags it' do + node = factory.get_parse_node('text/plain', 'café'.encode(Encoding::ISO_8859_1)) + expect(node.get_string_value).to eq('café') + expect(node.get_string_value).to be_valid_encoding + end + + it 'drops a byte order mark' do + node = factory.get_parse_node('text/plain', "\uFEFF42".b) + expect([node.get_string_value, node.get_number_value]).to eq(['42', 42]) + end + + it 'gets the valid content type' do + expect(factory.get_valid_content_type).to eq('text/plain') + end +end diff --git a/components/serialization/text/spec/text_parse_node_spec.rb b/components/serialization/text/spec/text_parse_node_spec.rb new file mode 100644 index 0000000..4712d9b --- /dev/null +++ b/components/serialization/text/spec/text_parse_node_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' +require_relative 'files/files' + +RSpec.describe MicrosoftKiotaSerializationText::TextParseNode do + let(:structured_data_error) { [StandardError, 'Text does not support structured data'] } + let(:user_factory) { ->(pn) { Files::User.create_from_discriminator_value(pn) } } + let(:sample_structured_data) do + '{"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity", ' \ + '"businessPhones": ["+1 425 555 0109"], "displayName": "Adele Vance", "mobilePhone": null, ' \ + '"officeLocation": "dunhill", "updatedAt": "2017 -07-29T03:07:25Z", "birthday": "2000-09-04", ' \ + '"isActive": true, "age": 21, "gpa": 3.7, "id": "76cabd60-f9aa-4d23-8958-64f5539b826a"}' + end + + it 'gets a string value' do + expect(described_class.new('Diego Siciliani').get_string_value).to eq('Diego Siciliani') + end + + it 'gets a number value' do + expect(described_class.new('1454').get_number_value).to eq(1454) + end + + it 'gets a boolean value' do + expect(described_class.new('false').get_boolean_value).to be(false) + end + + it 'gets a float value' do + expect(described_class.new('44.6').get_float_value).to eq(44.6) + end + + it 'gets a guid value' do + result = described_class.new('f58411c7-ae78-4d3c-bb0d-3f24d948de41').get_guid_value + expect(result).to eq(UUIDTools::UUID.parse('f58411c7-ae78-4d3c-bb0d-3f24d948de41')) + end + + it 'gets a date time value' do + expect(described_class.new('2022-01-27T12:59:45.596117').get_date_time_value).to be_a(DateTime) + end + + it 'gets a date value' do + result = described_class.new('2015-04-20').get_date_value + expect(result).to be_a(Date) + expect(result.to_s).to eq('2015-04-20') + end + + it 'gets a time value' do + result = described_class.new('12:59:45.596117').get_time_value + expect(result).to be_a(Time) + expect(result.strftime('%H:%M:%S.%6N')).to eq('12:59:45.596117') + end + + it 'gets a duration value' do + result = described_class.new('PT12H59M45S').get_duration_value + expect(result).to be_a(MicrosoftKiotaAbstractions::ISODuration) + expect([result.hours, result.minutes, result.seconds]).to eq([12, 59, 45]) + end + + it 'raises when getting a collection of primitive values' do + parse_node = described_class.new('[12.1, 12.2, 12.3, 12.4, 12.5]') + expect { parse_node.get_collection_of_primitive_values(Float) }.to raise_error(*structured_data_error) + end + + it 'gets a byte array value' do + result = described_class.new('U2Ftd2VsIGlzIHRoZSBiZXN0').get_byte_array_value + expect(result).to be_a(String) + expect(result).to eq('Samwel is the best') + end + + it 'raises when getting a collection of enum values' do + parse_node = described_class.new('dunhill,oval') + expect { parse_node.get_collection_of_enum_values(Files::OFFICE_LOCATION) }.to raise_error(*structured_data_error) + end + + it 'gets an enum value' do + expect(described_class.new('dunhill').get_enum_value(Files::OFFICE_LOCATION)).to eq(Files::OFFICE_LOCATION[:Dunhill]) + end + + it 'raises when getting an object value' do + parse_node = described_class.new(sample_structured_data) + expect { parse_node.get_object_value(user_factory) }.to raise_error(*structured_data_error) + end + + it 'raises when getting a collection of object values' do + parse_node = described_class.new(sample_structured_data) + expect { parse_node.get_collection_of_object_values(user_factory) }.to raise_error(*structured_data_error) + end + + it 'raises when getting a child node' do + expect { described_class.new(sample_structured_data).get_child_node('displayName') }.to raise_error(*structured_data_error) + end + + describe 'edge cases' do + it 'reads false as false rather than as a non-empty string' do + expect(described_class.new('FALSE').get_boolean_value).to be(false) + expect(described_class.new('True').get_boolean_value).to be(true) + expect(described_class.new('yes').get_boolean_value).to be_nil + end + + it 'reads an enum member by its wire value' do + access = { IncomingPayment: 'incoming-payment', ReadAll: 'read-all' }.freeze + expect(described_class.new('incoming-payment').get_enum_value(access)).to eq(access[:IncomingPayment]) + end + + it 'returns nil for an enum member the enum does not declare' do + expect(described_class.new('purple').get_enum_value(Files::OFFICE_LOCATION)).to be_nil + end + + it 'returns nil from every reader for empty or missing text' do + ['', nil].each do |text| + parse_node = described_class.new(text) + readers = %i[get_string_value get_boolean_value get_number_value get_float_value get_guid_value get_date_value + get_time_value get_date_time_value get_duration_value get_byte_array_value] + readers.each { |reader| expect(parse_node.public_send(reader)).to be_nil, "#{reader} on #{text.inspect}" } + expect(parse_node.get_enum_value(Files::OFFICE_LOCATION)).to be_nil + end + end + + it 'ignores surrounding whitespace in typed values but not in strings' do + expect(described_class.new("true\n").get_boolean_value).to be(true) + expect(described_class.new(" 42\n").get_number_value).to eq(42) + expect(described_class.new("f58411c7-ae78-4d3c-bb0d-3f24d948de41\r\n").get_guid_value) + .to eq(UUIDTools::UUID.parse('f58411c7-ae78-4d3c-bb0d-3f24d948de41')) + expect(described_class.new("dunhill\n").get_enum_value(Files::OFFICE_LOCATION)).to eq(Files::OFFICE_LOCATION[:Dunhill]) + expect(described_class.new("hi\n").get_string_value).to eq("hi\n") + end + + it 'returns nil from typed readers for blank text' do + parse_node = described_class.new(" \n") + readers = %i[get_boolean_value get_number_value get_float_value get_guid_value get_date_value get_time_value + get_date_time_value get_duration_value get_byte_array_value] + readers.each { |reader| expect(parse_node.public_send(reader)).to be_nil, reader.to_s } + end + + it 'reads a date time with a space separator, like the JSON gem' do + expect(described_class.new('2024-01-02 03:04:05Z').get_date_time_value).to eq(DateTime.new(2024, 1, 2, 3, 4, 5)) + end + + it 'reads base64 split across lines, as Python and .NET do' do + expect(described_class.new("U2Ftd2Vs\nIGlzIHRoZSBiZXN0").get_byte_array_value).to eq('Samwel is the best') + end + + it 'returns nil for text that is not a number' do + expect(described_class.new('twelve').get_number_value).to be_nil + expect(described_class.new('12.5').get_number_value).to be_nil + expect(described_class.new('twelve').get_float_value).to be_nil + end + end +end diff --git a/components/serialization/text/spec/text_serialization_writer_factory_spec.rb b/components/serialization/text/spec/text_serialization_writer_factory_spec.rb new file mode 100644 index 0000000..a7a5aaf --- /dev/null +++ b/components/serialization/text/spec/text_serialization_writer_factory_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' + +RSpec.describe MicrosoftKiotaSerializationText::TextSerializationWriterFactory do + subject(:factory) { described_class.new } + + it 'gets a serialization writer' do + expect(factory.get_serialization_writer('text/plain')).to be_a(MicrosoftKiotaSerializationText::TextSerializationWriter) + end + + it 'raises without a content type' do + expect { factory.get_serialization_writer('') }.to raise_error(ArgumentError, 'content type cannot be undefined or empty') + end + + it 'raises for an unsupported content type' do + expect { factory.get_serialization_writer('application/xml') }.to raise_error( + ArgumentError, 'expected a text/plain content type' + ) + end + + it 'matches the content type ignoring case' do + expect(factory.get_serialization_writer('TEXT/PLAIN')).to be_a(MicrosoftKiotaSerializationText::TextSerializationWriter) + end + + it 'gets the valid content type' do + expect(factory.get_valid_content_type).to eq('text/plain') + end +end diff --git a/components/serialization/text/spec/text_serialization_writer_spec.rb b/components/serialization/text/spec/text_serialization_writer_spec.rb new file mode 100644 index 0000000..60d39ee --- /dev/null +++ b/components/serialization/text/spec/text_serialization_writer_spec.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' +require_relative 'files/files' + +RSpec.describe MicrosoftKiotaSerializationText::TextSerializationWriter do + subject(:writer) { described_class.new } + + let(:structured_data_error) { [StandardError, 'Text does not support structured data'] } + let(:two_hours) { MicrosoftKiotaAbstractions::ISODuration.new(hours: 2) } + let(:user1) { Files::User.new } + let(:user2) { Files::User.new } + + it 'writes a string value' do + writer.write_string_value('', 'Adele Vance') + expect(writer.get_serialized_content).to eq('Adele Vance') + end + + it 'raises when writing a string value with a key' do + expect { writer.write_string_value('displayName', 'Adele Vance') }.to raise_error(*structured_data_error) + end + + it 'writes a boolean value' do + writer.write_boolean_value(nil, true) + expect(writer.get_serialized_content).to eq('true') + end + + it 'raises when writing a boolean value with a key' do + expect { writer.write_boolean_value('isActive', true) }.to raise_error(*structured_data_error) + end + + it 'writes a number value' do + writer.write_number_value('', 28_192_199_291_929_192) + expect(writer.get_serialized_content).to eq('28192199291929192') + end + + it 'raises when writing a number value with a key' do + expect { writer.write_number_value('timestamp', 28_192_199_291_929_192) }.to raise_error(*structured_data_error) + end + + it 'writes a float value' do + writer.write_float_value('', 3.2) + expect(writer.get_serialized_content).to eq('3.2') + end + + it 'raises when writing a float value with a key' do + expect { writer.write_float_value('gpa', 3.2) }.to raise_error(*structured_data_error) + end + + it 'writes a guid value' do + writer.write_guid_value('', UUIDTools::UUID.parse('8f841f30-e6e3-439a-a812-ebd369559c36')) + expect(writer.get_serialized_content).to eq('8f841f30-e6e3-439a-a812-ebd369559c36') + end + + it 'raises when writing a guid value with a key' do + uuid = UUIDTools::UUID.parse('8f841f30-e6e3-439a-a812-ebd369559c36') + expect { writer.write_guid_value('id', uuid) }.to raise_error(*structured_data_error) + end + + it 'writes a date time value' do + writer.write_date_time_value('', DateTime.iso8601('2022-01-27T12:59:45.596117')) + expect(writer.get_serialized_content).to eq('2022-01-27T12:59:45.596117+00:00') + end + + it 'raises when writing a date time value with a key' do + date_time = DateTime.iso8601('2022-01-27T12:59:45.596117') + expect { writer.write_date_time_value('updatedAt', date_time) }.to raise_error(*structured_data_error) + end + + it 'writes a duration value' do + writer.write_duration_value('', two_hours) + expect(writer.get_serialized_content).to eq('PT2H') + end + + it 'raises when writing a duration value with a key' do + expect { writer.write_duration_value('diff', two_hours) }.to raise_error(*structured_data_error) + end + + it 'writes a date value' do + writer.write_date_value('', Date.iso8601('2000-09-04')) + expect(writer.get_serialized_content).to eq('2000-09-04') + end + + it 'raises when writing a date value with a key' do + expect { writer.write_date_value('birthday', Date.iso8601('2000-09-04')) }.to raise_error(*structured_data_error) + end + + it 'writes a time value' do + writer.write_time_value('', Time.parse('2022-01-27T12:59:45.596117')) + expect(writer.get_serialized_content).to eq('12:59:45.596117') + end + + it 'raises when writing a time value with a key' do + time = Time.parse('2022-01-27T12:59:45.596117') + expect { writer.write_time_value('time', time) }.to raise_error(*structured_data_error) + end + + it 'raises when writing a collection of primitive values' do + values = ['+1 412 555 0109', 1] + expect { writer.write_collection_of_primitive_values('businessPhones', values) }.to raise_error(*structured_data_error) + end + + it 'raises when writing a collection of object values' do + expect { writer.write_collection_of_object_values('users', [user1, user2]) }.to raise_error(*structured_data_error) + end + + it 'raises when writing a collection of enum values' do + values = Files::OFFICE_LOCATION.values_at(:Dunhill, :Oval) + expect { writer.write_collection_of_enum_values('officeLocation', values) }.to raise_error(*structured_data_error) + end + + it 'raises when writing an object value' do + expect { writer.write_object_value('user1', user1) }.to raise_error(*structured_data_error) + end + + it 'writes an enum value' do + writer.write_enum_value('', Files::OFFICE_LOCATION[:Dunhill]) + expect(writer.get_serialized_content).to eq('dunhill') + end + + it 'raises when writing an enum value with a key' do + expect { writer.write_enum_value('officeLocation', Files::OFFICE_LOCATION[:Dunhill]) }.to raise_error(*structured_data_error) + end + + it 'writes a null value' do + writer.write_null_value('') + expect(writer.get_serialized_content).to eq('null') + end + + it 'raises when writing a null value with a key' do + expect { writer.write_null_value('mobilePhone') }.to raise_error(*structured_data_error) + end + + it 'raises when writing additional data' do + additional_data = { + '@odata.context' => 'https://graph.microsoft.com/v1.0/$metadata#users/$entity', + 'businessPhones' => ['+1 205 555 0108'] + } + expect { writer.write_additional_data(additional_data) }.to raise_error(*structured_data_error) + end + + it 'writes a byte array value as base64' do + writer.write_byte_array_value(nil, 'Samwel is the best') + expect(writer.get_serialized_content).to eq('U2Ftd2VsIGlzIHRoZSBiZXN0') + end + + it 'raises when a second value is written' do + writer.write_string_value(nil, 'first') + expect { writer.write_string_value(nil, 'second') }.to raise_error( + StandardError, + 'a value was already written for this serialization writer, text content only supports a single value' + ) + end + + it 'writes an empty body when no content was written, as .NET does' do + writer.write_string_value(nil, '') + expect(writer.get_serialized_content).to eq('') + end + + it 'writes a string held in bytes as UTF-8' do + writer.write_string_value(nil, 'café'.b) + expect(writer.get_serialized_content).to eq('café') + end + + describe 'edge cases' do + it 'writes a zero number' do + writer.write_number_value(nil, 0) + expect(writer.get_serialized_content).to eq('0') + end + + it 'writes a zero float' do + writer.write_float_value(nil, 0.0) + expect(writer.get_serialized_content).to eq('0.0') + end + + it 'writes a duration as ISO 8601 that reads back to the same duration' do + writer.write_duration_value(nil, two_hours) + content = writer.get_serialized_content + expect(content).to eq('PT2H') + expect(MicrosoftKiotaSerializationText::TextParseNode.new(content).get_duration_value).to eq(two_hours) + end + end +end + +RSpec.describe MicrosoftKiotaSerializationText::TextSerializationWriter, '#write_any_value' do + subject(:writer) { described_class.new } + + let(:structured_data_error) { [StandardError, 'Text does not support structured data'] } + let(:two_hours) { MicrosoftKiotaAbstractions::ISODuration.new(hours: 2) } + + it 'writes any scalar value' do + uuid = UUIDTools::UUID.parse('8f841f30-e6e3-439a-a812-ebd369559c36') + { + 'hi' => 'hi', 42 => '42', 1.5 => '1.5', false => 'false', uuid => uuid.to_s, two_hours => 'PT2H', + Date.new(2000, 9, 4) => '2000-09-04', DateTime.new(2024, 1, 2, 3, 4, 5) => '2024-01-02T03:04:05+00:00', + Time.utc(2024, 1, 2, 3, 4, 5) => '03:04:05', :dunhill => 'dunhill' + }.each do |value, expected| + text_writer = described_class.new + text_writer.write_any_value(nil, value) + expect(text_writer.get_serialized_content).to eq(expected), value.inspect + end + end + + it 'raises for a structured or unknown value' do + expect { writer.write_any_value(nil, [1, 2]) }.to raise_error(*structured_data_error) + expect { writer.write_any_value(nil, { 'a' => 1 }) }.to raise_error(*structured_data_error) + expect { writer.write_any_value(nil, Rational(1, 3)) }.to raise_error(StandardError, /unknown value type/) + end +end From 7ceaf061eb73a98cc08c645bb334bc46aa764b1e Mon Sep 17 00:00:00 2001 From: andreatp Date: Mon, 12 Oct 2026 11:35:23 +0100 Subject: [PATCH 2/6] ci: add the text serialization gem to the build and release --- .github/dependabot.yml | 7 +++++++ .github/workflows/release.yml | 1 + .github/workflows/ruby.yml | 1 + Gemfile | 1 + README.md | 3 +++ Rakefile | 1 + release-please-config.json | 1 + 7 files changed, 15 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9efde0c..cca8e99 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -32,6 +32,13 @@ updates: open-pull-requests-limit: 10 cooldown: default-days: 7 +- package-ecosystem: bundler + directory: /components/serialization/text + schedule: + interval: daily + open-pull-requests-limit: 10 + cooldown: + default-days: 7 - package-ecosystem: bundler directory: /components/http schedule: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 86fddfa..ff6cf2e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,6 +52,7 @@ jobs: component: - { name: serialization-json, dir: components/serialization/json } - { name: serialization-form, dir: components/serialization/form } + - { name: serialization-text, dir: components/serialization/text } - { name: http, dir: components/http } - { name: authentication-oauth, dir: components/authentication/oauth } steps: diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 394d4c6..8293f14 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -29,6 +29,7 @@ jobs: - { name: abstractions, dir: components/abstractions } - { name: serialization-json, dir: components/serialization/json } - { name: serialization-form, dir: components/serialization/form } + - { name: serialization-text, dir: components/serialization/text } - { name: http, dir: components/http } - { name: authentication-oauth, dir: components/authentication/oauth } runs-on: ${{ matrix.os }} diff --git a/Gemfile b/Gemfile index cf80da9..6066d58 100644 --- a/Gemfile +++ b/Gemfile @@ -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_text', path: 'components/serialization/text' gem 'rake', '~> 13.0' gem 'rspec', '~> 3.0' diff --git a/README.md b/README.md index 332683b..48749fe 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Ruby 3.3 or newer is required. | [microsoft_kiota_abstractions](components/abstractions/) | Core abstractions for Kiota-generated clients | [![Gem Version](https://badge.fury.io/rb/microsoft_kiota_abstractions.svg)](https://rubygems.org/gems/microsoft_kiota_abstractions) | | [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_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) | @@ -26,6 +27,7 @@ Add the gems you need to your application's Gemfile: gem 'microsoft_kiota_abstractions' gem 'microsoft_kiota_serialization_json' gem 'microsoft_kiota_serialization_form' +gem 'microsoft_kiota_serialization_text' gem 'microsoft_kiota_faraday' gem 'microsoft_kiota_authentication_oauth' ``` @@ -48,6 +50,7 @@ To run tests for a specific component: rake abstractions:spec rake serialization-json:spec rake serialization-form:spec +rake serialization-text:spec rake http:spec rake authentication-oauth:spec ``` diff --git a/Rakefile b/Rakefile index 1f81b14..6a286cf 100644 --- a/Rakefile +++ b/Rakefile @@ -4,6 +4,7 @@ COMPONENTS = { 'abstractions' => 'components/abstractions', 'serialization-json' => 'components/serialization/json', 'serialization-form' => 'components/serialization/form', + 'serialization-text' => 'components/serialization/text', 'http' => 'components/http', 'authentication-oauth' => 'components/authentication/oauth' }.freeze diff --git a/release-please-config.json b/release-please-config.json index 193c875..530a344 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -11,6 +11,7 @@ "components/abstractions/lib/microsoft_kiota_abstractions/version.rb", "components/serialization/json/lib/microsoft_kiota_serialization_json/version.rb", "components/serialization/form/lib/microsoft_kiota_serialization_form/version.rb", + "components/serialization/text/lib/microsoft_kiota_serialization_text/version.rb", "components/http/lib/microsoft_kiota_faraday/version.rb", "components/authentication/oauth/lib/microsoft_kiota_authentication_oauth/version.rb" ] From a69f4ccbaca6593f453fb9d7cfcf32817f48bbe4 Mon Sep 17 00:00:00 2001 From: andreatp Date: Tue, 13 Oct 2026 15:43:32 +0100 Subject: [PATCH 3/6] refactor: count an empty string as the single value a text writer holds --- .../text_serialization_writer.rb | 1 - .../text/spec/text_serialization_writer_spec.rb | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/components/serialization/text/lib/microsoft_kiota_serialization_text/text_serialization_writer.rb b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_serialization_writer.rb index a8b5e75..a989851 100644 --- a/components/serialization/text/lib/microsoft_kiota_serialization_text/text_serialization_writer.rb +++ b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_serialization_writer.rb @@ -22,7 +22,6 @@ def initialize def write_string_value(key, value) return if value.nil? raise StandardError, NO_STRUCTURED_DATA_MESSAGE unless key.nil? || key.empty? - return if value.empty? raise StandardError, VALUE_ALREADY_WRITTEN_MESSAGE unless @content.nil? @content = value diff --git a/components/serialization/text/spec/text_serialization_writer_spec.rb b/components/serialization/text/spec/text_serialization_writer_spec.rb index 60d39ee..a2c48ba 100644 --- a/components/serialization/text/spec/text_serialization_writer_spec.rb +++ b/components/serialization/text/spec/text_serialization_writer_spec.rb @@ -157,6 +157,11 @@ expect(writer.get_serialized_content).to eq('') end + it 'raises when a value follows an empty one' do + writer.write_string_value(nil, '') + expect { writer.write_string_value(nil, 'second') }.to raise_error(StandardError, /a value was already written/) + end + it 'writes a string held in bytes as UTF-8' do writer.write_string_value(nil, 'café'.b) expect(writer.get_serialized_content).to eq('café') From a5a7d8a853f3d855eebc9c6bed6a9a6d51294ac8 Mon Sep 17 00:00:00 2001 From: andreatp Date: Wed, 14 Oct 2026 11:46:56 +0100 Subject: [PATCH 4/6] refactor: read unparseable text values as nil --- .../text_parse_node.rb | 23 +++++++++++-------- .../text/spec/text_parse_node_spec.rb | 6 +++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node.rb b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node.rb index e152f13..8658368 100644 --- a/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node.rb +++ b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node.rb @@ -37,28 +37,23 @@ def get_float_value end def get_guid_value - text = scalar_text - UUIDTools::UUID.parse(text) if text + parsed { |text| UUIDTools::UUID.parse(text) } end def get_date_value - text = scalar_text - Date.parse(text) if text + parsed { |text| Date.parse(text) } end def get_time_value - text = scalar_text - Time.parse(text) if text + parsed { |text| Time.parse(text) } end def get_date_time_value - text = scalar_text - DateTime.parse(text) if text + parsed { |text| DateTime.parse(text) } end def get_duration_value - text = scalar_text - MicrosoftKiotaAbstractions::ISODuration.new(text) if text + parsed { |text| MicrosoftKiotaAbstractions::ISODuration.new(text) } end def get_byte_array_value @@ -92,6 +87,14 @@ def get_object_value(_factory) private + # text that does not parse reads as nil, as .NET's TryParse and the form gem do + def parsed + text = scalar_text + yield text if text + rescue StandardError + nil + end + # typed values ignore surrounding whitespace, as .NET's TryParse does def scalar_text text = get_string_value&.strip diff --git a/components/serialization/text/spec/text_parse_node_spec.rb b/components/serialization/text/spec/text_parse_node_spec.rb index 4712d9b..a7e3f07 100644 --- a/components/serialization/text/spec/text_parse_node_spec.rb +++ b/components/serialization/text/spec/text_parse_node_spec.rb @@ -140,6 +140,12 @@ expect(described_class.new("U2Ftd2Vs\nIGlzIHRoZSBiZXN0").get_byte_array_value).to eq('Samwel is the best') end + it 'returns nil for text that is not a guid, date, time, date time or duration, as .NET and the form gem do' do + parse_node = described_class.new('not a value') + readers = %i[get_guid_value get_date_value get_time_value get_date_time_value get_duration_value] + readers.each { |reader| expect(parse_node.public_send(reader)).to be_nil, reader.to_s } + end + it 'returns nil for text that is not a number' do expect(described_class.new('twelve').get_number_value).to be_nil expect(described_class.new('12.5').get_number_value).to be_nil From 738b0084309ebe12982ca5bc5ba2d679d1472483 Mon Sep 17 00:00:00 2001 From: andreatp Date: Wed, 14 Oct 2026 12:29:07 +0100 Subject: [PATCH 5/6] refactor: read a text date, time or date time only in its own shape --- .../text_parse_node.rb | 11 ++++++++--- .../serialization/text/spec/text_parse_node_spec.rb | 9 +++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node.rb b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node.rb index 8658368..dd31bcc 100644 --- a/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node.rb +++ b/components/serialization/text/lib/microsoft_kiota_serialization_text/text_parse_node.rb @@ -11,6 +11,11 @@ class TextParseNode NO_STRUCTURED_DATA_MESSAGE = 'Text does not support structured data' + # temporal readers take only their own shape, as in the form gem, so a union picks the right member + DATE = /\A\d{4}-\d{2}-\d{2}\z/ + TIME = /\A\d{2}:\d{2}(:\d{2}(\.\d+)?)?\z/ + DATE_TIME = /\A\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?([Zz]|[+-]\d{2}(:?\d{2})?)?\z/ + def initialize(text) @text = text end @@ -41,15 +46,15 @@ def get_guid_value end def get_date_value - parsed { |text| Date.parse(text) } + parsed { |text| Date.parse(text) if DATE.match?(text) } end def get_time_value - parsed { |text| Time.parse(text) } + parsed { |text| Time.parse(text) if TIME.match?(text) } end def get_date_time_value - parsed { |text| DateTime.parse(text) } + parsed { |text| DateTime.parse(text) if DATE_TIME.match?(text) } end def get_duration_value diff --git a/components/serialization/text/spec/text_parse_node_spec.rb b/components/serialization/text/spec/text_parse_node_spec.rb index a7e3f07..24f2355 100644 --- a/components/serialization/text/spec/text_parse_node_spec.rb +++ b/components/serialization/text/spec/text_parse_node_spec.rb @@ -132,6 +132,15 @@ readers.each { |reader| expect(parse_node.public_send(reader)).to be_nil, reader.to_s } end + it 'reads a date, time or date time only in its own shape, as the form gem does' do + date, date_time, time = %w[2017-09-04 2017-07-29T03:07:25Z 08:00:00].map { |text| described_class.new(text) } + expect([date.get_date_value, date.get_date_time_value, date.get_time_value]).to eq([Date.new(2017, 9, 4), nil, nil]) + expect([date_time.get_date_value, date_time.get_time_value]).to eq([nil, nil]) + expect(date_time.get_date_time_value).to eq(DateTime.new(2017, 7, 29, 3, 7, 25)) + expect([time.get_date_value, time.get_date_time_value]).to eq([nil, nil]) + expect(time.get_time_value.strftime('%H:%M:%S')).to eq('08:00:00') + end + it 'reads a date time with a space separator, like the JSON gem' do expect(described_class.new('2024-01-02 03:04:05Z').get_date_time_value).to eq(DateTime.new(2024, 1, 2, 3, 4, 5)) end From b796bdab8dcd2696e2a1e455744dc6d63b43ed9e Mon Sep 17 00:00:00 2001 From: andreatp Date: Wed, 14 Oct 2026 12:48:00 +0100 Subject: [PATCH 6/6] fix: write a zero duration as PT0S --- .../serialization/iso_duration.rb | 4 ++-- .../abstractions/spec/microsoft_kiota_abstractions_spec.rb | 6 ++++++ .../text/spec/text_serialization_writer_spec.rb | 5 +++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/components/abstractions/lib/microsoft_kiota_abstractions/serialization/iso_duration.rb b/components/abstractions/lib/microsoft_kiota_abstractions/serialization/iso_duration.rb index d2a840a..2f04d36 100644 --- a/components/abstractions/lib/microsoft_kiota_abstractions/serialization/iso_duration.rb +++ b/components/abstractions/lib/microsoft_kiota_abstractions/serialization/iso_duration.rb @@ -49,7 +49,7 @@ def string end iso_str = iso_str.strip iso_str = iso_str.chomp('T') if iso_str[-1].eql? 'T' - iso_str + iso_str == 'P' ? 'PT0S' : iso_str end def normalize @@ -182,7 +182,7 @@ def parse_hash(input) end iso_str = iso_str.strip iso_str = iso_str.chomp('T') if iso_str[-1].eql? 'T' - ISO8601::Duration.new(iso_str) + ISO8601::Duration.new(iso_str == 'P' ? 'PT0S' : iso_str) end def update_member_variables diff --git a/components/abstractions/spec/microsoft_kiota_abstractions_spec.rb b/components/abstractions/spec/microsoft_kiota_abstractions_spec.rb index 29af336..6218035 100644 --- a/components/abstractions/spec/microsoft_kiota_abstractions_spec.rb +++ b/components/abstractions/spec/microsoft_kiota_abstractions_spec.rb @@ -100,6 +100,12 @@ expect(time3.string).to eq(MicrosoftKiotaAbstractions::ISODuration.new('P4Y2MT4H').string) end + it 'writes and builds a zero duration as PT0S' do + expect(MicrosoftKiotaAbstractions::ISODuration.new('PT0S').string).to eq('PT0S') + expect(MicrosoftKiotaAbstractions::ISODuration.new({ seconds: 0 }).string).to eq('PT0S') + expect((MicrosoftKiotaAbstractions::ISODuration.new('PT2H') - MicrosoftKiotaAbstractions::ISODuration.new('PT2H')).string).to eq('PT0S') + end + it 'handles subtraction' do time1 = MicrosoftKiotaAbstractions::ISODuration.new('P4Y2MT2H') time2 = MicrosoftKiotaAbstractions::ISODuration.new({ years: 1, months: 1, hours: 2 }) diff --git a/components/serialization/text/spec/text_serialization_writer_spec.rb b/components/serialization/text/spec/text_serialization_writer_spec.rb index a2c48ba..16a3368 100644 --- a/components/serialization/text/spec/text_serialization_writer_spec.rb +++ b/components/serialization/text/spec/text_serialization_writer_spec.rb @@ -168,6 +168,11 @@ end describe 'edge cases' do + it 'writes a zero duration as PT0S' do + writer.write_duration_value(nil, MicrosoftKiotaAbstractions::ISODuration.new('PT0S')) + expect(writer.get_serialized_content).to eq('PT0S') + end + it 'writes a zero number' do writer.write_number_value(nil, 0) expect(writer.get_serialized_content).to eq('0')