Repository: linkml/linkml

Issue:
Add more detailed markdowngen testing

Discussion:

Patch:
diff --git a/linkml/generators/markdowngen.py b/linkml/generators/markdowngen.py
index 9d2523b3..4365c268 100644
--- a/linkml/generators/markdowngen.py
+++ b/linkml/generators/markdowngen.py
@@ -8,7 +8,7 @@ from jsonasobj2 import JsonObj, values
 
 from linkml.generators.yumlgen import YumlGenerator
 from linkml_runtime.linkml_model.meta import SchemaDefinition, ClassDefinition, SlotDefinition, Element, ClassDefinitionName, \
-    TypeDefinition, EnumDefinition
+    TypeDefinition, EnumDefinition, SubsetDefinition
 from linkml_runtime.utils.formatutils import camelcase, be, underscore
 from linkml.utils.generator import Generator, shared_arguments
 from linkml.utils.typereferences import References
@@ -79,6 +79,10 @@ class MarkdownGenerator(Generator):
                 for enu in sorted(self.schema.enums.values(), key=lambda e: e.name):
                     self.enum_hier(enu)
 
+                self.header(3, 'Subsets')
+                for subset in sorted(self.schema.subsets.values(), key=lambda s: s.name):
+                    self.bullet(self.subset_link(subset, use_desc=True), 0)
+
                 self.header(3, 'Types')
                 self.header(4, 'Built in')
                 for builtin_name in sorted(self.synopsis.typebases.keys()):
@@ -238,6 +242,37 @@ class MarkdownGenerator(Generator):
                 self.element_header(obj=enum, name=enum.name, curie=enum_curie, uri=enum_uri)
                 self.element_properties(enum)
 
+    def visit_subset(self, subset: SubsetDefinition) -> None:
+        with open(self.exist_warning(self.dir_path(subset)), 'w') as subsetfile:
+            with redirect_stdout(subsetfile):
+                curie = self.namespaces.uri_or_curie_for(self.namespaces._base, underscore(subset.name))
+                uri = self.namespaces.uri_for(curie)
+                self.element_header(obj=subset, name=subset.name, curie=curie, uri=uri)
+                # TODO: consider showing hierarchy within a subset
+                self.header(3, 'Classes')
+                for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
+                    if not cls.mixin:
+                        if cls.in_subset and subset.name in cls.in_subset:
+                            self.bullet(self.class_link(cls, use_desc=True), 0)
+                self.header(3, 'Mixins')
+                for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
+                    if cls.mixin:
+                        if cls.in_subset and subset.name in cls.in_subset:
+                            self.bullet(self.class_link(cls, use_desc=True), 0)
+                self.header(3, 'Slots')
+                for slot in sorted(self.schema.slots.values(), key=lambda s: s.name):
+                    if slot.in_subset and subset.name in slot.in_subset:
+                        self.bullet(self.slot_link(slot, use_desc=True), 0)
+                self.header(3, 'Types')
+                for type in sorted(self.schema.types.values(), key=lambda s: s.name):
+                    if type.in_subset and subset.name in type.in_subset:
+                        self.bullet(self.type_link(type, use_desc=True), 0)
+                self.header(3, 'Enums')
+                for enum in sorted(self.schema.enums.values(), key=lambda s: s.name):
+                    if enum.in_subset and subset.name in enum.in_subset:
+                        self.bullet(self.enum_link(type, use_desc=True), 0)
+                self.element_properties(subset)
+
     def element_header(self, obj: Element, name: str, curie: str, uri: str) -> None:
         simple_name = curie.split(':', 1)[1]
         if isinstance(obj, TypeDefinition):
@@ -248,6 +283,8 @@ class MarkdownGenerator(Generator):
             obj_type = 'Slot'
         elif isinstance(obj, EnumDefinition):
             obj_type = 'Enum'
+        elif isinstance(obj, SubsetDefinition):
+            obj_type = 'Subset'
         else:
             obj_type = 'Class'
         self.header(1, f"{obj_type}: {name}" + (f" _(deprecated)_" if obj.deprecated else ""))
@@ -497,6 +534,9 @@ class MarkdownGenerator(Generator):
         elif isinstance(obj, ClassDefinition):
             link_name = camelcase(obj.name)
             link_ref = camelcase(link_name)
+        elif isinstance(obj, SubsetDefinition):
+            link_name = camelcase(obj.name)
+            link_ref = camelcase(link_name)
         else:
             link_name = obj.name
             link_ref = link_name
