# Path: fimfarchive/flavors.py
# class Flavor(Enum):
#     """
#     Base class for flavors.
#     """
# 
#     def __new__(cls, *args):
#         """
#         Automatically assigns an enum value.
#         """
#         value = len(cls.__members__) + 1
#         obj = object.__new__(cls)
#         obj._value_ = value
#         return obj
# 
#     def __repr__(self):
#         """
#         Custom representation for instances.
#         """
#         return "<flavor '{}.{}'>".format(
#             type(self).__name__,
#             getattr(self, 'name', None),
#         )
# 
# class MetaPurity(Flavor):
#     """
#     Indicates if story meta has been sanitized.
#     """
#     CLEAN = ()
#     DIRTY = ()
# 
# class UpdateStatus(Flavor):
#     """
#     Indicates if and how a story has changed.
#     """
#     CREATED = ()
#     REVIVED = ()
#     UPDATED = ()
#     DELETED = ()
# 
# Path: fimfarchive/commands/update.py
# class StoryFormatter(Iterable[str]):
#     """
#     Generates a text representation of story meta.
#     """
#     attrs = (
#         'title',
#         'author',
#         'status',
#         'words',
#         'likes',
#         'dislikes',
#         'approval',
#         'chapters',
#         'action',
#     )
# 
#     paths = {
#         'author': jmes('author.name'),
#         'status': jmes('completion_status || status'),
#         'words': jmes('num_words || words'),
#         'likes': jmes('num_likes || likes'),
#         'dislikes': jmes('num_dislikes || dislikes'),
#     }
# 
#     def __init__(self, story: Story) -> None:
#         """
#         Constructor.
# 
#         Args:
#             story: Instance to represent.
#         """
#         self.story = story
# 
#     def __getattr__(self, key: str) -> Any:
#         """
#         Returns a value from story meta, or None.
#         """
#         meta = self.story.meta
#         path = self.paths.get(key)
# 
#         if path:
#             return path.search(meta)
#         else:
#             return meta.get(key)
# 
#     def __iter__(self) -> Iterator[str]:
#         """
#         Yields the text representation line by line.
#         """
#         for attr in self.attrs:
#             label = attr.capitalize()
#             value = getattr(self, attr)
#             yield f"{label}: {value}"
# 
#     def __str__(self) -> str:
#         """
#         Returns the entire text representation.
#         """
#         return '\n'.join(self)
# 
#     @property
#     def approval(self) -> Optional[str]:
#         """
#         Returns the likes to dislikes ratio, or None.
#         """
#         likes = self.likes
#         dislikes = self.dislikes
# 
#         try:
#             ratio = likes / (likes + dislikes)
#         except TypeError:
#             return None
#         except ZeroDivisionError:
#             return f"{0:.0%}"
#         else:
#             return f"{ratio:.0%}"
# 
#     @property
#     def chapters(self) -> Optional[int]:
#         """
#         Returns the number of chapters, or None.
#         """
#         meta = self.story.meta
#         chapters = meta.get('chapters')
# 
#         if chapters is None:
#             return None
# 
#         return len(chapters)
# 
#     @property
#     def action(self) -> Optional[str]:
#         """
#         Returns the `UpdateStatus` name, or None.
#         """
#         for flavor in self.story.flavors:
#             if isinstance(flavor, UpdateStatus):
#                 return flavor.name.capitalize()
# 
#         return None


