{"id":21042587,"url":"https://github.com/asewhy/conversions","last_synced_at":"2026-02-18T06:31:28.473Z","repository":{"id":57732468,"uuid":"436328996","full_name":"AseWhy/conversions","owner":"AseWhy","description":"Simple response conversion module","archived":false,"fork":false,"pushed_at":"2024-10-16T07:45:34.000Z","size":3118,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-03T12:11:14.113Z","etag":null,"topics":["conversion","java","spring","spring-boot"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AseWhy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-12-08T17:11:15.000Z","updated_at":"2025-01-16T23:02:06.000Z","dependencies_parsed_at":"2024-10-17T21:22:59.948Z","dependency_job_id":"1aaac789-cda8-407f-9c20-92ea99c62c1e","html_url":"https://github.com/AseWhy/conversions","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AseWhy%2Fconversions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AseWhy%2Fconversions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AseWhy%2Fconversions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AseWhy%2Fconversions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AseWhy","download_url":"https://codeload.github.com/AseWhy/conversions/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254388048,"owners_count":22062975,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["conversion","java","spring","spring-boot"],"created_at":"2024-11-19T14:08:02.551Z","updated_at":"2025-10-12T23:02:29.269Z","avatar_url":"https://github.com/AseWhy.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Conversions\n\nНебольшой модуль для конвертации сущностей в DTO. Конвертация происходит за счет рефлексии. Для начала работы с модулем\nнеобходимо настроить сканирование компонентов, для чего нужно создать бин ConversionConfiguration любым удобным способом. Для\nавтоматического сканирования компонентов приложения нужно установить EnableConversions аннотацию в любую точку конфигурации\nприложения.\n\n### Зачем?\n\nДля удобной реализации паттерна DTO. Модуль позволяет не беспокоится об ручном заполнении объекта передачи данных, и делает это\nавтоматически.\n\n### Когда использовать?\n\nКогда есть достаточно сложные модели, и есть необходимость отправлять конкретные поля этого объекта конечному получателю.\nКогда используется DDD и класс A может быть представлен конечному получателю как C или D в зависимости от контекста.\n\n### Известные проблемы\n\n* При использовании конвертации на выходе из метода контроллера, может возникнуть ошибка hibernate: \"No Session\".\u003cbr/\u003eДля решения можно использовать eager загрузку полей, или использовать join загрузку полей JPA.\n* Рекурсивное преобразование к DTO вложенных полей.\u003cbr\u003eДля решения не нужно использовать вложенные поля и запрашивать их по отдельности.\n\n## Пример конфигурации модуля\n\n```java\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport io.github.asewhy.conversions.ConversionStore;\nimport io.github.asewhy.conversions.support.annotations.EnableConversions;\nimport io.github.asewhy.conversions.support.ConversionConfiguration;\nimport io.github.asewhy.conversions.support.naming.ConversionNamingStrategy;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\n@EnableConversions\npublic class ConversionConfig implements ConversionConfiguration {\n    @Autowired\n    protected ObjectMapper objectMapper;\n    @Autowired\n    protected ApplicationContext context;\n    @Autowired\n    protected ConversionSupportContext supportContext;\n\n    @Override\n    public ConversionStore conversionStore() {\n        var store = new ConversionStore(context);\n\n        store.from(\"com.example\");\n\n        return store;\n    }\n\n    @Override\n    public ObjectMapper objectMapper() {\n        return objectMapper;\n    }\n\n    @Override\n    public Object context() {\n        return supportContext;\n    }\n}\n```\n\nКак показано выше в методе `provideStore` необходимо вернуть экземпляр хранилища конверсий. В хранилище хранятся информация\nо конвертируемых сущностях, и маппинги полей к ним. Далее поставщиком конверсий все сущности будут создаваться из экземпляра этого\nхранилища.\n\nЕсли необходима собственная политика именования сущностей, то необходимо предоставить её в методе конфигурации `namingPolicy`. Пример измененной конфигурации показан ниже:\n\n```java\npublic class ConversionConfig implements ConversionConfiguration {\n    // ...\n    @Autowired\n    protected ConversionNamingStrategy conversionNamingStrategy;\n\n    @Override\n    public ConversionNamingStrategy namingStrategy() {\n        return conversionNamingStrategy;\n    }\n\n    // ...\n}\n```\n\nПример политики именования можно увидеть ниже:\n\n```java\nimport io.github.asewhy.conversions.support.CaseUtil;\nimport io.github.asewhy.conversions.support.naming.ExtrudableNamingStrategy;\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.stereotype.Component;\nimport paa.coder.noodleCriteriaBuilder.restFilter.payloads.RestFilter;\nimport paa.coder.noodleCriteriaBuilder.restFilter.payloads.RestOrder;\nimport paa.coder.noodleCriteriaBuilder.restFilter.payloads.RestPage;\nimport java.util.Set;\n\n@Component\npublic class ConversionNamingStrategy extends ExtrudableNamingStrategy {\n    /**\n     * Все сущности кроме этих будут следовать политике\n     */\n    private final static Set\u003cClass\u003c?\u003e\u003e EXCLUDED = Set.of(\n        RestFilter.class,\n        RestPage.class,\n        RestOrder.class\n    );\n\n    @Override\n    protected boolean isExcluded(@NotNull String defaultName, @NotNull Class\u003c?\u003e rawReturnType) {\n        return EXCLUDED.contains(rawReturnType);\n    }\n\n    /**\n     * Следуя этой политике, конвертер думает что все поля сущностей имеют snakeCase в запросе, и ответе\n     */\n    @Override\n    protected String convert(@NotNull String defaultName) {\n        return CaseUtil.toLowerSnakeCase(defaultName);\n    }\n}\n```\n\nВ примере показана работа с конкретно `ExtrudableNamingStrategy` классом, но никто не запрещает реализовать свою логику используя `ConversionNamingStrategy`;\n\n## Конвертация сущностей\n\nКонвертеры в модуле делятся на конвертеры ответа и конвертеры запроса.\n\n### Конвертация ответа\n\nДля указания того что класс является целью конвертации ответа необходимо пометить его как `@ResponseDTO`, как на примере ниже.\n\n```java\nimport io.github.asewhy.conversions.ConversionResponse;\nimport io.github.asewhy.conversions.support.annotations.ResponseDTO;\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.ToString;\n\n@Setter\n@Getter\n@ToString\n@ResponseDTO\npublic class SomeSourceObjectDTO extends ConversionResponse\u003cSomeSourceObject\u003e {\n    private Long id;\n}\n```\n\nЕсли есть какие-то поля, которые конвертер не может разрешить сам, можно указать метод fillInternal куда первым параметром\nбудет подаваться исходная сущность. Пример показан ниже.\n\n```java\nimport io.github.asewhy.conversions.ConversionResponse;\nimport io.github.asewhy.conversions.support.annotations.ResponseDTO;\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.ToString;\n\n@Setter\n@Getter\n@ToString\n@ResponseDTO\npublic class SomeSourceObjectDTO extends ConversionResponse\u003cSomeSourceObject\u003e {\n    private Long id;\n    private String someUnfilledField;\n    \n    @Override\n    protected void fillInternal(SomeSourceObject from, Object context) {\n        this.someUnfilledField = from.someMethodWhoReturnsSomeUnfilledFieldValue();\n    }\n}\n```\n\nТаким образом поле `someUnfilledField` будет заполнено результатом выполнения метода `someMethodWhoReturnsSomeUnfilledFieldValue`\nу `SomeSourceObject`.\n\n#### Конвертация из общего интерфейса\n\nНачиная с версии 1.0.3 есть возможность выполнять преобразования сущностей A и B реализовывающих общий \nинтерфейс в одну сущность ответа. Пример можно увидеть ниже:\n\n```java\npublic interface ExampleTestBook {\n    String getName();\n    String getIsbin();\n}\n```\n\n```java\n@Getter\n@Setter\n@ToString\n@AllArgsConstructor\npublic class ExampleTestBookInterfaceA implements ExampleTestBook {\n    private String name;\n    private String isbin;\n    private String genre;\n}\n```\n\n```java\n@Getter\n@Setter\n@ToString\n@AllArgsConstructor\npublic class ExampleTestBookInterfaceB implements ExampleTestBook {\n    private String name;\n    private String isbin;\n    private Integer pageCount;\n}\n```\n\n```java\n@Getter\n@Setter\n@ToString\n@ResponseDTO\npublic class ExampleTestBookInterfaceResponse extends ConversionResponse\u003cExampleTestBook\u003e {\n    private String name;\n    private String isbin;\n}\n```\n\nПример выше скажет конвертеру конвертировать все экземпляры `ExampleTestBook` в `ExampleTestBookInterfaceResponse`\n\n### Конвертация запроса\n\nДля указания того, что исходный объект является целью конвертации запроса нужно пометить его аннотацией `@ConversionMutator`, как в примере ниже.\n\n```java\nimport io.github.asewhy.conversions.ConversionMutator;\nimport io.github.asewhy.conversions.support.annotations.MutatorDTO;\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.ToString;\n\nimport javax.validation.constraints.Min;\n\n@Setter\n@Getter\n@ToString\n@MutatorDTO\npublic class SomeSourceObjectMutatorDTO extends ConversionMutator\u003cSomeSourceObjectDTO\u003e {\n    @Min(0)\n    private int someInt;\n}\n```\n\nЕсли есть какие-то поля, которые конвертер не может разрешить сам, можно указать метод fillInternal куда первым параметром\nбудет подаваться исходная сущность. Пример показан ниже.\n\n```java\nimport io.github.asewhy.conversions.ConversionMutator;\nimport io.github.asewhy.conversions.support.annotations.MutatorDTO;\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.ToString;\n\nimport javax.validation.constraints.Min;\n\n@Setter\n@Getter\n@ToString\n@MutatorDTO\npublic class SomeSourceObjectMutatorDTO extends ConversionMutator\u003cSomeSourceObjectDTO\u003e {\n    @Min(0)\n    private int someInt;\n    private long p;\n\n    @Override\n    protected void fillInternal(SomeSourceObjectDTO fill, Object context) {\n        if(context instanceof SomeService ss) {\n            p = ss.doSomeWhoReturnsP();\n        }\n    }\n}\n```\n\nНа примере выше, в процессе заполнения сущности поле p будет заполнено значением которое будет получено из сервиса SomeService. При \nэтом экземпляр SomeService нужно передать в конфигурации как контекст.\n\nЕсли есть мутируемая сущность является вложенной, то в ней можно заполнить некоторые поля из родительской сущности. Используя метод `fillParentInternal`\nможно инициализировать текущий объект при помощи родительского объекта. Пример показан ниже.\n\n```java\nimport io.github.asewhy.conversions.ConversionMutator;\nimport io.github.asewhy.conversions.support.annotations.MutatorDTO;\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.ToString;\n\nimport javax.validation.constraints.Min;\n\n@Setter\n@Getter\n@ToString\n@MutatorDTO\npublic class SomeParentSourceObjectMutatorDTO extends ConversionMutator\u003cSomeSourceObjectDTO\u003e {\n    private int id;\n    private SomeSourceObjectMutatorDTO children;\n}\n```\n\n```java\nimport io.github.asewhy.conversions.ConversionMutator;\nimport io.github.asewhy.conversions.support.annotations.MutatorDTO;\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.ToString;\n\nimport javax.validation.constraints.Min;\n\n@Setter\n@Getter\n@ToString\n@MutatorDTO\npublic class SomeSourceObjectMutatorDTO extends ConversionMutator\u003cSomeSourceObjectDTO\u003e {\n    @Min(0)\n    private int someInt;\n    private int parentId;\n\n    @Override\n    protected void fillParentInternal(SomeSourceObjectDTO fill, Object parent, Object context) {\n        if(parent instanceof SomeParentSourceObjectMutatorDTO p) {\n            this.parentId = p.getId();\n        }\n    }\n}\n```\n\nТаким образом поле `parentId` у мутатора `SomeSourceObjectMutatorDTO` будет заполнено родительским идентификатором, в случае\nесли сущность будет вложенной.\n\nМожет возникнуть ситуация когда мутатор будет вложенным объектом, тогда можно использовать кастомный ресолвер такого запроса. Например, \nимеется класс `ExampleTestNonMutatorRequest` одним из полей которого будет мутатор `ExampleTestMutatorRequest`, для примера это может быть\nнапример поле `request`. Тогда ресолвер для этого запроса может быть таким:\n\n```java\nimport com.fasterxml.jackson.databind.JsonNode;\nimport io.github.asewhy.conversions.ConversionProvider;\nimport org.springframework.stereotype.Component;\n\nimport java.lang.reflect.Type;\n\n@Component\npublic class ExampleTestNonMutatorRequestResolver extends RequestResolver\u003cExampleTestNonMutatorRequest\u003e {\n    @Override\n    protected ExampleTestNonMutatorRequest resolveInternalRequest(\n            @NotNull JsonNode node,\n            Class\u003c? extends ExampleTestNonMutatorRequest\u003e fromClass,\n            Type generics,\n            @NotNull ConversionProvider provider\n    ) {\n        var config = provider.getConfig();\n        var objectMapper = config.getObjectMapper();\n\n        try {\n            var data = objectMapper.treeToValue(node, ExampleTestNonMutatorRequest.class);\n\n            provider.createMutator(data.getRequest(), node.get(\"request\"));\n\n            return data;\n        } catch (JsonProcessingException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    protected boolean canProcess(Class\u003c?\u003e from, Type generics, ConversionProvider provider) {\n        return ExampleTestNonMutatorRequest.class.isAssignableFrom(from);\n    }\n}\n```\n\nОбратите внимание, что класс `ExampleTestNonMutatorRequest` не является мутатором.\n\n## Работа с контроллерами\n\nНа примере выше показан процесс декларации мутатора запроса, и объекта ответа. После декларации, его можно использовать в контроллере просто указав\nконтроллер как `@ShiftController` или пометив метод как `@ConvertResponse`, не будет работать если метод уже помечен, на пример как `@ResponseBody`. \nПример контроллера показан ниже.\n\n```java\nimport io.github.asewhy.conversions.support.annotations.ShiftController;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\n\n@ShiftController\n@RequestMapping(\"/comments\")\npublic class CommentController {\n    @GetMapping(\"/{id}\")\n    public SomeSourceObjectMutatorDTO get(@PathVariable(\"id\") I id) {\n        // Метод должен возвращать экземпляр SomeSourceObject\n        return (SomeSourceObjectMutatorDTO) provideService().restFindById(id);\n    }\n}\n```\n\nПример использования конвертера запроса показан ниже.\n\n```java\nimport io.github.asewhy.conversions.support.annotations.ConvertMutator;\nimport io.github.asewhy.conversions.support.annotations.ShiftController;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\n\nimport javax.validation.Valid;\n\n@ShiftController\n@RequestMapping(\"/comments\")\npublic class CommentController {\n    @PostMapping\n    public void editSomeEntity(@ConvertMutator @Valid SomeSourceObjectMutatorDTO payload) {\n        var foundEntity = null;// Получаем экземпляр редактируемой сущности\n        \n        // Метод fill поставляется базовым классом мутатора и автоматически заполняет поля с теми же названиями и типом в целевом объекте\n        // важно чтобы целевой объект был объектом указанным в generic при декларации мутатора\n        payload.fill(foundEntity);\n    }\n}\n```\n\n## Конвертация контейнеров\n\nУ вас могут быть контейнеры, которые включают в себя сущности для конвертации. Модуль не может их распознать автоматически, поэтому\nнеобходимо создавать специальные ресолверы для таких контейнеров. Пример ресолвера показан ниже.\n\n```java\nimport io.github.asewhy.conversions.ConversionProvider;\nimport io.github.asewhy.conversions.ResponseResolver;\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.transaction.annotation.Transactional;\nimport paa.coder.noodleCriteriaBuilder.restFilter.payloads.RestPage;\n\nimport java.lang.reflect.Type;\n\n@Component\npublic class RestPageResponseResolver extends ResponseResolver\u003cRestPage\u003c?\u003e\u003e {\n    @Override\n    protected RestPage\u003c?\u003e resolveInternalResponse(\n            @NotNull RestPage\u003c?\u003e restPage,\n            Class\u003c? extends RestPage\u003c?\u003e\u003e aClass,\n            @NotNull ConversionProvider conversionProvider,\n            String mapping\n    ) {\n        return new RestPage\u003c\u003e(\n                restPage.getFilter(),\n                restPage.getContent().parallelStream().map(conversionProvider::createResponse).toList(),\n                restPage.getTotalElements()\n        );\n    }\n\n    @Override\n    protected Class\u003c?\u003e extractInternalExample(@NotNull RestPage\u003c?\u003e from, String mapping, Object globalContextOrPassedContext) {\n        return from.stream().map(Object::getClass).findFirst().orElse(null);\n    }\n\n    @Override\n    protected boolean canProcess(Class\u003c?\u003e from, Type generics, ConversionProvider provider, String mapping) {\n        return RestPage.class.isAssignableFrom(from);\n    }\n}\n```\n\nВ примере выше ресолвится REST страница. Пример выше позволяет конвертировать содержимое Rest страницы. При этом оставляя тот же формат.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasewhy%2Fconversions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fasewhy%2Fconversions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasewhy%2Fconversions/lists"}