diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6aef2..4017d0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed Ruby: -- Fix random code changes of the generated code by sorting elements (e36a923). -- Fix some tests so they can pass by changing the expected JSON (e36a923). -- Enable loading of measures v1.0.2 (6ef17f7). +- Fix random code changes of the generated code by sorting elements (e36a9236fa012f87946b34c36cd463709d1cd2c5). +- Fix some tests so they can pass by changing the expected JSON (e36a9236fa012f87946b34c36cd463709d1cd2c5). +- Enable loading of measures v1.0.2 (6ef17f7d4a19aebd9d89b544db115d36f7e6fe93). +- Improve SKOS Concept parsing ([PR #10](https://github.com/datafoodconsortium/connector-codegen/pull/10)). ### Changed Ruby: -- Preload context to avoid remote loading (4741acb). +- Preload context to avoid remote loading (4741acb7b2a396dc56f66f0c3b5e2d64078c7130). ## [1.0.1] - 2023-11-06 @@ -45,4 +46,4 @@ This release requires [data-model-uml version 2.1.0](https://github.com/datafood [unreleased]: https://github.com/datafoodconsortium/connector-codegen/compare/v1.0.0...HEAD [1.0.1]: https://github.com/datafoodconsortium/connector-codegen/compare/v1.0.0...v1.0.1 -[1.0.0]: https://github.com/datafoodconsortium/connector-codegen/releases/tag/v1.0.0 \ No newline at end of file +[1.0.0]: https://github.com/datafoodconsortium/connector-codegen/releases/tag/v1.0.0 diff --git a/src/org/datafoodconsortium/connector/codegen/queries.mtl b/src/org/datafoodconsortium/connector/codegen/queries.mtl index 3b52751..911bbe6 100644 --- a/src/org/datafoodconsortium/connector/codegen/queries.mtl +++ b/src/org/datafoodconsortium/connector/codegen/queries.mtl @@ -154,6 +154,9 @@ [query public isPrimitive(t: Type): Boolean = if (t.name = 'String' or t.name = 'Boolean' or t.name = 'Real' or t.name = 'Integer') then true else false endif /] [query public isPrimitive(anElement: TypedElement): Boolean = anElement.type.isPrimitive() /] +[query public isSKOSConceptClass(c: Classifier): Boolean = (c.name = 'SKOSConcept') /] +[query public isSKOSConceptPrefLabel(p: Property): Boolean = (p.name = 'prefLabels') /] + [comment query public getAttributeForStereotypedOperation(c: Class, s: String, o: String): String = invoke('org.datafoodconsortium.connector.codegen.common.Common', 'getAttributeForStereotypedOperation(org.eclipse.uml2.uml.Class, java.lang.String, java.lang.String)', Sequence{c, s, o}) /] @@ -190,4 +193,4 @@ [comment query public getUnimplementedOperations(c: Classifier): Sequence(Operation) = invoke('org.datafoodconsortium.connector.codegen.common.Common', 'getUnimplementedOperations(org.eclipse.uml2.uml.Classifier)', Sequence{c}) -/] \ No newline at end of file +/] diff --git a/src/org/datafoodconsortium/connector/codegen/ruby/class.mtl b/src/org/datafoodconsortium/connector/codegen/ruby/class.mtl index 324eafe..4ef6e62 100644 --- a/src/org/datafoodconsortium/connector/codegen/ruby/class.mtl +++ b/src/org/datafoodconsortium/connector/codegen/ruby/class.mtl @@ -15,7 +15,8 @@ class DataFoodConsortium::Connector::[aClass.name.toUpperFirst()/][generateGeneralization(aClass)/] [if (aClass.isSemantic() and aClass.generalization->isEmpty())]include VirtualAssembly::Semantizer::SemanticObject[/if] - + [if (aClass.isSKOSConceptClass())]include DataFoodConsortium::Connector::SKOSHelper[/if] + [for (property: Property | aClass.ownedAttribute) separator('\n')] # @return ['['/][property.type.name /][']'/] attr_accessor :[property.name /] diff --git a/src/org/datafoodconsortium/connector/codegen/ruby/classifier.mtl b/src/org/datafoodconsortium/connector/codegen/ruby/classifier.mtl index 65cb431..8574c3a 100644 --- a/src/org/datafoodconsortium/connector/codegen/ruby/classifier.mtl +++ b/src/org/datafoodconsortium/connector/codegen/ruby/classifier.mtl @@ -1,11 +1,13 @@ [comment encoding = UTF-8 /] [module classifier('http://www.eclipse.org/uml2/5.0.0/UML')] +[import org::datafoodconsortium::connector::codegen::queries /] [import org::datafoodconsortium::connector::codegen::ruby::common /] [template public generateImports(classifier: Classifier)] [for (ei: ElementImport | classifier.elementImport->sortedBy(i: ElementImport | i.importedElement.name)) separator('\n')][if (ei.importedElement.oclIsTypeOf(Class))][generateImport(ei)/][/if][/for] [if (classifier.oclIsTypeOf(Class))]require "virtual_assembly/semantizer"[/if] +[if (classifier.isSKOSConceptClass())]require 'datafoodconsortium/connector/skos_helper'[/if] [/template] [template public generateImport(ei: ElementImport)]require "datafoodconsortium/connector/[generateFileName(ei.importedElement.name) /]"[/template] diff --git a/src/org/datafoodconsortium/connector/codegen/ruby/operation.mtl b/src/org/datafoodconsortium/connector/codegen/ruby/operation.mtl index 760da32..abff82d 100644 --- a/src/org/datafoodconsortium/connector/codegen/ruby/operation.mtl +++ b/src/org/datafoodconsortium/connector/codegen/ruby/operation.mtl @@ -20,7 +20,9 @@ end [template public generateConstructorBody(class: Class, operation: Operation)] [if not (class.generalization->isEmpty()) or class.isSemantic()]super([generateConstructorSuper(operation, class)/])[/if] -[for (p: Property | class.ownedAttribute) separator('\n')]@[p.name /] = [p.name /][/for] +[for (p: Property | class.ownedAttribute) separator('\n')][if (p.isSKOSConceptPrefLabel())]# Sort locale alphabetically +@[p.name /] = [p.name /].sort.to_h[else]@[p.name /] = [p.name /][/if][/for] + [if (class.isSemantic())][generateSetSemanticType(class)/][/if] [for (property: Property | class.ownedAttribute->select(p: Property | p.isSemantic())) separator('\n')][generateSemanticProperty(property)/][/for] [/template] @@ -64,4 +66,4 @@ raise "Not yet implemented." [template public genOperationParameter(parameter: Parameter)] [if (parameter.direction = ParameterDirectionKind::_in)][parameter.name/][/if] -[/template] \ No newline at end of file +[/template] diff --git a/src/org/datafoodconsortium/connector/codegen/ruby/static/CHANGELOG.md b/src/org/datafoodconsortium/connector/codegen/ruby/static/CHANGELOG.md index 1d749d6..301ad62 100644 --- a/src/org/datafoodconsortium/connector/codegen/ruby/static/CHANGELOG.md +++ b/src/org/datafoodconsortium/connector/codegen/ruby/static/CHANGELOG.md @@ -9,10 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fix random code changes of the generated code by sorting elements (e36a923). -- Fix some tests so they can pass by changing the expected JSON (e36a923). -- Enable loading of measures v1.0.2 (6ef17f7). +- Fix random code changes of the generated code by sorting elements (e36a9236fa012f87946b34c36cd463709d1cd2c5). +- Fix some tests so they can pass by changing the expected JSON (e36a9236fa012f87946b34c36cd463709d1cd2c5). +- Enable loading of measures v1.0.2 (6ef17f7d4a19aebd9d89b544db115d36f7e6fe93). - Allow output context to be configured ([PR #8](https://github.com/datafoodconsortium/connector-codegen/pull/8)). +- Improve SKOS Concept parsing ([PR #10](https://github.com/datafoodconsortium/connector-codegen/pull/10)). ### Changed diff --git a/src/org/datafoodconsortium/connector/codegen/ruby/static/lib/datafoodconsortium/connector/skos_helper.rb b/src/org/datafoodconsortium/connector/codegen/ruby/static/lib/datafoodconsortium/connector/skos_helper.rb new file mode 100644 index 0000000..1d4a7e4 --- /dev/null +++ b/src/org/datafoodconsortium/connector/codegen/ruby/static/lib/datafoodconsortium/connector/skos_helper.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module DataFoodConsortium::Connector::SKOSHelper + def addAttribute(name, value) + self.instance_variable_set("@#{name}", value) + self.define_singleton_method(name) do + instance_variable_get("@#{name}") + end + end + + def hasAttribute(name) + self.methods.include?(:"#{name}") + end +end diff --git a/src/org/datafoodconsortium/connector/codegen/ruby/static/lib/datafoodconsortium/connector/skos_parser.rb b/src/org/datafoodconsortium/connector/codegen/ruby/static/lib/datafoodconsortium/connector/skos_parser.rb index 786e05d..32e1754 100644 --- a/src/org/datafoodconsortium/connector/codegen/ruby/static/lib/datafoodconsortium/connector/skos_parser.rb +++ b/src/org/datafoodconsortium/connector/codegen/ruby/static/lib/datafoodconsortium/connector/skos_parser.rb @@ -1,17 +1,19 @@ +# frozen_string_literal: true + # MIT License -# +# # Copyright (c) 2023 Maxime Lecoq -# +# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: -# +# # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. -# +# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -20,124 +22,139 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +require 'datafoodconsortium/connector/skos_helper' require 'datafoodconsortium/connector/skos_concept' require 'datafoodconsortium/connector/skos_parser_element' class DataFoodConsortium::Connector::SKOSInstance + include DataFoodConsortium::Connector::SKOSHelper - def addAttribute(name, value) - self.instance_variable_set("@#{name}", value) - self.define_singleton_method(name) do - return instance_variable_get("@#{name}") - end - end - - def hasAttribute(name) - return self.methods.include?(:"#{name}") - end - + # Return a list of singelton methods, ie the list of Concept available + def topConcepts + self.methods(false).sort + end end class DataFoodConsortium::Connector::SKOSParser + CONCEPT_SCHEMES = ["Facet", "productTypes"].freeze + + def initialize + @results = DataFoodConsortium::Connector::SKOSInstance.new + @skosConcepts = {} + @rootElements = [] + @broaders = {} + # Flag used to tell the parser to use SkosConcept object when parsing data from Concept Scheme + # defined in CONCEPT_SCHEMES + @useSkosConcept = false + end + + def parse(data) + init + + data.each do |element| + current = DataFoodConsortium::Connector::SKOSParserElement.new(element) + + setSkosConceptFlag(current) + + if current.isConcept? || current.isCollection? + if !@skosConcepts.has_key?(current.id) + concept = createSKOSConcept(current) + @skosConcepts[current.id] = concept + end - def initialize() - @results = DataFoodConsortium::Connector::SKOSInstance.new - @skosConcepts = {} - @rootElements = [] - @broaders = {} - end - - def parse(data) - init() - - data.each do |element| - current = DataFoodConsortium::Connector::SKOSParserElement.new(element) - - if (current.isConcept?() || current.isCollection?()) - if (!@skosConcepts.has_key?(current.id)) - concept = createSKOSConcept(current) - @skosConcepts[current.id] = concept - end - - if (current.hasBroader()) - current.broader.each do |broaderId| - if (!@broaders.has_key?(broaderId)) - @broaders[broaderId] = [] - end - - @broaders[broaderId].push(current.id) - end - - # No broader, save the concept to the root - else - @rootElements.push(current.id) - end + if current.hasBroader + current.broader.each do |broaderId| + if !@broaders.has_key?(broaderId) + @broaders[broaderId] = [] end - end - - @rootElements.each do |rootElementId| - setResults(@results, rootElementId) - end - return @results + @broaders[broaderId].push(current.id) + end + # No broader, save the concept to the root + else + @rootElements.push(current.id) + end + end end - protected - - def createSKOSConcept(element) - skosConcept = DataFoodConsortium::Connector::SKOSConcept.new(element.id) - skosConcept.semanticType = element.type - return skosConcept + @rootElements.each do |rootElementId| + setResults(@results, rootElementId) end - def getValueWithoutPrefix(property) - name = 'undefined' - - if (!property.include?('http') && property.include?(':')) - name = property.split(':')[1] - elsif (property.include?('#')) - name = property.split('#')[1] - end + @results + end - name = name.gsub('-', '_'); + protected - # workaround to fix concepts starting with a number - # see https://github.com/datafoodconsortium/connector-ruby/issues/3 - # see https://github.com/datafoodconsortium/ontology/issues/66 - if (name.match?("^[0-9]")) - name = "_" + name - end + def createSKOSConcept(element) + skosConcept = DataFoodConsortium::Connector::SKOSConcept.new( + element.id, broaders: element.broader, narrowers: element.narrower, prefLabels: element.label + ) + skosConcept.semanticType = element.type + + skosConcept + end - return name.upcase + def getValueWithoutPrefix(property) + name = 'undefined' + + if !property.include?('http') && property.include?(':') + name = property.split(':')[1] + elsif property.include?('#') + name = property.split('#')[1] end - private - - def init() - @results = DataFoodConsortium::Connector::SKOSInstance.new - @skosConcepts = {} - @rootElements = [] - @broaders = {} + name = name.gsub('-', '_') + + # workaround to fix concepts starting with a number + # see https://github.com/datafoodconsortium/connector-ruby/issues/3 + # see https://github.com/datafoodconsortium/ontology/issues/66 + name = "_" + name if name.match?("^[0-9]") + + name.upcase + end + + private + + def init + @results = DataFoodConsortium::Connector::SKOSInstance.new + @skosConcepts = {} + @rootElements = [] + @broaders = {} + @useSkosConcept = false + end + + def setResults(parent, id) + name = getValueWithoutPrefix(id) + + if !parent.hasAttribute(name) + if @useSkosConcept && @skosConcepts[id] + parent.addAttribute(name, @skosConcepts[id]) + else + parent.addAttribute(name, DataFoodConsortium::Connector::SKOSInstance.new) + end end - def setResults(parent, id) - name = getValueWithoutPrefix(id) + # Leaf concepts, stop the process + if !@broaders.has_key?(id) + parent.instance_variable_set("@#{name}", @skosConcepts[id]) + return + end - if (!parent.hasAttribute(name)) - parent.addAttribute(name, DataFoodConsortium::Connector::SKOSInstance.new) - end - - # Leaf concepts, stop the process - if (!@broaders.has_key?(id)) - parent.instance_variable_set("@#{name}", @skosConcepts[id]) - return - end + @broaders[id].each do |narrower| + parentSkosInstance = parent.instance_variable_get("@#{name}") - @broaders[id].each do |narrower| - childName = getValueWithoutPrefix(narrower) - parentSkosInstance = parent.instance_variable_get("@#{name}") - setResults(parentSkosInstance, narrower) # recursive call - end + setResults(parentSkosInstance, narrower) # recursive call end + end -end \ No newline at end of file + def setSkosConceptFlag(current) + @useSkosConcept = true if current.isConceptScheme? && matchingConceptSchemes(current) + end + + def matchingConceptSchemes(current) + regex = /#{CONCEPT_SCHEMES.join("|")}/ + + current.id =~ regex + end +end diff --git a/src/org/datafoodconsortium/connector/codegen/ruby/static/lib/datafoodconsortium/connector/skos_parser_element.rb b/src/org/datafoodconsortium/connector/codegen/ruby/static/lib/datafoodconsortium/connector/skos_parser_element.rb index 67fc4c0..1d4b646 100644 --- a/src/org/datafoodconsortium/connector/codegen/ruby/static/lib/datafoodconsortium/connector/skos_parser_element.rb +++ b/src/org/datafoodconsortium/connector/codegen/ruby/static/lib/datafoodconsortium/connector/skos_parser_element.rb @@ -1,17 +1,19 @@ +# frozen_string_literal: true + # MIT License -# +# # Copyright (c) 2023 Maxime Lecoq -# +# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: -# +# # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. -# +# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -22,57 +24,74 @@ class DataFoodConsortium::Connector::SKOSParserElement - attr_reader :id - attr_reader :type - attr_reader :broader - - def initialize(element) - @broader = [] - - if (element) - @id = element["@id"] - - if (element["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"]) - @type = extractId(element["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"]) - elsif (element["@type"]) - @type = extractId(element["@type"]) - else - @type = "undefined" - end - - if (element["http://www.w3.org/2004/02/skos/core#broader"]) - element["http://www.w3.org/2004/02/skos/core#broader"].each do |broader| - @broader.push(broader["@id"]) - end - end - else - @id = "" - @type = "" + attr_reader :id + attr_reader :type + attr_reader :broader + attr_reader :narrower + attr_reader :label + + def initialize(element) + @broader = [] + @narrower = [] + @label = {} + + if element + @id = element["@id"] + + if element["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"] + @type = extractId(element["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"]) + elsif element["@type"] + @type = extractId(element["@type"]) + else + @type = "undefined" + end + + if element["http://www.w3.org/2004/02/skos/core#broader"] + element["http://www.w3.org/2004/02/skos/core#broader"].each do |broader| + @broader.push(broader["@id"]) end - end - - def hasBroader() - return @broader.count > 0 - end - - def isConcept?() - return @type == "http://www.w3.org/2004/02/skos/core#Concept" || @type == "skos:Concept" - end - - def isCollection?() - return @type == "http://www.w3.org/2004/02/skos/core#Collection" || @type == "skos:Collection" - end - - private - - def extractId(data) - id = data[0] - - if (id["@id"]) - return id["@id"] + end + + if element["http://www.w3.org/2004/02/skos/core#narrower"] + element["http://www.w3.org/2004/02/skos/core#narrower"].each do |narrower| + @narrower.push(narrower["@id"]) end + end - return id + if element["http://www.w3.org/2004/02/skos/core#prefLabel"] + element["http://www.w3.org/2004/02/skos/core#prefLabel"].each do |label| + @label[label["@language"].to_sym] = label["@value"] + end + end + else + @id = "" + @type = "" end + end + + def hasBroader() + @broader.count > 0 + end + + def isConcept?() + @type == "http://www.w3.org/2004/02/skos/core#Concept" || @type == "skos:Concept" + end + + def isCollection?() + @type == "http://www.w3.org/2004/02/skos/core#Collection" || @type == "skos:Collection" + end + + def isConceptScheme? + @type == "http://www.w3.org/2004/02/skos/core#ConceptScheme" + end + + private + + def extractId(data) + id = data[0] + + return id["@id"] if id["@id"] -end \ No newline at end of file + id + end +end diff --git a/src/org/datafoodconsortium/connector/codegen/ruby/static/spec/parse_with_skos_concept_spec.rb b/src/org/datafoodconsortium/connector/codegen/ruby/static/spec/parse_with_skos_concept_spec.rb new file mode 100644 index 0000000..ed2f842 --- /dev/null +++ b/src/org/datafoodconsortium/connector/codegen/ruby/static/spec/parse_with_skos_concept_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +describe "parse with skos concept" do + describe "productTypes" do + describe "topConcepts" do + it "returns a list of available topConcepts" do + product_types = connector.PRODUCT_TYPES.topConcepts + + expected = [ + :BAKERY, :DAIRY_PRODUCT, :DRINK, :FROZEN, :FRUIT, :INEDIBLE, :LOCAL_GROCERY_STORE, + :MEAT_PRODUCT, :VEGETABLE + ] + expect(product_types).to eq(expected) + end + end + + describe "prefLabels" do + it "populates SKOS Concept prefLabels" do + drink_type = connector.PRODUCT_TYPES.DRINK + expect(drink_type.prefLabels).to eq({en: "drink", fr: "boisson" }) + end + end + + it "parses the first level" do + drink_type = connector.PRODUCT_TYPES.DRINK + + expect(drink_type).to be_a DataFoodConsortium::Connector::SKOSConcept + expect(drink_type.broaders).to eq([]) + expect(drink_type.narrowers).to include(/alcoholic-beverage/, /soft-drink/) + end + + it "parses the second level" do + drink_type = connector.PRODUCT_TYPES.DRINK.SOFT_DRINK + + expect(drink_type).to be_a DataFoodConsortium::Connector::SKOSConcept + expect(drink_type.broaders).to include(/drink/) + expect(drink_type.narrowers).to include(/fruit-juice/, /lemonade/, /smoothie/) + end + + + it "parses leaf level" do + drink_type = connector.PRODUCT_TYPES.DRINK.SOFT_DRINK.LEMONADE + + expect(drink_type).to be_a DataFoodConsortium::Connector::SKOSConcept + expect(drink_type.broaders).to include(/soft-drink/) + expect(drink_type.narrowers).to eq([]) + end + end + + + describe "facets" do + describe "topConcepts" do + it "returns a list of available topConcepts" do + facets = connector.FACETS.topConcepts + + expected = [:CERTIFICATION, :CLAIM, :NATUREORIGIN, :PARTORIGIN, :TERRITORIALORIGIN] + expect(facets).to eq(expected) + end + end + + # We tested with Product Types further levels up to a leaf. So it's fair to expect + # if the first level is good, the others are as well due to the recursive nature of the parser + it "parses the first level" do + facet = connector.FACETS.CERTIFICATION + + expect(facet).to be_a DataFoodConsortium::Connector::SKOSConcept + expect(facet.broaders).to eq([]) + expect(facet.narrowers).to include( + /OrganicLabel/, /LocalLabel/, /BiodynamicLabel/, /EthicalLabel/, /MarketingLabel/ + ) + end + end +end