# -*- encoding: utf-8 -*- require 'test_helper' require 'hexapdf/dictionary' require 'hexapdf/reference' describe HexaPDF::Dictionary do def deref(obj) if obj.kind_of?(HexaPDF::Reference) HexaPDF::Object.new('deref', oid: obj.oid, gen: obj.gen) else obj end end def add(obj) klass = HexaPDF::Object klass = HexaPDF::Dictionary if obj.kind_of?(HexaPDF::Dictionary) || obj.kind_of?(Hash) klass.new(obj, oid: 1) end def delete(obj) obj.data.value = nil end def wrap(obj, type:) type.new(obj, document: self) end attr_accessor :version before do @version = '1.2' @test_class = Class.new(HexaPDF::Dictionary) @test_class.define_field(:Boolean, type: [TrueClass, FalseClass], default: false, version: '1.3') @test_class.define_field(:Array, type: Array, required: true, default: []) @test_class.define_field(:TestClass, type: @test_class, indirect: true) @dict = @test_class.new({Array: [3, 4], Other: 5, Object: HexaPDF::Object.new(:obj)}, document: self) end describe "class methods" do it "allows defining fields and retrieving their info" do field = @test_class.field(:Boolean) refute_nil(field) assert_equal('1.3', field.version) assert_equal(false, field.default) refute(field.required?) field = @test_class.field(:Array) assert(field.required?) assert_equal([], field.default) assert(@test_class.field(:TestClass).indirect) end it "can retrieve fields from parent classes" do @inherited_class = Class.new(@test_class) assert(@inherited_class.field(:Boolean)) refute(@inherited_class.field(:Unknown)) end it "can iterate over all fields" do @inherited_class = Class.new(@test_class) @inherited_class.define_field(:Inherited, type: [Array, Symbol]) assert_equal([:Boolean, :Array, :TestClass, :Inherited], @inherited_class.each_field.map {|k, _| k }) end it "allows field access without subclassing" do refute(HexaPDF::Dictionary.field(:Test)) assert_equal([], HexaPDF::Dictionary.each_field.to_a) end it "allows defining a static type" do @test_class.define_type(:MyClass) assert_equal(:MyClass, @test_class.type) assert_equal(:MyClass, @test_class.new({}).type) end it "::type returns nil if no static type has been defined" do assert_nil(@test_class.type) end end describe "after_data_change" do it "fails if the value is not a hash" do assert_raises(ArgumentError) { HexaPDF::Dictionary.new(:Name) } end it "sets the default value for a required field that has one" do @test_class.define_field(:Type, type: Symbol, required: true, default: :MyType) obj = @test_class.new(nil) assert_equal(:MyType, obj.value[:Type]) end it "doesn't set the default values for required fields if the type class might be wrong" do @test_class.define_type(:MyType) obj = @test_class.new({}) assert_equal([], obj.value[:Array]) obj = @test_class.new({Type: :MyType}) assert_equal([], obj.value[:Array]) obj = @test_class.new({Type: :OtherType}) refute(obj.key?(:Array)) end end describe "[]" do it "allows retrieving set field values" do assert_equal([3, 4], @dict[:Array]) assert_equal(5, @dict[:Other]) end it "uses a default value if no value is set" do assert_equal(false, @dict[:Boolean]) @dict.value[:Boolean] = true assert_equal(true, @dict[:Boolean]) end it "resolves references and stores the resolved object in place of the reference" do @dict[:value] = HexaPDF::Reference.new(1, 0) assert_equal('deref', @dict[:value]) assert_kind_of(HexaPDF::Object, @dict.value[:value]) end it "wraps hash values in specific subclasses" do @dict.value[:TestClass] = {Array: [1, 2]} assert_kind_of(@test_class, @dict[:TestClass]) assert_equal([1, 2], @dict[:TestClass][:Array]) @dict.value[:TestClass] = HexaPDF::Object.new([1, 2]) refute_kind_of(@test_class, @dict[:TestClass]) assert_equal([1, 2], @dict[:TestClass]) end it "fetches the value out of a HexaPDF::Object" do assert_equal(:obj, @dict[:Object]) end it "can convert data even if it is inside a HexaPDF::Object" do @test_class.define_field(:Binary, type: HexaPDF::DictionaryFields::PDFByteString) @dict[:Binary] = HexaPDF::Object.new('test') result = @dict[:Binary] assert_equal('test', result) assert_equal(Encoding::BINARY, result.encoding) assert_kind_of(HexaPDF::Object, @dict.value[:Binary]) assert_same(result, @dict.value[:Binary].value) @dict[:Test] = HexaPDF::Dictionary.new({}) @dict[:Test].data.value = nil assert_nil(@dict[:Test]) end end describe "[]=" do it "directly stores the value if the stored value is no HexaPDF::Object" do @dict[:Array] = [4, 5] assert_equal([4, 5], @dict.value[:Array]) @dict[:NewValue] = 7 assert_equal(7, @dict.value[:NewValue]) end it "stores the value in an existing HexaPDF::Object but only if it is not such an object" do @dict[:Object] = [4, 5] assert_equal([4, 5], @dict.value[:Object].value) @dict[:Object] = temp = HexaPDF::Object.new(:other) assert_equal(temp, @dict.value[:Object]) end it "doesn't store the value inside for subclasses of HexaPDF::Object" do (@dict[:TestClass] ||= {})[:Array] = [4, 5] assert_kind_of(@test_class, @dict[:TestClass]) @dict[:TestClass] = [4, 5] assert_equal([4, 5], @dict[:TestClass]) end it "doesn't store the value inside for HexaPDF::Reference objects" do @dict[:Object] = HexaPDF::Object.new(:test) assert_kind_of(HexaPDF::Object, @dict.value[:Object]) @dict[:Object] = HexaPDF::Reference.new(5, 0) assert_kind_of(HexaPDF::Reference, @dict.value[:Object]) end it "raises an error if the key is not a symbol object" do assert_raises(ArgumentError) { @dict[5] = 6 } end end describe "key?" do it "returns false for unset keys" do refute(@dict.key?(:UnsetKey)) end it "returns false for keys with a nil value" do @dict[:NilKey] = nil refute(@dict.key?(:NilKey)) end it "returns true for keys with a non-nil value" do assert(@dict.key?(:Other)) end end describe "validate_fields" do before do @test_class.define_field(:Inherited, type: [Array, Symbol], required: true, indirect: false) @test_class.define_field(:AllowedValues, type: Integer, allowed_values: [1, 5]) @obj = @test_class.new({Array: [], Inherited: :symbol}, document: self) end it "checks for the required fields w/wo auto_correct" do assert(@obj.validate(auto_correct: false)) assert_equal({Array: [], Inherited: :symbol}, @obj.value) @obj.value.delete(:Array) refute(@obj.validate(auto_correct: false)) assert(@obj.validate(auto_correct: true)) assert_equal({Array: [], Inherited: :symbol}, @obj.value) @obj.value.delete(:Inherited) refute(@obj.validate(auto_correct: true)) end it "updates the PDF version of the document if needed" do @obj.validate assert_equal('1.2', @version) @obj[:Boolean] = true refute(@obj.validate(auto_correct: false)) assert_equal('1.2', @version) assert(@obj.validate(auto_correct: true)) assert_equal('1.3', @version) end it "checks for the correct type of a set field" do @obj.value[:Inherited] = 'string' refute(@obj.validate(auto_correct: false)) @obj.value[:Inherited] = HexaPDF::Object.new(:symbol) assert(@obj.validate(auto_correct: false)) @obj.value[:Inherited] = Class.new(Array).new([5]) assert(@obj.validate(auto_correct: false)) @test_class.define_field(:StringField, type: String) @test_class.define_field(:NameField, type: Symbol) @obj.value[:StringField] = :symbol refute(@obj.validate(auto_correct: false)) assert(@obj.validate(auto_correct: true)) @obj.value[:NameField] = "string" assert(@obj.validate(auto_correct: true)) assert(@obj.validate(auto_correct: true)) end it "checks whether the value is an allowed one" do @obj.value[:AllowedValues] = 7 refute(@obj.validate(auto_correct: false)) @obj.value[:AllowedValues] = 1 assert(@obj.validate(auto_correct: false)) end it "checks whether a field needs to be indirect w/wo auto_correct" do @obj.value[:Inherited] = HexaPDF::Object.new(:test, oid: 1) refute(@obj.validate(auto_correct: false)) assert(@obj.validate(auto_correct: true)) assert_equal(:test, @obj.value[:Inherited]) @obj.value[:TestClass] = {Inherited: :symbol} refute(@obj.validate(auto_correct: false)) assert(@obj.validate(auto_correct: true)) assert_equal(1, @obj.value[:TestClass].oid) @obj.value[:TestClass] = HexaPDF::Object.new({Inherited: :symbol}) assert(@obj.validate(auto_correct: true)) assert_equal(1, @obj.value[:TestClass].oid) end it "validates values that are PDF objects" do @obj.value[:TestClass] = @test_class.new(nil, document: self) refute(@obj.validate) @obj.value[:TestClass][:Inherited] = :symbol assert(@obj.validate) end it "validates direct PDF objects nested in hashes" do @obj[:TestClass] = { Inherited: :symbol, Nested: {Nested: {TestClass: @test_class.new(nil, document: self)}}, } refute(@obj.validate) @obj[:TestClass][:Nested][:Nested][:TestClass][:Inherited] = :symbol assert(@obj.validate) end it "makes sure validation works in special case where the dictionary is modified" do @dict[:Array] = 5 refute(@dict.validate {|_, _, object| object[:Boolean] }) end end describe "delete" do it "deletes an entry from the underlying hash value and returns its value" do assert_equal(5, @dict.delete(:Other)) refute(@dict.value.key?(:Other)) end it "returns nil if no entry with the given name exists" do assert_nil(@dict.delete(:SomethingUnknown)) end end describe "each" do it "iterates over all name-value pairs in the dictionary" do @dict[:TestClass] = {} data = [:Array, [3, 4], :Other, 5, :Object, :obj, :TestClass, @dict[:TestClass]] @dict.each do |name, value| assert_equal(data.shift, name) assert_equal(data.shift, value) end end end describe "to_h" do it "returns a shallow copy of the value" do obj = @dict.to_h refute_equal(obj.object_id, @dict.value.object_id) assert_equal(obj, @dict.value) end end describe "type" do it "returns the /Type entry" do @dict[:Type] = :Test assert_equal(:Test, @dict.type) end it "returns the value from Object#type if not /Type entry is present" do assert_equal(:Unknown, @dict.type) end end describe "empty?" do it "returns true if the dictionary contains no entries" do assert(HexaPDF::Dictionary.new({}).empty?) refute(HexaPDF::Dictionary.new({x: 5}).empty?) end end end