diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..8ef7776 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--require spec_helper +--require virtual_assembly/semantizer diff --git a/lib/virtual_assembly/semantizer/semantic_object.rb b/lib/virtual_assembly/semantizer/semantic_object.rb index 7d1c303..c70492c 100644 --- a/lib/virtual_assembly/semantizer/semantic_object.rb +++ b/lib/virtual_assembly/semantizer/semantic_object.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright © 2023 Maxime Lecoq, # # Permission is hereby granted, free of charge, to any person obtaining a copy of this software @@ -23,153 +25,142 @@ # A semanticObject holds semantic properties (SemanticProperty) # that refers to linked data concepts. # -# For example, a Person object including this module could register +# For example, a Person object including this module could register # in its initializer method a semantic property for its name like: # Person.registerSemanticProperty("http://xmlns.com/foaf/0.1/name") {self.name} -module VirtualAssembly::Semantizer::SemanticObject - - # The semantic ID implements the concept of linked data ID. - # - # This ID is an uri pointing to the location of the object - # on the web like "https://mywebsite/myobject" for instance. - # - # If a SemanticObject doesn't define its ID, it - # will be considered as a blank node. - # - # This should be a String or nil. - attr_accessor :semanticId - - # The semantic type implements the concept of linked data type - # (also called class). - # - # This type is an uri pointing to the location of the linked - # data concept on the web like "http://xmlns.com/foaf/0.1/Person" - # for instance. - # - # This should be a String or nil. - attr_accessor :semanticType - - # This Array stores the semantic properties of the object. - # To append a SemanticProperty, use the dedicated - # registerSemanticProperty method. You should pass the value - # of the property as a block (callback) like so: - # registerSemanticProperty("http://xmlns.com/foaf/0.1/name") {self.name}. - attr_reader :semanticProperties - - # If the semanticId is nil, the object will be treated as a blank node. - def initialize(semanticId = nil, semanticType = nil) - @semanticProperties = Array.new - - # This Hash allows us to find a property using its name. - # - # Hash - # - # The key store the name of a property (String). - # The value store the index of the property in the - # semanticProperties array (Integer). - @semanticPropertiesNameIndex = Hash.new +module VirtualAssembly + module Semantizer + module SemanticObject + # The semantic ID implements the concept of linked data ID. + # + # This ID is an uri pointing to the location of the object + # on the web like "https://mywebsite/myobject" for instance. + # + # If a SemanticObject doesn't define its ID, it + # will be considered as a blank node. + # + # This should be a String or nil. + attr_accessor :semanticId + + # The semantic type implements the concept of linked data type + # (also called class). + # + # This type is an uri pointing to the location of the linked + # data concept on the web like "http://xmlns.com/foaf/0.1/Person" + # for instance. + # + # This should be a String or nil. + attr_accessor :semanticType + + # If the semanticId is nil, the object will be treated as a blank node. + def initialize(semanticId = nil, semanticType = nil) + @semanticPropertiesMap = {} # Ensure to call the setter methods self.semanticId = semanticId self.semanticType = semanticType - end - - def hasSemanticProperty?(name) - return @semanticPropertiesNameIndex.include?(name) - end - - def isBlankNode? - return @semanticId == nil || @semanticId == "" - end - - # Given the name of the property, it returns the value - # associated to a property of this object. - def semanticPropertyValue(name) - index = @semanticPropertiesNameIndex.fetch(name, nil) - return index != nil ? @semanticProperties[index].value : nil; - end - - # Use this method to append a semantic property to this object. - # The value of the property should be passed as a block so its - # value would be up to date when we will access it. - def registerSemanticProperty(name, &valueGetter) + end + + # This Array stores the semantic properties of the object. + # To append a SemanticProperty, use the dedicated + # registerSemanticProperty method. You should pass the value + # of the property as a block (callback) like so: + # registerSemanticProperty("http://xmlns.com/foaf/0.1/name") {self.name}. + def semanticProperties + @semanticPropertiesMap.values + end + + def hasSemanticProperty?(name) + @semanticPropertiesMap.key?(name) + end + + def isBlankNode? + @semanticId.nil? || @semanticId == '' + end + + # Given the name of the property, it returns the value + # associated to a property of this object. + def semanticPropertyValue(name) + semanticProperty(name)&.value + end + + # Given its name, returns the corresponding SemanticProperty + # stored by this object or nil if the property does not exist. + def semanticProperty(name) + @semanticPropertiesMap[name] + end + + # Use this method to append a semantic property to this object. + # The value of the property should be passed as a block so its + # value would be up to date when we will access it. + def registerSemanticProperty(name, &valueGetter) createOrUpdateSemanticProperty(name, valueGetter) - end - - # Sets the semantic id of the object and registers the - # corresponding semantic property. - # - # The semantic ID implements the concept of linked data ID. - # - # This ID is an uri pointing to the location of the object - # on the web like "https://mywebsite/myobject" for instance. - # - # If a SemanticObject doesn't define its ID, it - # will be considered as a blank node. - # - # This should be a String or nil. - def semanticId=(uri) + end + + # Sets the semantic id of the object and registers the + # corresponding semantic property. + # + # The semantic ID implements the concept of linked data ID. + # + # This ID is an uri pointing to the location of the object + # on the web like "https://mywebsite/myobject" for instance. + # + # If a SemanticObject doesn't define its ID, it + # will be considered as a blank node. + # + # This should be a String or nil. + def semanticId=(uri) @semanticId = uri - registerSemanticProperty("@id") {self.semanticId} - end - - # Sets the semantic type of the object and registers the - # corresponding semantic property. - # - # The semantic type implements the concept of linked data type - # (also called class). - # - # This type is an uri pointing to the location of the linked - # data concept on the web like "http://xmlns.com/foaf/0.1/Person" - # for instance. - # - # This should be a String or nil. - def semanticType=(type) + property = registerSemanticProperty('@id') { semanticId } + property.valueSetter = ->(value) { @semanticId = value } + end + + # Sets the semantic type of the object and registers the + # corresponding semantic property. + # + # The semantic type implements the concept of linked data type + # (also called class). + # + # This type is an uri pointing to the location of the linked + # data concept on the web like "http://xmlns.com/foaf/0.1/Person" + # for instance. + # + # This should be a String or nil. + def semanticType=(type) @semanticType = type - registerSemanticProperty("@type") {self.semanticType} - end - - # Serialize all the semantic properties of this object - # to an output format. - # - # You could use the HashSerializer to export as a Hash. - # This Hash should be then exported to JSON for instance. - def serialize(serializer) - return serializer.process(self) - end - - protected - - # If the semantic property already exist in this object, this - # method will simply update the valueGetter of the property. - # - # If this object does not holds the property, the new property - # will be added into the semanticProperties Array of this object. - def createOrUpdateSemanticProperty(name, valueGetter) - # Update - if (hasSemanticProperty?(name)) - semanticProperty = findSemanticProperty(name) - if (semanticProperty != nil) - semanticProperty.valueGetter = valueGetter - end - - # Create - else - @semanticProperties.push(VirtualAssembly::Semantizer::SemanticProperty.new(name, &valueGetter)) - index = @semanticProperties.count - 1 - @semanticPropertiesNameIndex.store(name, index); - end - end - - # Given its name, returns the corresponding SemanticProperty - # stored by this object or nil if the property does not exist. - def findSemanticProperty(name) - begin - index = @semanticPropertiesNameIndex.fetch(name) - return @semanticProperties.at(index) - rescue - return nil - end + property = registerSemanticProperty('@type') { semanticType } + property.valueSetter = ->(value) { @semanticType = value } + end + + # Serialize all the semantic properties of this object + # to an output format. + # + # You could use the HashSerializer to export as a Hash. + # This Hash should be then exported to JSON for instance. + def serialize(serializer) + serializer.process(self) + end + + protected + + # If the semantic property already exist in this object, this + # method will simply update the valueGetter of the property. + # + # If this object does not holds the property, the new property + # will be added into the semanticProperties Array of this object. + def createOrUpdateSemanticProperty(name, valueGetter) + # Update + if hasSemanticProperty?(name) + property = semanticProperty(name) + property&.valueGetter = valueGetter + + # Create + else + property = VirtualAssembly::Semantizer::SemanticProperty.new(name, &valueGetter) + @semanticPropertiesMap[name] = property end - -end \ No newline at end of file + property + end + end + end +end diff --git a/lib/virtual_assembly/semantizer/semantic_property.rb b/lib/virtual_assembly/semantizer/semantic_property.rb index 0c0fb71..c7d259d 100644 --- a/lib/virtual_assembly/semantizer/semantic_property.rb +++ b/lib/virtual_assembly/semantizer/semantic_property.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copyright © 2023 Maxime Lecoq, # # Permission is hereby granted, free of charge, to any person obtaining a copy of this software @@ -15,15 +17,15 @@ # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# The SemanticPropety class is designed to turn properties of +# The SemanticPropety class is designed to turn properties of # objects into linked data. # # A SemanticProperty has a name and a corresponding value that # can be fetched later (so its value would be up to date). # -# This class is intented to be used through the SemanticObject +# This class is intented to be used through the SemanticObject # class. -# +# # For instance, we can tell that the name of a Person object refers # to the linked data concept "name" from the FOAF project. The name # of the property would be the uri of the FOAF:name property while the @@ -31,33 +33,45 @@ # # You should use a block to pass the value like so: # SemanticProperty.new("http://xmlns.com/foaf/0.1/name") {self.name} -class VirtualAssembly::Semantizer::SemanticProperty - - # The name of the property. It generally points to an uri - # like "http://xmlns.com/foaf/0.1/name" or it is used to - # define a reserved linked data property like "@id". - # - # This should be a String. - attr_accessor :name - - # The function to call when the value is requested. - # - # This should be a Proc passed as a Block. - attr_accessor :valueGetter - - # @param name The name of the property, like - # "http://xmlns.com/foaf/0.1/name" or "@id" for instance. - # - # @param valueGetter A Proc used to retrieve the value of the - # property when requested. - def initialize(name, &valueGetter) +module VirtualAssembly + module Semantizer + class SemanticProperty + # The name of the property. It generally points to an uri + # like "http://xmlns.com/foaf/0.1/name" or it is used to + # define a reserved linked data property like "@id". + # + # This should be a String. + attr_accessor :name + + # The function to call when the value is requested. + # + # This should be a Proc passed as a Block. + attr_accessor :valueGetter + + # The function to call to store a new value. + # + # This should be a Proc + attr_accessor :valueSetter + + # @param name The name of the property, like + # "http://xmlns.com/foaf/0.1/name" or "@id" for instance. + # + # @param valueGetter A Proc used to retrieve the value of the + # property when requested. + def initialize(name, &valueGetter) @name = name @valueGetter = valueGetter - end + end - # Fetch and returns the value associated to this property. - def value - return @valueGetter.call - end + # Fetch and returns the value associated to this property. + def value + @valueGetter.call + end -end \ No newline at end of file + # Stores a new value for this property. + def value=(new_value) + @valueSetter.call(new_value) + end + end + end +end diff --git a/spec/lib/virtual_assembly/semantizer/semantic_object_spec.rb b/spec/lib/virtual_assembly/semantizer/semantic_object_spec.rb new file mode 100644 index 0000000..2ea7d1d --- /dev/null +++ b/spec/lib/virtual_assembly/semantizer/semantic_object_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +describe VirtualAssembly::Semantizer::SemanticObject do + subject(:object) do + Class.new(Object) do + include VirtualAssembly::Semantizer::SemanticObject + end.new + end + + it 'has a semantic id' do + expect { object.semanticId = 'five' } + .to change { object.semanticId }.from(nil).to('five') + end + + it 'has semantic properties' do + expect(object.hasSemanticProperty?('@id')).to eq true + end + + it 'allow to register properties' do + property = subject.registerSemanticProperty('smokes') + expect(property.name).to eq 'smokes' + end + + it 'allows to access properties' do + object.semanticId = 'five' + + expect { object.semanticProperty('@id').value = 'six' } + .to change { object.semanticPropertyValue('@id') }.from('five').to('six') + end +end diff --git a/spec/lib/virtual_assembly/semantizer/semantic_property_spec.rb b/spec/lib/virtual_assembly/semantizer/semantic_property_spec.rb new file mode 100644 index 0000000..be261da --- /dev/null +++ b/spec/lib/virtual_assembly/semantizer/semantic_property_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +describe VirtualAssembly::Semantizer::SemanticProperty do + it 'has a name' do + property = described_class.new('http://example.net/property5') + expect(property.name).to eq 'http://example.net/property5' + end + + it 'stores a getter' do + property = described_class.new('@id') { 'person/42' } + expect(property.value).to eq 'person/42' + end + + it 'can store a setter' do + weather = "rain" + + property = described_class.new("weather") { weather } + property.valueSetter = -> (value) { weather = value } + + expect { property.value = "sunshine" } + .to change { property.value } + .from("rain").to("sunshine") + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..1d4048f --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode + # config.disable_monkey_patching! + # + # # This setting enables warnings. It's recommended, but in some cases may + # # be too noisy due to issues in dependencies. + # config.warnings = true + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed +end