multi_xml 0.6.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: add5ff8df4d736d143b1d83c38c5f1b8b35d21cd
4
- data.tar.gz: ec8e8a3657ff340a9a4abefa2561e44dfee8371d
2
+ SHA256:
3
+ metadata.gz: c886733fad69eef555cb492092d63156cac53770594c3c98632a8b1a9449e8ba
4
+ data.tar.gz: 14987464ceb3c0512212aedb3416bcb31089ee44d205719def48018bde734284
5
5
  SHA512:
6
- metadata.gz: 1cb437e88276aa09e33c5b10a1e32bbf181ecae9543976ab13acc4ca458b00b1315a59f1e3e1cd95fd24e47559a85058585578ba16441ac83c98ffa81c5ee902
7
- data.tar.gz: 1931768faabe059174c01e24d12f9b1c8bcc0c9717891ec9ba9098f55c98535ad2a0efab7dcc4bfd756a569f137bf86e4b45b652a182652e6a468393103de9f5
6
+ metadata.gz: 45e2c0b67a9e1c67d72b1132cd1ca429fb6466e67275f6a8df740b8a5c8eddbff3a4c51dff432998e0b4ee3fefb07f6ce9eb43f80ac192102d40e881c242cbb5
7
+ data.tar.gz: 40f700f6a29035d870d4714e7249a64e2b4e225a0fbd91970081a4e0a4ba6103dc86444a2b7902b7e67f2b4b8164277b365f3c1a4c210dd9b4ef0a735e51201d
data/.mutant.yml ADDED
@@ -0,0 +1,16 @@
1
+ usage: opensource
2
+
3
+ integration:
4
+ name: minitest
5
+
6
+ includes:
7
+ - lib
8
+ - test
9
+
10
+ requires:
11
+ - multi_xml
12
+ - mutant/minitest/coverage
13
+
14
+ matcher:
15
+ subjects:
16
+ - MultiXml*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --order random
data/.rubocop.yml ADDED
@@ -0,0 +1,65 @@
1
+ require:
2
+ - standard
3
+
4
+ plugins:
5
+ - rubocop-performance
6
+ - rubocop-rake
7
+ - rubocop-minitest
8
+ - standard-performance
9
+
10
+ AllCops:
11
+ NewCops: enable
12
+ TargetRubyVersion: 3.2
13
+
14
+ Layout/ArgumentAlignment:
15
+ EnforcedStyle: with_fixed_indentation
16
+ IndentationWidth: 2
17
+
18
+ Layout/ArrayAlignment:
19
+ EnforcedStyle: with_fixed_indentation
20
+
21
+ Layout/CaseIndentation:
22
+ EnforcedStyle: end
23
+
24
+ Layout/EndAlignment:
25
+ EnforcedStyleAlignWith: start_of_line
26
+
27
+ Layout/LineLength:
28
+ Max: 140
29
+
30
+ Layout/ParameterAlignment:
31
+ EnforcedStyle: with_fixed_indentation
32
+ IndentationWidth: 2
33
+
34
+ Layout/SpaceInsideHashLiteralBraces:
35
+ EnforcedStyle: no_space
36
+
37
+ Metrics/ParameterLists:
38
+ CountKeywordArgs: false
39
+
40
+ Style/Alias:
41
+ EnforcedStyle: prefer_alias_method
42
+
43
+ Style/EmptyMethod:
44
+ EnforcedStyle: expanded
45
+
46
+ Style/FrozenStringLiteralComment:
47
+ EnforcedStyle: never
48
+
49
+ Style/RedundantConstantBase:
50
+ Enabled: false
51
+
52
+ Style/RescueStandardError:
53
+ EnforcedStyle: implicit
54
+
55
+ Style/StringLiterals:
56
+ EnforcedStyle: double_quotes
57
+
58
+ Style/StringLiteralsInInterpolation:
59
+ EnforcedStyle: double_quotes
60
+
61
+ Style/SymbolProc:
62
+ Enabled: false
63
+
64
+ Style/TernaryParentheses:
65
+ EnforcedStyle: require_parentheses_when_complex
data/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ 0.8.1
2
+ -----
3
+ * [Fix array unwrapping when elements contain nil](https://github.com/sferik/multi_xml/commit/09a875d832c45e2b567889398f45361ec9e36685)
4
+
5
+ 0.8.0
6
+ -----
7
+ * [Add per-parse :parser option to MultiXml.parse](https://github.com/sferik/multi_xml/commit/eb0c1ccadd9026980ba8b6dd0128d6862dc361c4)
8
+ * [Add SAX parsers for Nokogiri and LibXML](https://github.com/sferik/multi_xml/commit/5d67fe6cae3c1ef2c306f1e83fc91b9accfcb724)
9
+ * [Fix inconsistent whitespace handling across parsers](https://github.com/sferik/multi_xml/commit/55aa23f1c401e66984ad1c7d753c1b4258bf0dfd)
10
+ * [Make parsing errors inspectable with cause and xml accessors](https://github.com/sferik/multi_xml/commit/f676f1b657f3352a80ac171d9b839e41ad52a14d)
11
+ * [Drop support for JRuby](https://github.com/sferik/multi_xml/commit/27895ca3918c681ad7ddaa57c5cae7b8340bd601)
12
+
13
+ 0.7.2
14
+ -----
15
+ * [Drop support for Ruby 3.1](https://github.com/sferik/multi_xml/commit/fab6288edd36c58a2b13e0206d8bed305fcb4a4b)
16
+
17
+ 0.7.1
18
+ -----
19
+ * [Relax required Ruby version constraint to allow installation on Debian stable](https://github.com/sferik/multi_xml/commit/7d18711466a15e158dc71344ca6f6e18838ecc8d)
20
+
21
+ 0.7.0
22
+ -----
23
+ * [Add support for Ruby 3.3](https://github.com/sferik/multi_xml/pull/67)
24
+ * [Drop support for Ruby 3.0](https://github.com/sferik/multi_xml/commit/eec72c56307fede3a93f1a61553587cb278b0c8a) [and](https://github.com/sferik/multi_xml/commit/6a6dec80a36c30774a5525b45f71d346fb561e69) [earlier](https://github.com/sferik/multi_xml/commit/e7dad37a0a0be8383a26ffe515c575b5b4d04588)
25
+ * [Don't mutate strings](https://github.com/sferik/multi_xml/commit/71be3fff4afb0277a7e1c47c5f1f4b6106a8eb45)
26
+
27
+ 0.6.0
28
+ -----
29
+ * [Duplexed Streams](https://github.com/sferik/multi_xml/pull/45)
30
+ * [Support for Oga](https://github.com/sferik/multi_xml/pull/47)
31
+ * [Integer unification for Ruby 2.4](https://github.com/sferik/multi_xml/pull/54)
32
+
1
33
  0.5.5
2
34
  -----
3
35
  * [Fix symbolize_keys function](https://github.com/sferik/multi_xml/commit/a4cae3aeb690999287cd30206399abaa5ce1ae81)
data/Gemfile ADDED
@@ -0,0 +1,25 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "libxml-ruby", require: nil, platforms: :ruby
4
+ gem "nokogiri", require: nil
5
+ gem "oga", ">= 2.3", require: nil
6
+ gem "ox", require: nil, platforms: :ruby
7
+ gem "rexml", require: nil
8
+
9
+ gem "minitest", ">= 6"
10
+ gem "minitest-mock", ">= 5.27"
11
+ gem "mutant-minitest", ">= 0.14.1"
12
+ gem "rake", ">= 13.3.1"
13
+ gem "rdoc", ">= 7.0.2"
14
+ gem "rubocop", ">= 1.81.7"
15
+ gem "rubocop-minitest", ">= 0.36"
16
+ gem "rubocop-performance", ">= 1.26.1"
17
+ gem "rubocop-rake", ">= 0.7.1"
18
+ gem "simplecov", ">= 0.22"
19
+ gem "standard", ">= 1.52"
20
+ gem "standard-performance", ">= 1.9"
21
+ gem "steep", ">= 1.10", platforms: :ruby
22
+ gem "yard", ">= 0.9.38"
23
+ gem "yardstick", ">= 0.9.9"
24
+
25
+ gemspec
data/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010-2013 Erik Michaels-Ober
1
+ Copyright (c) 2010-2025 Erik Berlin
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,17 +1,5 @@
1
1
  # MultiXML
2
2
 
3
- [![Gem Version](http://img.shields.io/gem/v/multi_xml.svg)][gem]
4
- [![Build Status](http://img.shields.io/travis/sferik/multi_xml.svg)][travis]
5
- [![Dependency Status](http://img.shields.io/gemnasium/sferik/multi_xml.svg)][gemnasium]
6
- [![Code Climate](http://img.shields.io/codeclimate/github/sferik/multi_xml.svg)][codeclimate]
7
- [![Coverage Status](http://img.shields.io/coveralls/sferik/multi_xml.svg)][coveralls]
8
-
9
- [gem]: https://rubygems.org/gems/multi_xml
10
- [travis]: http://travis-ci.org/sferik/multi_xml
11
- [gemnasium]: https://gemnasium.com/sferik/multi_xml
12
- [codeclimate]: https://codeclimate.com/github/sferik/multi_xml
13
- [coveralls]: https://coveralls.io/r/sferik/multi_xml
14
-
15
3
  A generic swappable back-end for XML parsing
16
4
 
17
5
  ## Installation
@@ -23,10 +11,6 @@ A generic swappable back-end for XML parsing
23
11
  [documentation]: http://rdoc.info/gems/multi_xml
24
12
 
25
13
  ## Usage Examples
26
- Lots of Ruby libraries utilize XML parsing in some form, and everyone has their
27
- favorite XML library. In order to best support multiple XML parsers and
28
- libraries, `multi_xml` is a general-purpose swappable XML backend library. You
29
- use it like so:
30
14
  ```ruby
31
15
  require 'multi_xml'
32
16
 
@@ -54,24 +38,20 @@ The `parser` setter takes either a symbol or a class (to allow for custom XML
54
38
  parsers) that responds to `.parse` at the class level.
55
39
 
56
40
  MultiXML tries to have intelligent defaulting. That is, if you have any of the
57
- supported parsers already loaded, it will utilize them before attempting to
58
- load any. When loading, libraries are ordered by speed: first Ox, then LibXML,
41
+ supported parsers already loaded, it will use them before attempting to load
42
+ a new one. When loading, libraries are ordered by speed: first Ox, then LibXML,
59
43
  then Nokogiri, and finally REXML.
60
44
 
61
45
  ## Supported Ruby Versions
62
- This library aims to support and is [tested against][travis] the following Ruby
46
+ This library aims to support and is tested against the following Ruby
63
47
  implementations:
64
48
 
65
- * Ruby 1.9.3
66
- * Ruby 2.0.0
67
- * Ruby 2.1
68
- * Ruby 2.2
69
- * Ruby 2.3
70
- * [JRuby 9000][jruby]
71
-
72
- [jruby]: http://jruby.org/
49
+ * 3.2
50
+ * 3.3
51
+ * 3.4
52
+ * 4.0
73
53
 
74
- If something doesn't work on one of these interpreters, it's a bug.
54
+ If something doesn't work on one of these versions, it's a bug.
75
55
 
76
56
  This library may inadvertently work (or seem to work) on other Ruby
77
57
  implementations, however support will only be provided for the versions listed
@@ -90,6 +70,6 @@ MultiXML was inspired by [MultiJSON][].
90
70
  [multijson]: https://github.com/intridea/multi_json/
91
71
 
92
72
  ## Copyright
93
- Copyright (c) 2010-2013 Erik Michaels-Ober. See [LICENSE][] for details.
73
+ Copyright (c) 2010-2025 Erik Berlin. See [LICENSE][] for details.
94
74
 
95
75
  [license]: LICENSE.md
data/Rakefile ADDED
@@ -0,0 +1,61 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ # Override release task to skip gem push (handled by GitHub Actions with attestations)
4
+ Rake::Task["release"].clear
5
+ desc "Build gem and create tag (gem push handled by CI)"
6
+ task release: %w[build release:guard_clean release:source_control_push]
7
+
8
+ require "rake/testtask"
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << "test"
11
+ t.pattern = "test/**/*_test.rb"
12
+ end
13
+
14
+ require "standard/rake"
15
+ require "rubocop/rake_task"
16
+ RuboCop::RakeTask.new
17
+
18
+ require "yard"
19
+ YARD::Rake::YardocTask.new do |task|
20
+ task.files = ["lib/**/*.rb", "-", "LICENSE.md"]
21
+ task.options = [
22
+ "--no-private",
23
+ "--protected",
24
+ "--output-dir", "doc/yard",
25
+ "--markup", "markdown"
26
+ ]
27
+ end
28
+
29
+ require "yardstick/rake/measurement"
30
+ Yardstick::Rake::Measurement.new do |measurement|
31
+ measurement.output = "measurement/report.txt"
32
+ end
33
+
34
+ require "yardstick/rake/verify"
35
+ Yardstick::Rake::Verify.new do |verify|
36
+ verify.threshold = 100
37
+ end
38
+
39
+ # Steep requires native extensions not available on JRuby or Windows
40
+ unless RUBY_PLATFORM == "java" || Gem.win_platform?
41
+ require "steep/rake_task"
42
+ Steep::RakeTask.new
43
+ end
44
+
45
+ desc "Run linters"
46
+ task lint: %i[rubocop standard]
47
+
48
+ # Mutant uses fork() which is not available on Windows or JRuby
49
+ desc "Run mutation testing"
50
+ task :mutant do
51
+ if Gem.win_platform? || RUBY_PLATFORM == "java"
52
+ puts "Skipping mutant on Windows/JRuby (fork not supported)"
53
+ else
54
+ system("bundle", "exec", "mutant", "run") || exit(1)
55
+ end
56
+ end
57
+
58
+ default_tasks = %i[test lint verify_measurements mutant]
59
+ default_tasks << :steep unless RUBY_PLATFORM == "java" || Gem.win_platform?
60
+
61
+ task default: default_tasks
data/Steepfile ADDED
@@ -0,0 +1,22 @@
1
+ D = Steep::Diagnostic
2
+
3
+ target :lib do
4
+ signature "sig"
5
+
6
+ # Check core library files (excluding parser implementations that depend on optional gems)
7
+ check "lib/multi_xml.rb"
8
+ check "lib/multi_xml/constants.rb"
9
+ check "lib/multi_xml/errors.rb"
10
+ check "lib/multi_xml/file_like.rb"
11
+ check "lib/multi_xml/helpers.rb"
12
+ check "lib/multi_xml/version.rb"
13
+
14
+ # Use stdlib types
15
+ library "date"
16
+ library "time"
17
+ library "yaml"
18
+ library "bigdecimal"
19
+ library "stringio"
20
+
21
+ configure_code_diagnostics(D::Ruby.strict)
22
+ end
@@ -0,0 +1,134 @@
1
+ module MultiXml
2
+ # Hash key for storing text content within element hashes
3
+ #
4
+ # @api public
5
+ # @return [String] the key "__content__" used for text content
6
+ # @example Accessing text content
7
+ # result = MultiXml.parse('<name>John</name>')
8
+ # result["name"] #=> "John" (simplified, but internally uses __content__)
9
+ TEXT_CONTENT_KEY = "__content__".freeze
10
+
11
+ # Maps Ruby class names to XML type attribute values
12
+ #
13
+ # @api public
14
+ # @return [Hash{String => String}] mapping of Ruby class names to XML types
15
+ # @example Check XML type for a Ruby class
16
+ # RUBY_TYPE_TO_XML["Integer"] #=> "integer"
17
+ RUBY_TYPE_TO_XML = {
18
+ "Symbol" => "symbol",
19
+ "Integer" => "integer",
20
+ "BigDecimal" => "decimal",
21
+ "Float" => "float",
22
+ "TrueClass" => "boolean",
23
+ "FalseClass" => "boolean",
24
+ "Date" => "date",
25
+ "DateTime" => "datetime",
26
+ "Time" => "datetime",
27
+ "Array" => "array",
28
+ "Hash" => "hash"
29
+ }.freeze
30
+
31
+ # XML type attributes disallowed by default for security
32
+ #
33
+ # These types are blocked to prevent code execution vulnerabilities.
34
+ #
35
+ # @api public
36
+ # @return [Array<String>] list of disallowed type names
37
+ # @example Check default disallowed types
38
+ # DISALLOWED_TYPES #=> ["symbol", "yaml"]
39
+ DISALLOWED_TYPES = %w[symbol yaml].freeze
40
+
41
+ # Values that represent false in XML boolean attributes
42
+ #
43
+ # @api public
44
+ # @return [Set<String>] values considered false
45
+ # @example Check false values
46
+ # FALSE_BOOLEAN_VALUES.include?("0") #=> true
47
+ FALSE_BOOLEAN_VALUES = Set.new(%w[0 false]).freeze
48
+
49
+ # Default parsing options
50
+ #
51
+ # @api public
52
+ # @return [Hash] default options for parse method
53
+ # @example View defaults
54
+ # DEFAULT_OPTIONS[:symbolize_keys] #=> false
55
+ DEFAULT_OPTIONS = {
56
+ typecast_xml_value: true,
57
+ disallowed_types: DISALLOWED_TYPES,
58
+ symbolize_keys: false
59
+ }.freeze
60
+
61
+ # Parser libraries in preference order (fastest first)
62
+ #
63
+ # @api public
64
+ # @return [Array<Array>] pairs of [require_path, parser_symbol]
65
+ # @example View parser order
66
+ # PARSER_PREFERENCE.first #=> ["ox", :ox]
67
+ PARSER_PREFERENCE = [
68
+ ["ox", :ox],
69
+ ["libxml", :libxml],
70
+ ["nokogiri", :nokogiri],
71
+ ["rexml/document", :rexml],
72
+ ["oga", :oga]
73
+ ].freeze
74
+
75
+ # Parses datetime strings, trying Time first then DateTime
76
+ #
77
+ # @api private
78
+ # @return [Proc] lambda that parses datetime strings
79
+ PARSE_DATETIME = lambda do |string|
80
+ Time.parse(string).utc
81
+ rescue ArgumentError
82
+ DateTime.parse(string).to_time.utc
83
+ end
84
+
85
+ # Creates a file-like StringIO from base64-encoded content
86
+ #
87
+ # @api private
88
+ # @return [Proc] lambda that creates file objects
89
+ FILE_CONVERTER = lambda do |content, entity|
90
+ StringIO.new(content.unpack1("m")).tap do |io|
91
+ io.extend(FileLike)
92
+ file_io = io # : FileIO
93
+ file_io.original_filename = entity["name"]
94
+ file_io.content_type = entity["content_type"]
95
+ end
96
+ end
97
+
98
+ # Type converters for XML type attributes
99
+ #
100
+ # Maps type attribute values to lambdas that convert string content.
101
+ # Converters with arity 2 receive the content and the full entity hash.
102
+ #
103
+ # @api public
104
+ # @return [Hash{String => Proc}] mapping of type names to converter procs
105
+ # @example Using a converter
106
+ # TYPE_CONVERTERS["integer"].call("42") #=> 42
107
+ TYPE_CONVERTERS = {
108
+ # Primitive types
109
+ "symbol" => :to_sym.to_proc,
110
+ "string" => :to_s.to_proc,
111
+ "integer" => :to_i.to_proc,
112
+ "float" => :to_f.to_proc,
113
+ "double" => :to_f.to_proc,
114
+ "decimal" => ->(s) { BigDecimal(s) },
115
+ "boolean" => ->(s) { !FALSE_BOOLEAN_VALUES.include?(s.strip) },
116
+
117
+ # Date and time types
118
+ "date" => Date.method(:parse),
119
+ "datetime" => PARSE_DATETIME,
120
+ "dateTime" => PARSE_DATETIME,
121
+
122
+ # Binary types
123
+ "base64Binary" => ->(s) { s.unpack1("m") },
124
+ "binary" => ->(s, entity) { (entity["encoding"] == "base64") ? s.unpack1("m") : s },
125
+ "file" => FILE_CONVERTER,
126
+
127
+ # Structured types
128
+ "yaml" => lambda do |string|
129
+ YAML.safe_load(string, permitted_classes: [Symbol, Date, Time])
130
+ rescue ArgumentError, Psych::SyntaxError
131
+ string
132
+ end
133
+ }.freeze
134
+ end
@@ -0,0 +1,93 @@
1
+ module MultiXml
2
+ # Raised when XML parsing fails
3
+ #
4
+ # Preserves the original XML and underlying cause for debugging.
5
+ #
6
+ # @api public
7
+ # @example Catching a parse error
8
+ # begin
9
+ # MultiXml.parse('<invalid>')
10
+ # rescue MultiXml::ParseError => e
11
+ # puts e.xml # The malformed XML
12
+ # puts e.cause # The underlying parser exception
13
+ # end
14
+ class ParseError < StandardError
15
+ # The original XML that failed to parse
16
+ #
17
+ # @api public
18
+ # @return [String, nil] the XML string that caused the error
19
+ # @example Access the failing XML
20
+ # error.xml #=> "<invalid>"
21
+ attr_reader :xml
22
+
23
+ # The underlying parser exception
24
+ #
25
+ # @api public
26
+ # @return [Exception, nil] the original exception from the parser
27
+ # @example Access the cause
28
+ # error.cause #=> #<Nokogiri::XML::SyntaxError: ...>
29
+ attr_reader :cause
30
+
31
+ # Create a new ParseError
32
+ #
33
+ # @api public
34
+ # @param message [String, nil] Error message
35
+ # @param xml [String, nil] The original XML that failed to parse
36
+ # @param cause [Exception, nil] The underlying parser exception
37
+ # @return [ParseError] the new error instance
38
+ # @example Create a parse error
39
+ # ParseError.new("Invalid XML", xml: "<bad>", cause: original_error)
40
+ def initialize(message = nil, xml: nil, cause: nil)
41
+ @xml = xml
42
+ @cause = cause
43
+ super(message)
44
+ end
45
+ end
46
+
47
+ # Raised when no XML parser library is available
48
+ #
49
+ # This error is raised when MultiXml cannot find any supported XML parser.
50
+ # Install one of: ox, nokogiri, libxml-ruby, or oga.
51
+ #
52
+ # @api public
53
+ # @example Catching the error
54
+ # begin
55
+ # MultiXml.parse('<root/>')
56
+ # rescue MultiXml::NoParserError => e
57
+ # puts "Please install an XML parser gem"
58
+ # end
59
+ class NoParserError < StandardError; end
60
+
61
+ # Raised when an XML type attribute is in the disallowed list
62
+ #
63
+ # By default, 'yaml' and 'symbol' types are disallowed for security reasons.
64
+ #
65
+ # @api public
66
+ # @example Catching a disallowed type error
67
+ # begin
68
+ # MultiXml.parse('<data type="yaml">--- :key</data>')
69
+ # rescue MultiXml::DisallowedTypeError => e
70
+ # puts e.type #=> "yaml"
71
+ # end
72
+ class DisallowedTypeError < StandardError
73
+ # The disallowed type that was encountered
74
+ #
75
+ # @api public
76
+ # @return [String] the type attribute value that was disallowed
77
+ # @example Access the disallowed type
78
+ # error.type #=> "yaml"
79
+ attr_reader :type
80
+
81
+ # Create a new DisallowedTypeError
82
+ #
83
+ # @api public
84
+ # @param type [String] The disallowed type attribute value
85
+ # @return [DisallowedTypeError] the new error instance
86
+ # @example Create a disallowed type error
87
+ # DisallowedTypeError.new("yaml")
88
+ def initialize(type)
89
+ @type = type
90
+ super("Disallowed type attribute: #{type.inspect}")
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,62 @@
1
+ module MultiXml
2
+ # Mixin that provides file-like metadata to StringIO objects
3
+ #
4
+ # Used when parsing base64-encoded file content from XML.
5
+ # Adds original_filename and content_type attributes to StringIO.
6
+ #
7
+ # @api public
8
+ # @example Extending a StringIO
9
+ # io = StringIO.new("file content")
10
+ # io.extend(MultiXml::FileLike)
11
+ # io.original_filename = "document.pdf"
12
+ # io.content_type = "application/pdf"
13
+ module FileLike
14
+ # Default filename when none is specified
15
+ # @api public
16
+ # @return [String] the default filename "untitled"
17
+ DEFAULT_FILENAME = "untitled".freeze
18
+
19
+ # Default content type when none is specified
20
+ # @api public
21
+ # @return [String] the default MIME type "application/octet-stream"
22
+ DEFAULT_CONTENT_TYPE = "application/octet-stream".freeze
23
+
24
+ # Set the original filename
25
+ #
26
+ # @api public
27
+ # @param value [String] The filename to set
28
+ # @return [String] the filename that was set
29
+ # @example Set filename
30
+ # io.original_filename = "report.pdf"
31
+ attr_writer :original_filename
32
+
33
+ # Set the content type
34
+ #
35
+ # @api public
36
+ # @param value [String] The MIME type to set
37
+ # @return [String] the content type that was set
38
+ # @example Set content type
39
+ # io.content_type = "application/pdf"
40
+ attr_writer :content_type
41
+
42
+ # Get the original filename
43
+ #
44
+ # @api public
45
+ # @return [String] the original filename or "untitled" if not set
46
+ # @example Get filename
47
+ # io.original_filename #=> "document.pdf"
48
+ def original_filename
49
+ @original_filename || DEFAULT_FILENAME
50
+ end
51
+
52
+ # Get the content type
53
+ #
54
+ # @api public
55
+ # @return [String] the content type or "application/octet-stream" if not set
56
+ # @example Get content type
57
+ # io.content_type #=> "application/pdf"
58
+ def content_type
59
+ @content_type || DEFAULT_CONTENT_TYPE
60
+ end
61
+ end
62
+ end