@@ -540,6 +580,10 @@ class MarkdownGenerator(Generator):
              return self._link(self.schema.enums[ref] if isinstance(ref, str) else ref, after_link=after_link,
                              use_desc=use_desc, add_subset=add_subset)
 
+    def subset_link(self, ref:Optional[Union[str, SubsetDefinition]], *, after_link: str = None, use_desc: bool=False) -> str:
+             return self._link(self.schema.subsets[ref] if isinstance(ref, str) else ref, after_link=after_link,
+                             use_desc=use_desc)
+
     def xlink(self, id_: str) -> str:
         return f'[{id_}]({self.id_to_url(id_)})'
 


Test patch:
diff --git a/tests/test_generators/input/kitchen_sink.yaml b/tests/test_generators/input/kitchen_sink.yaml
index 6c0c5fbd..d0438118 100644
--- a/tests/test_generators/input/kitchen_sink.yaml
+++ b/tests/test_generators/input/kitchen_sink.yaml
@@ -38,7 +38,16 @@ subsets:
 
   subset A:
     description: >-
-      A test subset
+      test subset A
+    comments:
+      - this subset is meaningless, it is just here for testing
+    aliases:
+      - A
+  subset B:
+    description: >-
+      test subset B
+    aliases:
+      - B
 
 classes:
 
@@ -49,6 +58,8 @@ classes:
         multivalued: true
 
   Person:
+    in_subset:
+      - subset A
     mixins:
       - HasAliases
     slots:
@@ -151,24 +162,34 @@ classes:
 slots:
   employed at:
     range: Company
+    in_subset:
+      - subset A
   is current:
     range: boolean
   has employment history:
     range: EmploymentEvent
     multivalued: true
     inlined_as_list: true
+    in_subset:
+      - subset B
   has marriage history:
     range: MarriageEvent
     multivalued: true
     inlined_as_list: true
+    in_subset:
+      - subset B
   has medical history:
     range: MedicalEvent
     multivalued: true
     inlined_as_list: true
+    in_subset:
+      - subset B
   has familial relationships:
     range: FamilialRelationship
     multivalued: true
     inlined_as_list: true
+    in_subset:
+      - subset B
   married to:
     range: Person
   in location:
@@ -180,6 +201,9 @@ slots:
     range: integer
     minimum_value: 0
     maximum_value: 999
+    in_subset:
+      - subset A
+      - subset B
   related to:
   type:
   street:
diff --git a/tests/test_generators/test_markdowngen.py b/tests/test_generators/test_markdowngen.py
new file mode 100644
index 00000000..e393b75c
--- /dev/null
+++ b/tests/test_generators/test_markdowngen.py
@@ -0,0 +1,51 @@
+import os
+import unittest
+from contextlib import redirect_stdout
+
+
+from linkml.generators.markdowngen import MarkdownGenerator
+from tests.test_generators.environment import env
+
+SCHEMA = env.input_path('kitchen_sink.yaml')
+MD_DIR = env.expected_path('kitchen_sink_docs')
+
+def assert_mdfile_contains(filename, text, after=None, description=None) -> None:
+    found = False
+    is_after = False  ## have we reached the after mark?
+    with open(os.path.join(MD_DIR, filename)) as stream:
+        for line in stream.readlines():
+            if text in line:
+                if after is None:
+                    found = True
+                else:
+                    if is_after:
+                        found = True
+            if after is not None and after in line:
+                is_after = True
+    assert found
+
+class MarkdownGeneratorTestCase(unittest.TestCase):
+
+    def test_markdowngen(self):
+        """ DDL  """
+        gen = MarkdownGenerator(SCHEMA, mergeimports=True, no_types_dir=True)
+        md = gen.serialize(directory=MD_DIR)
+
+        assert_mdfile_contains('index.md', 'Address', after='Classes')
+        assert_mdfile_contains('index.md', 'HasAliases', after='Mixins')
+        assert_mdfile_contains('index.md', 'acted on behalf of', after='Slots')
+        assert_mdfile_contains('index.md', 'DiagnosisType', after='Enums')
+        assert_mdfile_contains('index.md', 'test subset A', after='Subsets')
+
+        assert_mdfile_contains('SubsetA.md', '* [Person](Person.md)',
+                               description="person class is declared to be in subset A")
+
+        assert_mdfile_contains('Person.md', 'has medical history', after='Own')
+        assert_mdfile_contains('Person.md', 'aliases', after='Mixed in from HasAliases')
+
+        assert_mdfile_contains('has_medical_history.md', 'MedicalEvent', after='Domain and Range')
+        assert_mdfile_contains('has_medical_history.md', 'subset B', after='Other properties')
+
+
+if __name__ == '__main__':
+    unittest.main()