{"id":15562074,"url":"https://github.com/hakanensari/structure","last_synced_at":"2025-10-12T19:31:24.912Z","repository":{"id":59156708,"uuid":"1154184","full_name":"hakanensari/structure","owner":"hakanensari","description":"Turn hashes into data objects","archived":false,"fork":false,"pushed_at":"2025-10-08T15:44:37.000Z","size":665,"stargazers_count":29,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-08T17:48:54.803Z","etag":null,"topics":["data-modeling","ruby","value-object"],"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/hakanensari.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2010-12-09T19:40:38.000Z","updated_at":"2025-10-08T15:44:40.000Z","dependencies_parsed_at":"2025-09-11T13:04:52.323Z","dependency_job_id":"d48988e4-fc2f-4e6c-9e3f-480742d6c6eb","html_url":"https://github.com/hakanensari/structure","commit_stats":null,"previous_names":[],"tags_count":89,"template":false,"template_full_name":null,"purl":"pkg:github/hakanensari/structure","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hakanensari%2Fstructure","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hakanensari%2Fstructure/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hakanensari%2Fstructure/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hakanensari%2Fstructure/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hakanensari","download_url":"https://codeload.github.com/hakanensari/structure/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hakanensari%2Fstructure/sbom","scorecard":{"id":453175,"data":{"date":"2025-08-11","repo":{"name":"github.com/hakanensari/structure","commit":"145f75df6d116471c3db128a13c7edf1907bd6e1"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":4.4,"checks":[{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Maintained","score":10,"reason":"20 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: jobLevel 'contents' permission set to 'write': .github/workflows/ci.yml:36","Info: topLevel 'contents' permission set to 'read': .github/workflows/ci.yml:6","Warn: topLevel 'contents' permission set to 'write': .github/workflows/dependabot-auto-merge.yml:6"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/hakanensari/structure/ci.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/hakanensari/structure/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:25: update your workflow using https://app.stepsecurity.io/secureworkflow/hakanensari/structure/ci.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yml:26: update your workflow using https://app.stepsecurity.io/secureworkflow/hakanensari/structure/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:38: update your workflow using https://app.stepsecurity.io/secureworkflow/hakanensari/structure/ci.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yml:43: update your workflow using https://app.stepsecurity.io/secureworkflow/hakanensari/structure/ci.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/dependabot-auto-merge.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/hakanensari/structure/dependabot-auto-merge.yml/main?enable=pin","Info:   0 out of   3 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   4 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 1 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-19T08:44:15.567Z","repository_id":59156708,"created_at":"2025-08-19T08:44:15.567Z","updated_at":"2025-08-19T08:44:15.567Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279012668,"owners_count":26085159,"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","status":"online","status_checked_at":"2025-10-12T02:00:06.719Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["data-modeling","ruby","value-object"],"created_at":"2024-10-02T16:11:21.373Z","updated_at":"2025-10-12T19:31:24.905Z","avatar_url":"https://github.com/hakanensari.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Structure\n\n[![CI/CD Pipeline](https://github.com/hakanensari/structure/actions/workflows/ci.yml/badge.svg)](https://github.com/hakanensari/structure/actions/workflows/ci.yml)\n\n![Ruby](https://raw.githubusercontent.com/hakanensari/structure/refs/heads/main/images/ruby.png)\n\n**Structure your data**\n\nTurn unruly hashes into clean [Ruby Data](https://docs.ruby-lang.org/en/3.4/Data.html) objects with type coercion.\n\n```ruby\n# Before: Hash drilling\nuser_name = response[\"user\"][\"name\"]\nuser_age = response[\"user\"][\"age\"].to_i\nuser_active = response[\"user\"][\"is_active\"] == \"true\"\n\n# After: Clean, typed objects\nuser.name     # =\u003e \"Alice\" (String)\nuser.age      # =\u003e 25 (Integer)\nuser.active?  # =\u003e true\n```\n\nBuilt on [Ruby Data](https://docs.ruby-lang.org/en/3.4/Data.html) for immutability, pattern matching, and all the other good stuff. Zero dependencies.\n\n## Installation\n\nAdd to your Gemfile:\n\n```ruby\ngem \"structure\"\n```\n\n## Usage\n\n### The Basics\n\n```ruby\nUser = Structure.new do\n  attribute(:name, String)\n  attribute(:age, Integer)\n  attribute(:active, :boolean)\nend\n\nuser = User.parse({\n  \"name\" =\u003e \"Alice\",\n  \"age\" =\u003e \"25\",\n  \"active\" =\u003e \"true\"\n})\n\nuser.name     # =\u003e \"Alice\" (String)\nuser.age      # =\u003e 25 (Integer)\nuser.active   # =\u003e true (TrueClass)\nuser.active?  # =\u003e true (predicate method)\n```\n\n### Type Coercion\n\nUses Ruby's built-in coercion methods to convert data:\n\n```ruby\nProduct = Structure.new do\n  attribute(:title, String)      # Uses String(val)\n  attribute(:price, Float)       # Uses Float(val)\n  attribute(:quantity, Integer)  # Uses Integer(val)\n  attribute(:available, :boolean) # Custom boolean logic\nend\n\nproduct = Product.parse({\n  \"title\" =\u003e 123,\n  \"price\" =\u003e \"19.99\",\n  \"quantity\" =\u003e \"5\",\n  \"available\" =\u003e \"1\"\n})\n\nproduct.title     # =\u003e \"123\"\nproduct.price     # =\u003e 19.99\nproduct.quantity  # =\u003e 5\nproduct.available # =\u003e true\n```\n\n### Key Mapping\n\nClean up gnarly keys:\n\n```ruby\nPerson = Structure.new do\n  attribute(:name, String, from: \"full_name\")\n  attribute(:active, :boolean, from: \"is_active\")\nend\n\nperson = Person.parse({\n  \"full_name\" =\u003e \"Bob Smith\",\n  \"is_active\" =\u003e \"true\"\n})\n\nperson.name    # =\u003e \"Bob Smith\"\nperson.active? # =\u003e true\n```\n\n### Optional Attributes\n\nStructure wraps Data classes. All attributes are required when creating instances, even if their value is `nil`.\n\n```ruby\nUser = Structure.new do\n  attribute(:name, String)\n  attribute(:age, Integer)\nend\n\nUser.parse(name: \"Alice\", age: 30) # Works\nUser.parse(name: nil, age: nil)    # Works, nil values allowed\nUser.parse(name: \"Alice\")          # ArgumentError: missing keyword: :age\n```\n\nUse `attribute?` to make attributes truly optional. The key can then be omitted entirely.\n\n```ruby\nUser = Structure.new do\n  attribute(:name, String)\n  attribute?(:age, Integer)\nend\n\n# Now you can omit the optional attribute\nUser.parse(name: \"Bob\")            # Works, age defaults to nil\n\n# You still must provide regular attributes\nUser.parse(age: 10)                # ArgumentError: missing keyword: :name\n```\n\n### Default Values\n\nHandle missing data:\n\n```ruby\nConfig = Structure.new do\n  attribute(:timeout, Integer, default: 30)\n  attribute(:debug, :boolean, default: false)\nend\n\nconfig = Config.parse({})  # Empty data\n\nconfig.timeout # =\u003e 30\nconfig.debug   # =\u003e false\n```\n\n### Array Types\n\nArrays with automatic element coercion:\n\n```ruby\nOrder = Structure.new do\n  attribute(:items, [String])\n  attribute(:quantities, [Integer])\n  attribute(:flags, [:boolean])\nend\n\norder = Order.parse({\n  \"items\" =\u003e [123, 456, \"hello\"],\n  \"quantities\" =\u003e [\"1\", \"2\", 3.5],\n  \"flags\" =\u003e [\"true\", 0, 1, \"false\"]\n})\n\norder.items      # =\u003e [\"123\", \"456\", \"hello\"]\norder.quantities # =\u003e [1, 2, 3]\norder.flags      # =\u003e [true, false, true, false]\n```\n\n### Nested Objects\n\nCompose structures for complex data:\n\n```ruby\nAddress = Structure.new do\n  attribute(:street, String)\n  attribute(:city, String)\nend\n\nUser = Structure.new do\n  attribute(:name, String)\n  attribute(:address, Address)\nend\n\nuser = User.parse({\n  \"name\" =\u003e \"Alice\",\n  \"address\" =\u003e {\n    \"street\" =\u003e \"123 Main St\",\n    \"city\" =\u003e \"Boston\"\n  }\n})\n\nuser.name           # =\u003e \"Alice\"\nuser.address.street # =\u003e \"123 Main St\"\nuser.address.city   # =\u003e \"Boston\"\n```\n\n### Arrays of Objects\n\nCombine array syntax with nested objects:\n\n```ruby\nTag = Structure.new do\n  attribute(:name, String)\n  attribute(:color, String)\nend\n\nProduct = Structure.new do\n  attribute(:title, String)\n  attribute(:tags, [Tag])\nend\n\nproduct = Product.parse({\n  \"title\" =\u003e \"Laptop\",\n  \"tags\" =\u003e [\n    { \"name\" =\u003e \"electronics\", \"color\" =\u003e \"blue\" },\n    { \"name\" =\u003e \"computers\", \"color\" =\u003e \"green\" }\n  ]\n})\n\nproduct.title           # =\u003e \"Laptop\"\nproduct.tags.first.name # =\u003e \"electronics\"\n```\n\n### Lazy Resolution\n\nTo handle circular dependencies between classes, you can use string class names that are resolved lazily:\n\n```ruby\nmodule MyApp\n  Order = Structure.new do\n    attribute(:id, String)\n    attribute(:items, [\"OrderItem\"])  # String resolved lazily\n    attribute(:customer, \"Customer\")  # String resolved lazily\n  end\n\n  OrderItem = Structure.new do\n    attribute(:name, String)\n    attribute(:order, \"Order\")  # Circular reference back to Order\n  end\n\n  Customer = Structure.new do\n    attribute(:name, String)\n    attribute(:orders, [\"Order\"])  # Circular reference to Order\n  end\nend\n\n# Works despite circular dependencies\norder = MyApp::Order.parse({\n  \"id\" =\u003e \"123\",\n  \"customer\" =\u003e { \"name\" =\u003e \"Alice\" },\n  \"items\" =\u003e [{ \"name\" =\u003e \"Widget\" }]\n})\n\norder.customer.name      # =\u003e \"Alice\"\norder.items.first.name   # =\u003e \"Widget\"\n```\n\n### Custom Transformations\n\nWhen you need custom logic:\n\n```ruby\nOrder = Structure.new do\n  attribute :price do |value|\n    Money.new(value[\"amount\"], value[\"currency\"])\n  end\nend\n\norder = Order.parse({\n  \"price\" =\u003e { \"amount\" =\u003e \"29.99\", \"currency\" =\u003e \"USD\" }\n})\n\norder.price # =\u003e #\u003cMoney:0x... @amount=\"29.99\", @currency=\"USD\"\u003e\n```\n\n### Boolean Conversion\n\nStructure follows Rails-style boolean conversion:\n\n**Truthy values:** `true`, `1`, `\"1\"`, `\"t\"`, `\"T\"`, `\"true\"`, `\"TRUE\"`, `\"on\"`, `\"ON\"`\n**Falsy values:** Everything else (including `false`, `0`, `\"0\"`, `\"false\"`, `\"\"`, `nil`)\n\n```ruby\nUser = Structure.new do\n  attribute(:active, :boolean)\nend\n\nUser.parse(active: \"true\").active   # =\u003e true\nUser.parse(active: \"1\").active      # =\u003e true\nUser.parse(active: \"false\").active  # =\u003e false\nUser.parse(active: \"0\").active      # =\u003e false\nUser.parse(active: \"\").active       # =\u003e false\n```\n\n### Supported Types\n\nStructure supports Ruby's kernel coercion methods like `String(val)`, `Integer(val)`, `Float(val)`, etc., plus:\n\n- `:boolean` - Custom Rails-style boolean conversion\n- `[Type]` - Arrays with element coercion\n- Custom classes with `.parse` method\n- Ruby standard library classes with `.parse`, including:\n  - `Date` - Parses date strings\n  - `Time` - Parses various time formats\n  - `URI` - Parses URLs into URI objects\n\n```ruby\nEvent = Structure.new do\n  attribute(:name, String)\n  attribute(:date, Date)\n  attribute(:starts_at, Time)\n  attribute(:website, URI)\nend\n\nevent = Event.parse({\n  \"name\" =\u003e \"RubyConf\",\n  \"date\" =\u003e \"2024-12-25\",\n  \"starts_at\" =\u003e \"2024-12-25T09:00:00-05:00\",\n  \"website\" =\u003e \"https://rubyconf.org\"\n})\n\nevent.date      # =\u003e #\u003cDate: 2024-12-25\u003e\nevent.starts_at # =\u003e 2024-12-25 09:00:00 -0500\nevent.website   # =\u003e #\u003cURI::HTTPS https://rubyconf.org\u003e\n```\n\n### Custom Types\n\nThe type system is flexible. Any object that responds to `.call` (procs, lambdas) or `.parse` (classes) can be used as a type:\n\n```ruby\n# Using a lambda for simple transformations\nUppercaseString = -\u003e(val) { val.to_s.upcase }\n\n# Using a class with .parse for complex types\nclass Money\n  def self.parse(data)\n    return nil unless data\n    amount = data.is_a?(Hash) ? data['amount'] : data\n    new(amount.to_f)\n  end\n\n  def initialize(amount)\n    @amount = amount\n  end\n\n  attr_reader :amount\nend\n\nProduct = Structure.new do\n  attribute :name, UppercaseString\n  attribute :price, Money\nend\n\nproduct = Product.parse({\n  \"name\" =\u003e \"widget\",\n  \"price\" =\u003e { \"amount\" =\u003e \"19.99\" }\n})\n\nproduct.name  # =\u003e \"WIDGET\"\nproduct.price.amount # =\u003e 19.99\n```\n\n### Self-Referential Types\n\nBuild tree structures and other self-referential data:\n\n```ruby\nTree = Structure.new do\n  attribute(:id, Integer)\n  attribute(:name, String)\n  attribute(:children, [:self], default: [])\nend\n\ntree = Tree.parse({\n  \"id\" =\u003e 1,\n  \"name\" =\u003e \"Electronics\",\n  \"children\" =\u003e [\n    { \"id\" =\u003e 2, \"name\" =\u003e \"Computers\" },\n    { \"id\" =\u003e 3, \"name\" =\u003e \"Phones\", \"children\" =\u003e [\n      { \"id\" =\u003e 4, \"name\" =\u003e \"Smartphones\" }\n    ]}\n  ]\n})\n\ntree.name                           # =\u003e \"Electronics\"\ntree.children.first.name            # =\u003e \"Computers\"\ntree.children[1].children.first.name # =\u003e \"Smartphones\"\n```\n\nUse `:self` for single references or `[:self]` for arrays of self-references. Perfect for modeling hierarchical data like navigation menus, comment threads, or organizational charts.\n\n### After Parse Callbacks\n\nAdd validation or post-processing logic that runs after parsing:\n\n```ruby\nOrder = Structure.new do\n  attribute(:order_id, String)\n  attribute(:total, Float)\n\n  after_parse do |order|\n    raise \"Order ID is required\" if order.order_id.nil?\n    raise \"Total must be positive\" if order.total \u0026\u0026 order.total \u003c= 0\n  end\nend\n\n# Raises error for invalid data\nOrder.parse(total: -10)  # =\u003e RuntimeError: Total must be positive\n\n# Works fine with valid data\norder = Order.parse(order_id: \"123\", total: 99.99)\norder.order_id  # =\u003e \"123\"\n```\n\nThe `after_parse` callback receives the parsed instance and runs after all attributes have been coerced. Any exception raised prevents the instance from being returned.\n\n### Custom Methods\n\nDefine instance and class methods directly in the Structure block, just like `Data.define`:\n\n```ruby\nUser = Structure.new do\n  attribute(:name, String)\n  attribute(:age, Integer)\n  attribute(:active, :boolean)\n\n  # Instance methods\n  def adult?\n    age \u003e= 18\n  end\n\n  def greeting\n    \"Hello, I'm #{name}\"\n  end\n\n  def status\n    active ? \"online\" : \"offline\"\n  end\n\n  # Class methods\n  def self.create_guest\n    parse(name: \"Guest\", age: 0, active: false)\n  end\nend\n\nuser = User.parse(name: \"Alice\", age: 25, active: true)\nuser.adult?      # =\u003e true\nuser.greeting    # =\u003e \"Hello, I'm Alice\"\nuser.status      # =\u003e \"online\"\n\nguest = User.create_guest\nguest.name       # =\u003e \"Guest\"\nguest.adult?     # =\u003e false\n```\n\nCustom methods work seamlessly with all Structure features including type coercion, key mapping, defaults, optional attributes, nested structures, and arrays:\n\n```ruby\nProduct = Structure.new do\n  attribute(:name, String)\n  attribute(:price, Float)\n  attribute(:tags, [String])\n  attribute?(:discount, Float)\n\n  def discounted_price\n    return price unless discount\n\n    price * (1 - discount)\n  end\n\n  def has_tag?(tag)\n    tags.include?(tag)\n  end\n\n  def self.categories\n    [\"electronics\", \"books\", \"clothing\"]\n  end\nend\n\nproduct = Product.parse(\n  name: \"Laptop\",\n  price: \"999.99\",\n  tags: [\"electronics\", \"computers\"],\n  discount: \"0.1\"\n)\n\nproduct.discounted_price         # =\u003e 899.991\nproduct.has_tag?(\"electronics\")  # =\u003e true\nProduct.categories               # =\u003e [\"electronics\", \"books\", \"clothing\"]\n```\n\n### RBS Type Signatures\n\nGenerate RBS type signatures for your Structure classes:\n\n```ruby\nrequire 'structure/rbs'\n\nUser = Structure.new do\n  attribute(:name, String)\n  attribute(:age, Integer)\n  attribute(:tags, [String])\nend\n\n# Generate RBS content\nStructure::RBS.emit(User)\n# =\u003e class User \u003c Data\n#      def self.new: (name: String?, age: Integer?, tags: Array[String]?) -\u003e instance\n#      def self.parse: (?(Hash[String | Symbol, untyped]), **untyped) -\u003e instance\n#      attr_reader name: String?\n#      attr_reader age: Integer?\n#      attr_reader tags: Array[String]?\n#      ...\n#    end\n\n# Write RBS to file\nStructure::RBS.write(User, dir: \"sig\")  # =\u003e \"sig/user.rbs\"\n```\n\n#### Custom Methods and Steep\n\n`Structure::RBS.emit` generates type signatures for custom methods with parameters and return types defaulting to `untyped`:\n\n```ruby\nUser = Structure.new do\n  attribute(:age, Integer)\n\n  # steep:ignore:start\n  def adult?\n    age \u003e= 18\n  end\n  # steep:ignore:end\nend\n\nStructure::RBS.emit(User)\n# =\u003e ...\n#      def adult?: () -\u003e untyped\n#    end\n```\n\nThe generated signatures work for code that uses your Structure classes, but Steep may report warnings in definition files when custom methods are present. This happens because the `Structure.new` block is evaluated in two different contexts at runtime (once for DSL methods like `attribute`, once for custom methods), but Steep can only analyze one static context. Wrap custom methods with `# steep:ignore:start` and `# steep:ignore:end` comments, or exclude definition files from Steep checking in your `Steepfile`.\n\nSee also: [RBS Data/Struct documentation](https://github.com/ruby/rbs/blob/master/docs/data_and_struct.md), [RBS issue #654](https://github.com/ruby/rbs/issues/654), [RBS issue #1077](https://github.com/ruby/rbs/issues/1077)\n\n## Development\n\n```bash\n$ bundle install\n$ bundle exec rbs collection install\n$ bundle exec rake\n```\n\n### Performance Considerations\n\nString-based method generation with `class_eval` is more performant but also overcomplicates the code. For now, I prioritize legibility.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhakanensari%2Fstructure","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhakanensari%2Fstructure","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhakanensari%2Fstructure/lists"}