diff --git a/earthsnake/share.py b/earthsnake/share.py new file mode 100644 index 0000000..aee3eb7 --- /dev/null +++ b/earthsnake/share.py @@ -0,0 +1,61 @@ +"""Share handling""" + +from random import choice +import re +from typing import Optional + +from .exc import ValidationError +from .types import ALPHA_LOWER, ALPHA_LOWER_OR_DIGIT + + +class Share: + """Class to handle a share (space or workspace in older terminology)""" + + _NAME_PATTERN = f'[{ALPHA_LOWER}][{ALPHA_LOWER_OR_DIGIT}]{{0,14}}' + _SUFFIX_PATTERN = f'[{ALPHA_LOWER}][{ALPHA_LOWER_OR_DIGIT}]{{0,52}}' + PATTERN = f'^[+]{_NAME_PATTERN}[.]{_SUFFIX_PATTERN}$' + + def __init__(self, name: str, suffix: Optional[str] = None) -> None: + suffix = suffix or self.generate_random_suffix() + + if not re.match(f'^{self._NAME_PATTERN}$', name): + raise ValidationError(f'Invalid name "{name}"') + + if not re.match(f'^{self._SUFFIX_PATTERN}$', suffix): + raise ValidationError(f'Invalid suffix "{suffix}"') + + self.name = name + self.suffix = suffix + + @classmethod + def validate_address(cls, share_address: str) -> None: + """Check if share_address is a valid share address""" + + if not re.match(cls.PATTERN, share_address): + raise ValidationError(f'Invalid share address {share_address}') + + @classmethod + def from_address(cls, share_address: str) -> 'Share': + """Create a Share object from a share address""" + + cls.validate_address(share_address) + + name, suffix = share_address[1:].split('.') + + return cls(name, suffix) + + @staticmethod + def generate_random_suffix(length: int = 53) -> str: + """Generate a random share suffix of the desired length""" + + assert 54 > length > 0 + + return choice(ALPHA_LOWER) + ''.join( + choice(ALPHA_LOWER_OR_DIGIT) for _ in range(length - 1) + ) + + def __str__(self) -> str: + return f'+{self.name}.{self.suffix}' + + def __repr__(self) -> str: + return f'' diff --git a/tests/test_share.py b/tests/test_share.py new file mode 100644 index 0000000..817bfb5 --- /dev/null +++ b/tests/test_share.py @@ -0,0 +1,113 @@ +"""Tests for the Share class""" + +import pytest + +from earthsnake.types import ALPHA_LOWER, ALPHA_LOWER_OR_DIGIT +from earthsnake.exc import ValidationError +from earthsnake.share import Share + + +@pytest.mark.parametrize( + 'name,suffix,error', + [ + pytest.param('80smusic', 'suffix', 'Invalid name "80smusic"', id='name_digitstart'), + pytest.param('a.b', 'suffix', 'Invalid name "a.b"', id='name_period'), + pytest.param('PARTY', 'suffix', 'Invalid name "PARTY"', id='name_uppercase'), + pytest.param('test', 'b.c', 'Invalid suffix "b.c"', id='suffix_period'), + pytest.param('test', '4ever', 'Invalid suffix "4ever"', id='suffix_digitstart'), + pytest.param('test', 'TIME', 'Invalid suffix "TIME"', id='suffix_uppercase'), + ], +) +def test_create_invalid(name: str, suffix: str, error: str) -> None: + """Test if share creation fails with invalid values""" + + with pytest.raises(ValidationError) as ctx: + Share(name, suffix=suffix) + + assert str(ctx.value) == error + + +def test_create() -> None: + """Test if creating a share with a name and a suffix succeeds""" + + share = Share('test', suffix='suffix') + + assert share.name == 'test' + assert share.suffix == 'suffix' + + +def test_create_no_suffix() -> None: + """Test if creating succeeds if no suffix is given, and the suffix is randomly chosen""" + + share = Share('test') + + assert share.name == 'test' + assert len(share.suffix) == 53 + + +def test_str() -> None: + """Test the __str__ method""" + + share = Share('test', 'suffix') + + assert str(share) == '+test.suffix' + + +def test_repr() -> None: + """Test the __repr__ method""" + + share = Share('test', 'suffix') + + assert repr(share) == '' + + +@pytest.mark.parametrize( + 'address', + ( + pytest.param('test.suffix', id='no_prefix'), + pytest.param('+t.st.suffix', id='invalid_name_char'), + ), +) +def test_validate_invalid(address: str) -> None: + """Test if validate_address fails if the address is invalid""" + + with pytest.raises(ValidationError) as ctx: + Share.validate_address(address) + + assert str(ctx.value) == f'Invalid share address {address}' + + +@pytest.mark.parametrize( + 'address', + ( + pytest.param('test.suffix', id='no_prefix'), + pytest.param('+t.st.suffix', id='invalid_name_char'), + ), +) +def test_from_address_invalid(address: str) -> None: + """Test if from_address fails if the address is invalid""" + + with pytest.raises(ValidationError) as ctx: + Share.from_address(address) + + assert str(ctx.value) == f'Invalid share address {address}' + + +def test_from_address() -> None: + """Test constructing a share from a string address""" + + share = Share.from_address('+test.suffix') + + assert share.name == 'test' + assert share.suffix == 'suffix' + + +@pytest.mark.parametrize('length', range(1, 54)) +def test_generate_suffix(length: int) -> None: + """Test the random suffix generator""" + + suffix = Share.generate_random_suffix(length=length) + + assert suffix[0] in ALPHA_LOWER + assert all(char in ALPHA_LOWER_OR_DIGIT for char in suffix) + assert len(suffix) == length