from textwrap import dedent
from typing import Any, Dict, Set
from fimfarchive.flavors import Flavor, MetaPurity, UpdateStatus
from fimfarchive.commands.update import StoryFormatter
            'status': 'visible',
            'completion_status': 'C',
            'num_words': 4,
            'num_likes': 3,
            'num_dislikes': 2,
            'chapters': [
                1
            ],
        }

        expected = """
            Title: A
            Author: B
            Status: C
            Words: 4
            Likes: 3
            Dislikes: 2
            Approval: 60%
            Chapters: 1
            Action: Created
        """

        story = story.merge(meta=meta, flavors=flavors)
        self.assert_formatted_equals(expected, story)

    def test_edge_meta(self, story):
        """
        Tests story formatting with some edge cases.
        """
        flavors: Set[Flavor] = {

# Path: fimfarchive/flavors.py
# class Flavor(Enum):
#     """
#     Base class for flavors.
#     """
# 
#     def __new__(cls, *args):
#         """
#         Automatically assigns an enum value.
#         """
#         value = len(cls.__members__) + 1
#         obj = object.__new__(cls)
#         obj._value_ = value
#         return obj
# 
#     def __repr__(self):
#         """
#         Custom representation for instances.
#         """
#         return "<flavor '{}.{}'>".format(
#             type(self).__name__,
#             getattr(self, 'name', None),
#         )
# 
# class MetaPurity(Flavor):
#     """
#     Indicates if story meta has been sanitized.
#     """
#     CLEAN = ()
#     DIRTY = ()
# 
# class UpdateStatus(Flavor):
#     """
#     Indicates if and how a story has changed.
#     """
#     CREATED = ()
#     REVIVED = ()
#     UPDATED = ()
#     DELETED = ()
# 
# Path: fimfarchive/commands/update.py
# class StoryFormatter(Iterable[str]):
#     """
#     Generates a text representation of story meta.
#     """
#     attrs = (
#         'title',
#         'author',
#         'status',
#         'words',
#         'likes',
#         'dislikes',
#         'approval',
#         'chapters',
#         'action',
#     )
# 
#     paths = {
#         'author': jmes('author.name'),
#         'status': jmes('completion_status || status'),
#         'words': jmes('num_words || words'),
#         'likes': jmes('num_likes || likes'),
#         'dislikes': jmes('num_dislikes || dislikes'),
#     }
# 
#     def __init__(self, story: Story) -> None:
#         """
#         Constructor.
# 
#         Args:
#             story: Instance to represent.
#         """
#         self.story = story
# 
#     def __getattr__(self, key: str) -> Any:
#         """
#         Returns a value from story meta, or None.
#         """
#         meta = self.story.meta
#         path = self.paths.get(key)
# 
#         if path:
#             return path.search(meta)
#         else:
#             return meta.get(key)
# 
#     def __iter__(self) -> Iterator[str]:
#         """
#         Yields the text representation line by line.
#         """
#         for attr in self.attrs:
#             label = attr.capitalize()
#             value = getattr(self, attr)
#             yield f"{label}: {value}"
# 
#     def __str__(self) -> str:
#         """
#         Returns the entire text representation.
#         """
#         return '\n'.join(self)
# 
#     @property
#     def approval(self) -> Optional[str]:
#         """
#         Returns the likes to dislikes ratio, or None.
#         """
#         likes = self.likes
#         dislikes = self.dislikes
# 
#         try:
#             ratio = likes / (likes + dislikes)
#         except TypeError:
#             return None
#         except ZeroDivisionError:
#             return f"{0:.0%}"
#         else:
#             return f"{ratio:.0%}"
# 
#     @property
#     def chapters(self) -> Optional[int]:
#         """
#         Returns the number of chapters, or None.
#         """
#         meta = self.story.meta
#         chapters = meta.get('chapters')
# 
#         if chapters is None:
#             return None
# 
#         return len(chapters)
# 
#     @property
#     def action(self) -> Optional[str]:
#         """
#         Returns the `UpdateStatus` name, or None.
#         """
#         for flavor in self.story.flavors:
#             if isinstance(flavor, UpdateStatus):
#                 return flavor.name.capitalize()
# 
#         return None


# Path: tests/commands/test_update.py
from textwrap import dedent
from typing import Any, Dict, Set
from fimfarchive.flavors import Flavor, MetaPurity, UpdateStatus
from fimfarchive.commands.update import StoryFormatter

            'status': 'visible',
            'completion_status': 'C',
            'num_words': 4,
            'num_likes': 3,
            'num_dislikes': 2,
            'chapters': [
                1
            ],
        }

        expected = """
            Title: A
            Author: B
            Status: C
            Words: 4
            Likes: 3
            Dislikes: 2
            Approval: 60%
            Chapters: 1
            Action: Created
        """

        story = story.merge(meta=meta, flavors=flavors)
        self.assert_formatted_equals(expected, story)

    def test_edge_meta(self, story):
        """
        Tests story formatting with some edge cases.
        """
        flavors: Set[Flavor] = {

            MetaPurity.DIRTY,