{"id":15619200,"url":"https://github.com/yykamei/strong_csv","last_synced_at":"2025-04-28T13:14:32.955Z","repository":{"id":39631870,"uuid":"483677582","full_name":"yykamei/strong_csv","owner":"yykamei","description":"Type checker for a CSV file.","archived":false,"fork":false,"pushed_at":"2025-04-07T14:19:10.000Z","size":187,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-28T13:14:25.054Z","etag":null,"topics":["csv","hacktoberfest","ruby"],"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/yykamei.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","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":"2022-04-20T14:00:07.000Z","updated_at":"2025-04-07T14:19:13.000Z","dependencies_parsed_at":"2025-04-07T15:36:02.221Z","dependency_job_id":null,"html_url":"https://github.com/yykamei/strong_csv","commit_stats":{"total_commits":129,"total_committers":3,"mean_commits":43.0,"dds":0.2790697674418605,"last_synced_commit":"947a27621567755c35a33eec5f3b31856c8ba466"},"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yykamei%2Fstrong_csv","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yykamei%2Fstrong_csv/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yykamei%2Fstrong_csv/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yykamei%2Fstrong_csv/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yykamei","download_url":"https://codeload.github.com/yykamei/strong_csv/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251319593,"owners_count":21570428,"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":["csv","hacktoberfest","ruby"],"created_at":"2024-10-03T08:03:29.289Z","updated_at":"2025-04-28T13:14:32.933Z","avatar_url":"https://github.com/yykamei.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# strong_csv\n\n\u003ca href=\"https://rubygems.org/gems/strong_csv\"\u003e\u003cimg alt=\"strong_csv\" src=\"https://img.shields.io/gem/v/strong_csv\"\u003e\u003c/a\u003e\n\nType checker for a CSV file inspired by [strong_json](https://github.com/soutaro/strong_json).\n\n## Motivation\n\nSome applications have a feature to receive a CSV file uploaded by a user,\nand in general, it needs to validate each cell of the CSV file.\n\nHow should applications validate them?\nOf course, it depends, but there would be common validation logic for CSV files.\nFor example, some columns may have to be integers because of database requirements.\nIt would be cumbersome to write such validations always.\n\nstrong_csv helps you to mitigate such a drudgery by letting you declare desired types beforehand.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"strong_csv\"\n```\n\nAnd then execute:\n\n```console\nbundle\n```\n\nOr install it yourself as:\n\n```console\ngem install strong_csv\n```\n\n## Usage\n\nThe most important APIs of strong_csv are `StrongCSV.new` and `StrongCSV#parse`.\n`StrongCSV.new` lets you declare types for each CSV column with Ruby's block syntax.\nInside the block, you will mainly use `let` and declare types for a column.\n\nAfter defining types, you can parse CSV content with `StrongCSV#parse`.\n`StrongCSV#parse` won't raise errors as possible and just store error messages in its rows.\nThe reason why it won't raise errors is CSV content may contain _invalid_ rows,\nbut sometimes, it makes sense to ignore them and process something for _valid_ rows.\nIf you want to stop all processes with invalid rows,\ncheck whether all rows are valid before proceeding with computation.\n\nHere is an example usage of this gem:\n\n```ruby\nrequire \"strong_csv\"\n\nstrong_csv = StrongCSV.new do\n  let :stock, integer\n  let :tax_rate, float\n  let :name, string(within: 1..255)\n  let :description, string?(within: 1..1000)\n  let :active, boolean\n  let :started_at, time?(format: \"%Y-%m-%dT%H:%M:%S\")\n  let :price, integer, error_message: \"This should be Integer\"\n\n  # Literal declaration\n  let :status, 0..6\n  let :priority, 10, 20, 30, 40, 50\n  let :size, \"S\", \"M\", \"L\" do |value| # The input must be one of \"S\", \"M\", or \"L\", and it will be casted as the returned value of the block.\n    case value\n    when \"S\"\n      1\n    when \"M\"\n      2\n    when \"L\"\n      3\n    end\n  end\n\n  # Regular expressions\n  let :url, %r{\\Ahttps://}\n\n  # Custom validation\n  #\n  # This example sees the database to fetch exactly stored `User` IDs,\n  # and it checks the `:user_id` cell really exists in the `users` table.\n  # `pick` would be useful to avoid N+1 problems.\n  pick :user_id, as: :user_ids do |ids|\n    User.where(id: ids).ids\n  end\n  let :user_id, integer(constraint: -\u003e(i) { user_ids.include?(i) })\nend\n\ndata = \u003c\u003c~CSV\n  stock,tax_rate,name,active,status,priority,size,url,price\n  12,0.8,special item,True,4,20,M,https://example.com,PRICE\nCSV\n\nstrong_csv.parse(data, field_size_limit: 2048) do |row|\n  if row.valid?\n    row[:tax_rate] # =\u003e 0.8\n    row[:active] # =\u003e true\n    # do something with row\n  else\n    row.errors # =\u003e {:price=\u003e[\"This should be Integer\"], :user_id=\u003e[\"`nil` can't be casted to Integer\"]}\n    # do something with row.errors\n  end\nend\n```\n\nYou can also define types without CSV headers by specifying column numbers.\n\n\u003e [!NOTE]\n\u003e The numbers must start from `0` (zero-based index).\n\n```ruby\nStrongCSV.new do\n  let 0, integer\n  let 1, string\n  let 2, 1..10\nend\n```\n\nThis declaration expects a CSV has the contents like this:\n\n```csv\n123,abc,3\n830,mno,10\n```\n\n## Available types\n\n\u003ctable\u003e\n    \u003ctr\u003e\n        \u003cth\u003eType\u003c/th\u003e\n        \u003cth\u003eDescription\u003c/th\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e\u003ca href=\"#integer-and-integer\"\u003e\u003ccode\u003einteger\u003c/code\u003e and \u003ccode\u003einteger?\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n        \u003ctd\u003eThe value must be casted to \u003ccode\u003eInteger\u003c/code\u003e.\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e\u003ca href=\"#float-and-float\"\u003e\u003ccode\u003efloat\u003c/code\u003e and \u003ccode\u003efloat?\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n        \u003ctd\u003eThe value must be casted to \u003ccode\u003eFloat\u003c/code\u003e.\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e\u003ca href=\"#boolean-and-boolean\"\u003e\u003ccode\u003eboolean\u003c/code\u003e and \u003ccode\u003eboolean?\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n        \u003ctd\u003eThe value must be casted to Boolean (\u003ccode\u003etrue\u003c/code\u003e or \u003ccode\u003efalse\u003c/code\u003e).\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e\u003ca href=\"#string-and-string\"\u003e\u003ccode\u003estring\u003c/code\u003e and \u003ccode\u003estring?\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n        \u003ctd\u003eThe value must be casted to \u003ccode\u003eString\u003c/code\u003e.\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e\u003ca href=\"#time-and-time\"\u003e\u003ccode\u003etime\u003c/code\u003e and \u003ccode\u003etime?\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n        \u003ctd\u003eThe value must be casted to \u003ccode\u003eTime\u003c/code\u003e.\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e\u003ca href=\"#optional\"\u003e\u003ccode\u003eoptional\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n        \u003ctd\u003eThe value can be \u003ccode\u003enil\u003c/code\u003e. If the value exists, it must satisfy the given type constraint.\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e\u003ca href=\"#literal\"\u003e\u003ccode\u003e23\u003c/code\u003e (Integer literal)\u003c/a\u003e\u003c/td\u003e\n        \u003ctd\u003eThe value must be casted to the specific \u003ccode\u003eInteger\u003c/code\u003e literal.\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e\u003ca href=\"#literal\"\u003e\u003ccode\u003e15.12\u003c/code\u003e (Float literal)\u003c/a\u003e\u003c/td\u003e\n        \u003ctd\u003eThe value must be casted to the specific \u003ccode\u003eFloat\u003c/code\u003e literal.\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e\u003ca href=\"#literal\"\u003e\u003ccode\u003e1..10\u003c/code\u003e (Range literal)\u003c/a\u003e\u003c/td\u003e\n        \u003ctd\u003eThe value must be casted to the beginning of \u003ccode\u003eRange\u003c/code\u003e and be covered with it.\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e\u003ca href=\"#literal\"\u003e\u003ccode\u003e\"abc\"\u003c/code\u003e (String literal)\u003c/a\u003e\u003c/td\u003e\n        \u003ctd\u003eThe value must be casted to the specific \u003ccode\u003eString\u003c/code\u003e literal.\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e\u003ca href=\"#literal\"\u003e\u003ccode\u003e%r{\\Ahttps://}\u003c/code\u003e (Regexp literal)\u003c/a\u003e\u003c/td\u003e\n        \u003ctd\u003eThe value must be casted to a \u003ccode\u003eString\u003c/code\u003e that matches the specified Regexp.\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n        \u003ctd\u003e\u003ca href=\"#union\"\u003e\u003ccode\u003e,\u003c/code\u003e (Union type)\u003c/a\u003e\u003c/td\u003e\n        \u003ctd\u003eThe value must satisfy one of the subtypes.\u003c/td\u003e\n    \u003c/tr\u003e\n\u003c/table\u003e\n\n### `integer` and `integer?`\n\nThe value must be casted to Integer. `integer?` allows the value to be `nil`, so you can declare optional integer type\nfor columns. It also lets you allow values that satisfy the specified limitation through `:constraint`.\n\n_Example_\n\n```ruby\nstrong_csv = StrongCSV.new do\n  let :stock, integer\n  let :state, integer?\n  let :user_id, integer(constraint: -\u003e(v) { user_ids.include?(v)})\n  pick :user_id, as: :user_ids do |values|\n    User.where(id: values).ids\n  end\nend\n\nresult = strong_csv.parse(\u003c\u003c~CSV)\n  stock,state,user_id\n  12,0,1\n  20,,2\n  non-integer,1,4\nCSV\n\nresult.map(\u0026:valid?) # =\u003e [true, true, false]\nresult[0].slice(:stock, :state, :user_id) # =\u003e {:stock=\u003e12, :state=\u003e0, :user_id=\u003e1}\nresult[1].slice(:stock, :state, :user_id) # =\u003e {:stock=\u003e20, :state=\u003enil, :user_id=\u003e2}\nresult[2].slice(:stock, :state, :user_id) # =\u003e {:stock=\u003e\"non-integer\", :state=\u003e1, :user_id=\u003e\"4\"}\nresult[2].errors.slice(:stock, :user_id) # =\u003e {:stock=\u003e[\"`\\\"non-integer\\\"` can't be casted to Integer\"], :user_id=\u003e[\"`\\\"4\\\"` does not satisfy the specified constraint\"]}\n```\n\n### `float` and `float?`\n\nThe value must be casted to Float. `float?` allows the value to be `nil`, so you can declare optional float type for\ncolumns. It also lets you allow values that satisfy the specified limitation through `:constraint`.\n\n_Example_\n\n```ruby\nstrong_csv = StrongCSV.new do\n  let :tax_rate, float\n  let :fail_rate, float?\nend\n\nresult = strong_csv.parse(\u003c\u003c~CSV)\n  tax_rate,fail_rate\n  0.02,0.1\n  0.05,\n  ,0.8\nCSV\n\nresult.map(\u0026:valid?) # =\u003e [true, true, false]\nresult[0].slice(:tax_rate, :fail_rate) # =\u003e {:tax_rate=\u003e0.02, :fail_rate=\u003e0.1}\nresult[1].slice(:tax_rate, :fail_rate) # =\u003e {:tax_rate=\u003e0.05, :fail_rate=\u003enil}\nresult[2].slice(:tax_rate, :fail_rate) # =\u003e {:tax_rate=\u003enil, :fail_rate=\u003e0.8} (`nil` is not allowed for `tax_rate`)\n```\n\n### `boolean` and `boolean?`\n\nThe value must be casted to Boolean (`true` of `false`).\n`\"true\"`, `\"True\"`, and `\"TRUE\"` are casted to `true`,\nwhile `\"false\"`, `\"False\"`, and `\"FALSE\"` are casted to `false`.\n`boolean?` allows the value to be `nil` as an optional boolean\nvalue.\n\n_Example_\n\n```ruby\nstrong_csv = StrongCSV.new do\n  let :enabled, boolean\n  let :active, boolean?\nend\n\nresult = strong_csv.parse(\u003c\u003c~CSV)\n  enabled,active\n  True,True\n  False,\n  ,\nCSV\n\nresult.map(\u0026:valid?) # =\u003e [true, true, false]\nresult[0].slice(:enabled, :active) # =\u003e {:enabled=\u003etrue, :active=\u003etrue}\nresult[1].slice(:enabled, :active) # =\u003e {:enabled=\u003efalse, :active=\u003enil}\nresult[2].slice(:enabled, :active) # =\u003e {:enabled=\u003enil, :active=\u003enil} (`nil` is not allowed for `enabled`)\n```\n\n### `string` and `string?`\n\nThe value must be casted to String. `string?` allows the value to be `nil` as an optional string value.\nThey also support `:within` in its arguments, and it limits the length of the string value within the specified `Range`.\n\n_Example_\n\n```ruby\nstrong_csv = StrongCSV.new do\n  let :name, string(within: 1..4)\n  let :description, string?\nend\n\nresult = strong_csv.parse(\u003c\u003c~CSV)\n  name,description\n  JB,Hello\n  yykamei,\n  ,🤷\nCSV\n\nresult.map(\u0026:valid?) # =\u003e [true, false, false]\nresult[0].slice(:name, :description) # =\u003e {:name=\u003e\"JB\", :description=\u003e\"Hello\"}\nresult[1].slice(:name, :description) # =\u003e {:name=\u003e\"yykamei\", :description=\u003enil} (\"yykamei\" exceeds the `Range` specified with `:within`)\nresult[2].slice(:name, :description) # =\u003e {:name=\u003enil, :description=\u003e\"🤷\"} (`nil` is not allowed for `name`)\n```\n\n### `time` and `time?`\n\nThe value must be casted to Time. `time?` allows the value to be `nil` as an optional time value.\nThey have the `:format` argument, which is used as the format\nof [`Time.strptime`](https://rubydoc.info/stdlib/time/Time.strptime);\nit means you can ensure the value must satisfy the time format. The default value of `:format` is `\"%Y-%m-%d\"`.\n\n_Example_\n\n```ruby\nstrong_csv = StrongCSV.new do\n  let :start_on, time\n  let :updated_at, time?(format: \"%FT%T\")\nend\n\nresult = strong_csv.parse(\u003c\u003c~CSV)\n  start_on,updated_at\n  2022-04-01,2022-04-30T15:30:59\n  2022-05-03\n  05-03,2021-09-03T09:48:23\nCSV\n\nresult.map(\u0026:valid?) # =\u003e [true, true, false]\nresult[0].slice(:start_on, :updated_at) # =\u003e {:start_on=\u003e2022-04-01 00:00:00 +0900, :updated_at=\u003e2022-04-30 15:30:59 +0900}\nresult[1].slice(:start_on, :updated_at) # =\u003e {:start_on=\u003e2022-05-03 00:00:00 +0900, :updated_at=\u003enil}\nresult[2].slice(:start_on, :updated_at) # =\u003e {:start_on=\u003e\"05-03\", :updated_at=\u003e2021-09-03 09:48:23 +0900} (\"05-03\" does not satisfy the default format `\"%Y-%m-%d\"`)\n```\n\n### `optional`\n\nWhile each type above has its optional type with `?`, literals cannot be suffixed with `?`.\nHowever, there would be a case to have an optional literal type.\nIn this case, `optional` might be useful and lets you declare such types.\n\n_Example_\n\n```ruby\nstrong_csv = StrongCSV.new do\n  let :foo, optional(123)\n  let :bar, optional(\"test\")\nend\n\nresult = strong_csv.parse(\u003c\u003c~CSV)\n  foo,bar\n  123,test\n  ,\n  124\nCSV\n\nresult.map(\u0026:valid?) # =\u003e [true, true, false]\nresult[0].slice(:foo, :bar) # =\u003e {:foo=\u003e123, :bar=\u003e\"test\"}\nresult[1].slice(:foo, :bar) # =\u003e {:foo=\u003enil, :bar=\u003enil}\nresult[2].slice(:foo, :bar) # =\u003e {:foo=\u003e\"124\", :bar=\u003enil} (124 is not equal to 123)\n```\n\n### Literal\n\nYou can declare literal value as types. The supported literals are `Integer`, `Float`, `String`, and `Range`.\n\n_Example_\n\n```ruby\nstrong_csv = StrongCSV.new do\n  let 0, 123\n  let 1, \"test\"\n  let 2, 2.5\n  let 3, 1..10\n  let 4, /[a-z]+/\nend\n\nresult = strong_csv.parse(\u003c\u003c~CSV)\n  123,test,2.5,9,abc\n  123,test,2.5,0,xyz\n  123,Hey,2.5,10,!\nCSV\n\nresult.map(\u0026:valid?) # =\u003e [true, false, false]\nresult[0].slice(0, 1, 2, 3, 4) # =\u003e {0=\u003e123, 1=\u003e\"test\", 2=\u003e2.5, 3=\u003e9, 4=\u003e\"abc\"}\nresult[1].slice(0, 1, 2, 3, 4) # =\u003e {0=\u003e123, 1=\u003e\"test\", 2=\u003e2.5, 3=\u003e\"0\", 4=\u003e\"xyz\"} (0 is out of 1..10)\nresult[2].slice(0, 1, 2, 3, 4) # =\u003e {0=\u003e123, 1=\u003e\"Hey\", 2=\u003e2.5, 3=\u003e10, 4=\u003e\"!\"} (\"Hey\" is not equal to \"test\", and \"!\" does not match /[a-z]+/)\n```\n\n### Union\n\nThere would be a case that it's alright if a value satisfies one of the types.\nUnion types are useful for such a case.\n\n_Example_\n\n```ruby\nstrong_csv = StrongCSV.new do\n  let :priority, 10, 20, 30\n  let :size, \"S\", \"M\", \"L\"\nend\n\nresult = strong_csv.parse(\u003c\u003c~CSV)\n  priority,size\n  10,M\n  30,A\n  11,S\nCSV\n\nresult.map(\u0026:valid?) # =\u003e [true, false, false]\nresult[0].slice(:priority, :size) # =\u003e {:priority=\u003e10, :size=\u003e\"M\"}\nresult[1].slice(:priority, :size) # =\u003e {:priority=\u003e30, :size=\u003e\"A\"} (\"A\" is not one of \"S\", \"M\", and \"L\")\nresult[2].slice(:priority, :size) # =\u003e {:priority=\u003e\"11\", :size=\u003e\"S\"} (11 is not one of 10, 20, and 30)\n```\n\n## Contributing\n\nBug reports and pull requests are welcome on the [GitHub repository](https://github.com/yykamei/strong_csv).\nThis project is intended to be a safe, welcoming space for collaboration,\nand contributors are expected to adhere to the\n[code of conduct](https://github.com/yykamei/strong_csv/blob/main/CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyykamei%2Fstrong_csv","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyykamei%2Fstrong_csv","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyykamei%2Fstrong_csv/lists"}