{"id":15044256,"url":"https://github.com/bxfsh/boxfish-commons-web-model","last_synced_at":"2026-01-02T04:35:08.181Z","repository":{"id":57717946,"uuid":"72774968","full_name":"bxfsh/boxfish-commons-web-model","owner":"bxfsh","description":"Minimalist Rails-like HashMap to be used as Input/Update/Query models that scrutinises the input.","archived":false,"fork":false,"pushed_at":"2017-08-23T21:33:57.000Z","size":129,"stargazers_count":2,"open_issues_count":3,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-01-20T13:23:28.655Z","etag":null,"topics":["boxfish","java","java-8","magic","model","rest-api","sanitization","type-conversion","validation"],"latest_commit_sha":null,"homepage":"http://www.aiqudo.com","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bxfsh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-11-03T18:30:33.000Z","updated_at":"2020-04-15T19:34:03.000Z","dependencies_parsed_at":"2022-08-24T07:20:54.766Z","dependency_job_id":null,"html_url":"https://github.com/bxfsh/boxfish-commons-web-model","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bxfsh%2Fboxfish-commons-web-model","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bxfsh%2Fboxfish-commons-web-model/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bxfsh%2Fboxfish-commons-web-model/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bxfsh%2Fboxfish-commons-web-model/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bxfsh","download_url":"https://codeload.github.com/bxfsh/boxfish-commons-web-model/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243451605,"owners_count":20293168,"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":["boxfish","java","java-8","magic","model","rest-api","sanitization","type-conversion","validation"],"created_at":"2024-09-24T20:50:20.834Z","updated_at":"2026-01-02T04:35:08.136Z","avatar_url":"https://github.com/bxfsh.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# boxfish-commons-web-model\n\n![Boxfish Logo](https://raw.github.com/bxfsh/boxfish-commons-web-model/master/src/main/resources/boxfish-logo.jpg)\n\nBy [Boxfish](http://www.boxfish.com)\n\n[![Build Status](https://travis-ci.org/bxfsh/boxfish-commons-web-model.svg?branch=master)](https://travis-ci.org/bxfsh/boxfish-commons-web-model)\n\nMinimalist Rails-like HashMap to be used as Input/Update/Query models that scrutinises the input.\n\n* It binds the request input from body AND querystring\n* Allows you to *permit* or ignore inbound data\n* Allows inline specification of requirements and validation rules\n* Provides a model that can be used as body to express the 400 errors\n* Removes the need for a InputForm and UpdateForms\n* Removes the need for a Validator\n* Takes the \"magic\" of 400 error exception handling magic away\n* Standardise input field names, helping compatibility cross camelCase and snake_case\n\nDepends on:\n\n* Nothing! it's a pure Java HashMap specialised to operate inbound input.\n\n## Why a HashModel rather than an InputForm or the Entity?\n\nFor a little background, read: [How Homakov hacked GitHub and the line of code that could have prevented it] (https://gist.github.com/peternixey/1978249)\n\nThis problem is very similar the one you have if you do this.\n\n```java\n\n    // Controller.java\n    @RequestMapping(value = \"\", method = POST)\n    public ResponseEntity\u003c?\u003e create(final @RequestBody @Valid Entity input) {\n        final EntityDTO entity = repo.save(input);\n        return ResponseEntity\n            .created(URI.create(\"resources/\" + entity.getId()))\n            .body(entity);\n    }\n\n```\n\nThe common solution to the problem is creating an *InputForm* (for PUT operations, another model *UpdateForm* may be required as well). Following some good SRP, you will end up having:\n\n* *EntityInputForm:* only allows the wanted input to be bound\n* *EntityMapper:* copies the data from your input form into your entity prior to save\n* *EntityValidator:* that goes beyond the @Valid annotation and is often required\n\nThis *boxfish-commons-web-model* allows the 3 items above to be all performed in-line, reducing the amount of code repetition, keeping our entity clean and ultimately making the controller code a lot more fluent where it takes care of the inbound input.\n\n## Getting started\n\nInstall the dependency\n\n```gradle\ncompile 'com.boxfish:boxfish-commons-web-model:1.0.4'\n```\n\nThen update your classpath\n\n```console\ngradle eclipse build --refresh-dependencies\n```\n\nInclude the HashModel as your model in your controller\n\n```java\n\n    @RequestMapping(path = \"\", method = POST)\n    public ResponseEntity\u003c?\u003e create(\n            final @RequestBody RestModel input) throws Exception {\n\n        input.require(\"name\");\n        if (input.isValid()) {\n            final LabelDTO dto = service.create(groupId, input);\n        final String resource = \"groups/\" + groupId + \"/labels/\" + dto.getId();\n            return created(URI.create(resource)).body(dto);\n    }\n    else\n            return badRequest().body(input.errors());\n    }\n\n```\n\nAnd finally\n\n```java\n\n    // Controller.java\n    @RequestMapping(path = \"\", method = POST)\n    public ResponseEntity\u003c?\u003e create(\n            final @RequestBody RestModel input) throws Exception {\n\n        input\n            .require(\"tags\")\n            .rulesOnEachChildOf(\"tags\", validateTagStructure(\"tags\"));\n\n        if (input.isValid())\n            return ok(service.create(input));\n        else\n            return badRequest().body(input.errors());\n    }\n\n```\n\nAnd you can update your entity model like this:\n\n```java\n\n    // Service.java\n    public TagDTO create(final HashModel input) throws Exception {\n        if (input.isValid()) {\n            final TagEntity tag = new TagEntity();\n            tag.setName(input.get(\"name\").asString());\n            return TagDTO.builderFor(tag).build();\n        }\n        return null;\n    }\n\n```\n\nYou may use your validation within your service.\n\nHowever, I believe that worrying about the quality of the input is part of the controller, which will also enable you to control imperatively the Response Status Code based on the quality of your input.\n\n## Features\n\nFind each of the extensions provided by the `RestModel` to the standard java hash map.\n\n### Field Name Normalization\n\n`display_name`, `displayName` for the `RestModel` are the same. It normalizes the input casing and facilitates varying standards of consumers.\nE.g.: Javascript will preffer camel case (especially if ESLint is being used), whereas nativaly ruby and python would\nhave their models in snake case.\n\n### Factory Methods\n\n| Operation                            | Description                                                                                                      |\n| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |\n| `newRestModel()`                     | Simple factory for a new empty `RestModel`, usage: `newRestModel()`                                              |\n| `restModelFrom(Map\u003cString, Object\u003e)` | Creates a restModel form an existing map, used specially for injesting querystring, usage: `restModelFrom(map)`. |\n\n### Basic Operations\n\nAll the operations implemented by the HashMap\u003cString, Object\u003e are avaiable in the `RestModel`.\nWe placed here only the operations that extend those of the originally available in the HashMap\u003c,\u003e in any way.\n\n| Operation                                     | Description                                                                                                                                                        |\n| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `require(field...)`                           | makes a field required, failing during validation `isValid()` if not provided by the user; required fields are automatically permitted.                            |\n| `permit(field...)`                            | authorizes a a field to be found/retrieved in the restModel; only permitted fields are visible in the `has(field)`, `containsKey(field)` and `value(field)`        |\n| `permitAll()`                                 | permits any field, not requiring each `permit(field)` to be invoked individually.                                                                                  |\n| `baseline(field, value)`                      | defines default values for the field, which is only returned when no other value is provided through `value(field)`, or even during the construction of the map.   |\n| `isAccepted(field)`                           | `true` if the field is accepted (either required or permitted), `false` otherwise, usage: `isAccepted(field)`                                                      |\n| `isValid()`                                   | `restModel.isValid()` returns `true` if all the required fields were provided and all the rules passed, returns `false` otherwise.                                 |\n| `errors()`                                    | provides a JSON-friendly POJO loaded with validation errors, usage: `badRequest().body(restModel.errors())`                                                        |\n| `get(field)`                                  | returns the RestValue wrapper for the field, and can be used exactly the same way as the `value(field)` method.                                                    |\n| `value(field)`                                | `value(field)` is an alias to the method `get(field)` and returns the value, usage: `value(field).asDouble()` for example.                                         |\n| `containsValue(field)`                        | *unsupported*, one of the very few exceptions, the rest model does not support the `containsValue`operation.                                                       |\n| `has(field)`                                  | checks if the field is available, not null, not empty and *not blank*, usage: `has(field)`.                                                                        |\n| `rules(field, validatorBuilder)`              | allow performing extended validation that goes beyond the `required(field)` rule; for usage, see the validation section to learn how to use the validation builder |\n| `rulesOnEachChildOf(field, validatorBuilder)` | allow performing extended validation that goes beyond the `required(field)` rule; for usage, see the validation section to learn how to use the validation builder |\n\n### Validation\n\nThe `RestModel` provides a simple API for simple validation of inbound fields.\n\n#### Required fields\n\nUse `required(String... fields)` to define the fields that must be provided\n\n```java\n\n    // let input be loaded by @RequestBody in a @RestController (spring)\n    // input.errors() will return an error model that can be returned as response body.\n    boolean valid = input\n        .required(\"email\", \"password\", \"firstName\", \"lastName\")\n        .permit(\"dateOfBirth\")\n        .isValid();\n\n```\n\n#### Rules\n\nIt's also possible to define more complex validation rules using anonymous functions (expressions)\nin a very convenient way. Say that only 18+ years old are allowed to acquire a particular service:\n\n```java\n\n    // let input be loaded by @RequestBody in a @RestController (spring)\n    // input.errors() will return an error model that can be returned as response body.\n    boolean valid = input\n        .require(\"dateOfBirth\")\n        .rules(\"dateOfBirth\", (c) -\u003e c\n            .forType(Instant.class)\n            .ifValueFailsOn((model, dateOfBirth) -\u003e now().minus(18, YEARS).isBefore(dateOfBirth))\n            .warnWith(\"the user is required to be 18+ years old.\"))\n        .isValid();\n\n    UserEntity entity = UserEntity.builderFor(input).build();\n    if (valid)\n        return ok(dao.save(entity));\n    else\n        return badRequest().body(input.errors());\n\n```\n\n#### Rules on Each Child\n\nLikewise, you can run the same `rules` with a few exceptions. See the example below for more information.\n\n```java\n\n    // let input be loaded by @RequestBody in a @RestController (spring)\n    // input.errors() will return an error model that can be returned as response body.\n    boolean valid = input\n        .require(\"datesOfArrival\")\n        .rulesOnEachChildOf(\"datesOfArrival\", (c) -\u003e c\n            .ifValueFailsOn((model, rawDateOfArrival) -\u003e now().minus(2, YEARS).isBefore(rawDateOfArrival.asInstant()))\n            .warnWith(\"one of the arrivals happened more than 2 years ago\"))\n        .isValid();\n\n```\n\n## Castless Conversions\n\nAll values injested by a `RestModel`, on the way out are wrapped by a class called `RestValue`. This class has operations for most of the\ndefault types, all their generic `List\u003cT\u003e` and to the `RestModel` itself, which ultimately allow limitless nesting of the structure.\n\nThe types supported are the following:\n\n* `asString()`\n* `asLong()`\n* `asInteger()`\n* `asShort()`\n* `asBoolean()`\n* `asBigDecimal()`\n* `asFloat()`\n* `asDouble()`\n* `asInstant()`\n* `asTimestamp()`\n* `asModel()`\n* `asList()`\n* `asListOf(T)`","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbxfsh%2Fboxfish-commons-web-model","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbxfsh%2Fboxfish-commons-web-model","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbxfsh%2Fboxfish-commons-web-model/lists"}