Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Attribute Naming Strategy #348

Closed
gokgozf opened this issue Dec 11, 2020 · 13 comments · Fixed by #374
Closed

Custom Attribute Naming Strategy #348

gokgozf opened this issue Dec 11, 2020 · 13 comments · Fixed by #374

Comments

@gokgozf
Copy link

gokgozf commented Dec 11, 2020

I was looking for a custom attribute naming for generated classes.
I investigated the HandlerInterface and Attr class.

Initially, I thought the 'local_name' in Attr class is XML's local name and name is the attribute name.
But I guess localname is not used there.

Is it possible to create an AttributeNameHandler to implement a custom naming strategy?

@tefra
Copy link
Owner

tefra commented Dec 11, 2020

Do you mean a custom naming strategy for the generated models and their fields?

You can use a configuration file with the cli to access some more advance features like naming conventions or add custom aliases.

@gokgozf
Copy link
Author

gokgozf commented Dec 17, 2020

Do you mean something like this?

@task("generate_sources")
@depends("prepare")
def generate_classes(project, logger):
    from xsdata.models.config import GeneratorAlias
    from xsdata.cli import resolve_source
    from xsdata.models.mixins import attribute
    from xsdata.codegen.transformer import SchemaTransformer
    from xsdata.models.config import GeneratorConfig, OutputFormat, OutputStructure
    logger.info("Generating Sources")

    class AttributeAliasGenerator(GeneratorAlias):
        source: str = attribute(required=True)
        target: str = attribute(required=True)

    class ClassAliasGenerator(GeneratorAlias):
        pass

    config = GeneratorConfig()
    config.output.format = OutputFormat.DATACLASS
    config.output.package = project.get_mandatory_property("xsData_package")
    config.output.wsdl = False
    config.output.structure = OutputStructure.NAMESPACES
    config.aliases.field_name.append(AttributeAliasGenerator())
    config.aliases.class_name.append(ClassAliasGenerator())

    uris = resolve_source(project.get_mandatory_property("xsData_source"), wsdl=config.output.wsdl)
    transformer = SchemaTransformer(config=config, print=False)
    transformer.process_schemas(list(uris))

@tefra
Copy link
Owner

tefra commented Dec 17, 2020

If you are going with a programmatically approach you have more options, I just merged this #355 to make the NameCase enumeration callable so you can do something like this

from xsdata.cli import resolve_source
from xsdata.codegen.transformer import SchemaTransformer
from xsdata.models.config import GeneratorConfig
from xsdata.models.config import NameCase, NameConvention


class MyNameCase(NameCase, Enum):

    RANDOM = "random"

    @property
    def callback(self) -> Callable:
        if self == MyNameCase.RANDOM:
            return lambda input: ''.join(random.choice(string.ascii_letters) for _ in input)
        return super(MyNameCase, self).callback()


config = GeneratorConfig()
config.conventions.class_name = NameConvention(MyNameCase.RANDOM, "type")

uris = resolve_source(file_path, wsdl=False)
transformer = SchemaTransformer(config=config, print=False)
transformer.process_schemas(list(uris))

Just out of curiosity are you trying to implement a new NameCase scheme?

@gokgozf
Copy link
Author

gokgozf commented Dec 18, 2020

Thank you for the explanation. I will look into your new merge as well.
yes i am trying to implement new namecase scheme that is true.

@tefra
Copy link
Owner

tefra commented Dec 18, 2020

Let me know, if its generic enough I wouldn't mind adding a new one to the library

@gokgozf
Copy link
Author

gokgozf commented Dec 29, 2020

Today I was looking at the implementation for name case and I upgraded to version 20.12.
But I have just one question on that,
is your callback approach included in version 20.12, or were you expecting me to build another version to test?

Because I see your commit on the 20.12 release

but when I download the source the following is what I have

class NameCase(Enum):
    """Available naming schemes, pascal, snake, camel, mixed and mixed
    underscore."""

    PASCAL = "pascalCase"
    CAMEL = "camelCase"
    SNAKE = "snakeCase"
    MIXED = "mixedCase"
    MIXED_SNAKE = "mixedSnakeCase"

    @property
    def func(self) -> Callable:
        """Return the actual callable of the scheme."""
        return __name_case_func__[self.value]

maybe I simply miss something :)

@note Regarding the generality of the namecase, maybe not all parts are reusable there will be some model specific transformations

@tefra
Copy link
Owner

tefra commented Dec 29, 2020

It's not included in the 20.12 release, you will have to install from git until the next release

@gokgozf
Copy link
Author

gokgozf commented Dec 29, 2020

thanks for the reply :). I thought I made a stupid mistake

@gokgozf
Copy link
Author

gokgozf commented Jan 4, 2021

Hi, @tefra
Happy new year!

It looks like the transformation that I require is stateful, is there an easy way to access the current class name while processing the fields?

@tefra
Copy link
Owner

tefra commented Jan 4, 2021

Hi @gokgozf happy new year,

Currently there is no class scope I am afraid, these schemes are translated to simple jinja2 filters applied directly to names.

I am not saying it's not possible but it will make things very complicated, can you explain your use case because I don't see the point of this, why would you want to generate different field names with the same source name

Class Original Output
A field a_field
B field b_field

@gokgozf
Copy link
Author

gokgozf commented Jan 5, 2021

The model, currently I am working on is UML-based and translated to XSD during that translation there are unwanted side effects and unfortunately, it is not possible to change.

following is an example of the condition
e.g a class name => {SOMENAME}
field name => {SOMENAME}FIELDNAME

so the unfortunate issue is that based on the field name the prefix {somename} sometimes is needed to be deleted and sometimes not.

It is mostly for JSON serialization. But since there are only few rules for the above transformation, if it is easily achievable it would save me from defining JSON binding for individual fields

@tefra
Copy link
Owner

tefra commented Jan 8, 2021

Hi @gokgozf,

I 've updated the field name filters to provide the class name as context to the naming schemes.

Here is an example that adds the class name as prefix to the field names,

class MyNameCase(NameCase, Enum):
    CUSTOM = "custom"
    @property
    def callback(self) -> Callable:
        if self == MyNameCase.CUSTOM:
            return my_naming_scheme

        return super(MyNameCase, self).callback()

def my_naming_scheme(name: str, **kwargs):
    if "class_name" in kwargs:  # We are transforming a field name
        return kwargs["class_name"] + "_" + name
    return name

I am not really proud of the solution, I hope at some point in the future this will change, ideally I want to add a hook just before the final output generation that people can use to plug whatever transformations they want to do. Clean attribute names, rename classes anything, but that will take some time to design and implement.

@gokgozf
Copy link
Author

gokgozf commented Jan 9, 2021

Hi @tefra
thank you for your effort and the explanation.
With the current form, I can do the required transformation.

btw your idea of Post-processing hook is actually really nice for later

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants