{"id":14235539,"url":"https://github.com/Nakilon/nakischema","last_synced_at":"2025-08-11T00:31:48.143Z","repository":{"id":47740330,"uuid":"395844057","full_name":"Nakilon/nakischema","owner":"Nakilon","description":"The most compact yet powerful arbitrary nested Ruby objects and XML (Oga) validator. Handy to validate data coming from someone else.","archived":false,"fork":false,"pushed_at":"2024-11-14T23:21:14.000Z","size":47,"stargazers_count":5,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-11-15T00:26:17.457Z","etag":null,"topics":["gem","rubygem","validation","validation-library"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Nakilon.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-08-14T00:52:46.000Z","updated_at":"2024-11-14T23:21:17.000Z","dependencies_parsed_at":"2023-10-11T14:55:47.095Z","dependency_job_id":null,"html_url":"https://github.com/Nakilon/nakischema","commit_stats":{"total_commits":20,"total_committers":2,"mean_commits":10.0,"dds":"0.050000000000000044","last_synced_commit":"fc49ccba35eb4290015b9c8ec3279368d2da6d9e"},"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nakilon%2Fnakischema","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nakilon%2Fnakischema/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nakilon%2Fnakischema/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nakilon%2Fnakischema/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Nakilon","download_url":"https://codeload.github.com/Nakilon/nakischema/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":229478661,"owners_count":18079371,"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":["gem","rubygem","validation","validation-library"],"created_at":"2024-08-20T21:02:03.341Z","updated_at":"2024-12-13T01:30:22.107Z","avatar_url":"https://github.com/Nakilon.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Nakischema\n\n## Why?\n\nI often process complex and undocumented data such as game assets or web responses. Previously I used to add asserts everywhere to check things on the fly but now I think the much better practice is to split the job into two steps and preprocess the whole data, i.e. validate it, before starting the actual work with it, because:\n\n1. Having asserts in random places may cause running them more than once for no purpose.\n2. Having asserts at all is just slowing your program.\n3. It is much simpler to figure things out after you've processed all the data. Valid schema is a valid documentation.\n\nThe whole Nakischema API is just one method.  \nThe whole schema is just one Ruby object.  \nSay no to needless DSLs.\n\nAlso exceptions are informative -- they tell you where and how things went wrong.\n\n## How?\n\n### Install\n\n```bash\ngem install nakischema\n```\n```ruby\nrequire \"nakischema\"\n```\n\n### Validate Ruby objects\n\nSchema can be as simple as just checking basic scalar objects for equality or class:\n\n```ruby\nNakischema.validate \"John\", String  # =\u003e nil\nNakischema.validate \"John\", \"Joe\"   # Nakischema::Error: expected \"Joe\" != \"John\"\nNakischema.validate \"John\", Numeric # Nakischema::Error: expected Numeric != String\n```\n\nAnd schema can be nested to validate nested objects.  \nArray and Hash objects have to be validated by Hash object schema with special keys:\n* `:each` for validating every Array item with nested schema object\n* `:hash` for validating every value via exact Hash keys match\n\n```ruby\nNakischema.validate( [\n  {name: \"John\", age: 20},\n  {name: \"Bill\", age: 15},\n], {\n  each: {\n    hash: {\n      name: /\\A[A-Z][a-z]+\\z/,\n      age: 18..100,\n    },\n  },\n} )\n```\n```none\nexpected 18..100 != 15 (at [:\"#1\", :age]) (Nakischema::Error)\n```\n\n* `:size` to specify allowed Array size range\n* `:hash_req` to specify required Hash items\n* `:hash_opt` to specify optional Hash items -- subtracted when validating `:hash` if present\n\nYour schema object can be recursive to validate objects that are recursive or just look like that:\n\n```ruby\nhuman_schema = {}\nhuman_schema.replace( {\n  hash_req: {name: /\\A[A-Z][a-z]+\\z/, age: 0..100},\n  hash_opt: {parents: {size: 2..2, each: human_schema}},\n} )\n\nfather = {name: \"John\", age: 40}\nmother = {name: \"Anna\", age: 40}\nNakischema.validate( [\n  father,\n  mother,\n  {name: \"Bill\", age: 18, parents: [father, mother]},\n],\n  {each: human_schema}\n)\n```\n\nThe `[[ ]]` syntax validates an Array in order (it also can be nested).\n\n```ruby\npets = [\n  [\"cat\", \"Thomas\"],\n]\nNakischema.validate( pets, {\n  each: [[/\\A[a-z]+\\z/, /\\A[A-Z]/]]\n} )\n```\n\nThe \"or-group\" `[ ]` tries to match the object with any of a given list of \"rules\" (schemas).\n* `:assertions` allows passing a list of lambdas to do arbitrary checks and return booleans. \n\n```ruby\nhumans = [\n  {name: \"John\", gender: :male},\n  {name: \"Anna\", gender: :female, pets: %w{ Thomas }},\n  {name: \"Bill\", gender: :attack_helicopter},\n]\nNakischema.validate( humans, {\n  each: {\n    hash_opt: {\n      pets: {\n        each: {\n          assertions: [\n            -\u003e pet_id, _ { pets.map(\u0026:last).include? pet_id },\n          ],\n        }\n      },\n    },\n    hash_req: {\n      name: /\\A[A-Z]/,\n      gender: [:male, :female],\n    },\n  },\n} )\n```\n```none\nexpected at least one of 2 rules to match the :attack_helicopter, errors: (Nakischema::Error)\n  expected :male != :attack_helicopter (at [:\"#2\", :gender, :\"variant#0\"])\n  expected :female != :attack_helicopter (at [:\"#2\", :gender, :\"variant#1\"])\n```\n\nHere you can see that nested schema validation errors produce nested exception messages with indentation so you can easily see the whole validation object tree path that was made.\n\nAnd if Anna had a pet with unfamiliar name that custom assertion would throw:\n```none\ncustom assertion failed (at [:\"#1\", :pets, :\"#0\"]) (Nakischema::Error)\n```\n\nKnown issue and workaround: the `[[[]]]` is being interpereted as `[[ [] ]]`, not `[ [[]] ]`, but you can force it by doing: `[:something, [[]]]`.\n\nThere are a few other special keys. You'll find them in source code easily.\n\n### Validate Oga objects\n\n[`gem oga`](https://rubygems.org/gems/oga) is a Nokogiri analogue that I recommend you to try out because it's written in pure Ruby. `Nakischema.validate_oga_xml` is a method more or less similar to `validate`. See basic [Wolfram Alpha API](https://products.wolframalpha.com/api/) response validation as an example:\n\n```ruby\nxml = Oga.parse_xml open link, \u0026:read\nNakischema.validate_oga_xml xml, {\n  exact: {\n    \"queryresult\" =\u003e [[ {\n      attr_req: {\"success\": \"true\", \"error\": \"false\", \"inputstring\": query},\n      assertions: [\n        -\u003en,_{ n.at_xpath(\"./pod\")[\"id\"] == \"Input\" },\n        -\u003en,_{ n.xpath(\"/pod\").each{ |_| _[\"id\"] == _[\"title\"].delete(\" \") } },\n      ],\n      exact: {\"pod\" =\u003e {size: 8..8}, \"assumptions\" =\u003e [[{}]]},\n      children: {\n        \"/*[@error='true']\" =\u003e [[]],\n        \"/pod\" =\u003e {each: {attr_req: {\"id\": /\\A([A-Z][a-z]+)+(:([A-Z][a-z]+)+)?\\z/, \"scanner\": /\\A([A-Z][a-z]+)+\\z/}}},\n        \"./pod[@title='Input']\" =\u003e [[{children: {\"subpod\" =\u003e [[{exact: {\"plaintext\" =\u003e [[{text: query}]]}}]]}}]],\n        \"./pod[@primary='true']\" =\u003e [[{children: {\"subpod\" =\u003e [[{exact: {\"plaintext\" =\u003e [[{}]]}}]]}}]],\n        \"/pod[@scanner='Numeric']\" =\u003e {each: {children: {\"subpod\" =\u003e [[{exact: {\"plaintext\" =\u003e [[{}]]}}]]}}},\n      },\n    } ]],\n  },\n}\n```\n\n* `exact` -- similar to `hash` when using `validate`  \n* `attr_req` -- required attributes  \n* `children` -- children nodes  \n* `text` -- ...text\n\n`exact` and `req` accept either node name or XPath. Each results in an array of nodes of any length, even empty -- this is why you see so many `[[]]`. Also note that results of these selectors can overlap so you are able to apply multiple schemas to the same node.\n\n### Another real life example -- validating non-json object while using `gem minitest`\n\nImagine you want to validate the args that were passed to `Monitoring.log` when calling the `log_ruby_processes`. First you write such test to see the actual args passed:\n\n```ruby\nit :log_ruby_processes do\n  mock = MiniTest::Mock.new.expect :call, nil do |*args|\n\n    require \"pp\"\n    pp args\n\n  end\n  Monitoring.stub :log, mock do\n\n    log_ruby_processes\n\n  end\n  mock.verify\nend\n```\n\n```none\n$ ruby test.rb\n...\n[2021-09-06 00:59:58 UTC,\n nil,\n {:total=\u003e3,\n  :each=\u003e\n   [[\"/Users/nakilon/.../ruby test.rb\", 35.53125, 43],\n    [\"/Users/nakilon/.../ruby take.rb\", 3.20703125, 41],\n    [\"/Users/nakilon/.../bin/paster\", 1.89453125, 14]]}]\n...\n```\n\nNow just replace the `require \"pp\"; pp args` with something like this:\n\n```ruby\n    Nakischema.validate args, [[\n      Time,\n      NilClass,\n      {\n        hash: {\n          total: 3..3,\n          each: {\n            size: 3..3,\n            each: [[/\\A\\/Users\\//, Float, Fixnum]],\n          },\n        },\n        assertions: [-\u003e h,_ { h[:total] == h[:each].size }],\n      },\n    ]]\n```\n\n### Custom mismatch message\n\nImagine you don't want a number higher than 5 (unless it's 10):\n\n```ruby\nNakischema.validate [1, 10, 7], {\n  each: [\n    { assertions: [-\u003e x, _ {\n      x \u003c= 5\n    } ] },\n    10..10,\n  ],\n}\n```\n```none\nexpected at least one of 2 rules to match the 7, errors: (Nakischema::Error)\n  custom assertion failed (at [:\"#2\", :\"variant#0\", :\"assertion#0\"])\n  expected 10..10 != 7 (at [:\"#2\", :\"variant#1\"])\n```\n\nTo replace the generic \"custom assertion failed\" message with something more useful you can raise the `Nakischema::Error` manually (don't forget to return `true` otherwise):\n\n```ruby\nNakischema.validate [1, 10, 7], {\n  each: [\n    { assertions: [-\u003e x, _ {\n      raise Nakischema::Error.new \"#{x} is too much\" unless x \u003c= 5\n      true\n    } ] },\n    10..10,\n  ],\n}\n```\n```none\nexpected at least one of 2 rules to match the 7, errors: (Nakischema::Error)\n  7 is too much (at [:\"#2\", :\"variant#0\", :\"assertion#0\"])\n  expected 10..10 != 7 (at [:\"#2\", :\"variant#1\"])\n```\n\n## Why such stupid name?\n\nInitially I wanted to call it something like \"SchemaValidator\" but:\n\n```none\n$ gem search schema | grep valid\n...\nschema-validator (0.0.1)\nschema_validations (2.3.0)\nschema_validator (0.1.1)\nvalidates_by_schema (0.4.0)\nvalidates_schema (1.1.3)\n...\n$ gem search schema | wc -l\n288\n```\n\n## TODO\n\n* make some tests and GitHub Action for them\n* consider replacing `.is_a?` with `.respond_to?`\n* ability to assert at the same time the range and class (like is it's float or integer)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FNakilon%2Fnakischema","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FNakilon%2Fnakischema","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FNakilon%2Fnakischema/lists"}